Merge "Use a bool for ForceDefaults instead of an int"
diff --git a/WORKSPACE b/WORKSPACE
index 4bb6551..fb6e2cb 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -406,7 +406,7 @@
     path = "third_party/ceres",
 )
 
-# Downloaded from http://devsite.ctr-electronics.com/maven/release/com/ctre/frcbeta/phoenix/api-cpp/5.17.1/.
+# Downloaded from http://devsite.ctr-electronics.com/maven/release/com/ctre/phoenix/api-cpp/5.18.1/.
 http_archive(
     name = "ctre_phoenix_api_cpp_headers",
     build_file_content = """
@@ -416,13 +416,13 @@
     hdrs = glob(['ctre/phoenix/**/*.h']),
 )
 """,
-    sha256 = "b75761d13e367ece7a114237fc68670ed3b2f39daa4d4ff2a67f9e254d2ed39b",
+    sha256 = "230ff927e36b2f75e746da0f0bf9852e5a049bb3e95c4617138ef0618b2e80d9",
     urls = [
-        "http://www.frc971.org/Build-Dependencies/api-cpp-5.17.1-headers.zip",
+        "http://www.frc971.org/Build-Dependencies/api-cpp-5.18.1-headers.zip",
     ],
 )
 
-# Downloaded from http://devsite.ctr-electronics.com/maven/release/com/ctre/frcbeta/phoenix/api-cpp/5.17.1/.
+# Downloaded from http://devsite.ctr-electronics.com/maven/release/com/ctre/phoenix/api-cpp/5.18.1/.
 http_archive(
     name = "ctre_phoenix_api_cpp_athena",
     build_file_content = """
@@ -433,16 +433,17 @@
     restricted_to = ['@//tools:roborio'],
     deps = [
       '@ctre_phoenix_core_headers//:core',
+      '@ctre_phoenix_core_athena//:core',
     ],
 )
 """,
-    sha256 = "5678a1c6bf2af859bc5783040908b571dd1da63c6b1b5196610aa0cfa35ff9c3",
+    sha256 = "a4de1930e1e946e1c72c13ee272dae38c12c4b7b85b44477dbb67312724d96b1",
     urls = [
-        "http://www.frc971.org/Build-Dependencies/api-cpp-5.17.1-linuxathenastatic.zip",
+        "http://www.frc971.org/Build-Dependencies/api-cpp-5.18.1-linuxathenastatic.zip",
     ],
 )
 
-# Downloaded from http://devsite.ctr-electronics.com/maven/release/com/ctre/frcbeta/phoenix/diagnostics/5.17.1/.
+# Downloaded from http://devsite.ctr-electronics.com/maven/release/com/ctre/phoenix/diagnostics/5.18.1/.
 http_archive(
     name = "ctre_phoenix_diagnostics_headers",
     build_file_content = """
@@ -452,13 +453,13 @@
     hdrs = glob(['ctre/phoenix/**/*.h']),
 )
 """,
-    sha256 = "c922f635df06ad7b2d8b2b3e72ce166a2238a9b28b7040a2963ed15fb61ec102",
+    sha256 = "a94bff6c241de8dc2396a1cece9e2822fe4a7e4980aedaaea682c3e8c5de008c",
     urls = [
-        "http://www.frc971.org/Build-Dependencies/diagnostics-5.17.1-headers.zip",
+        "http://www.frc971.org/Build-Dependencies/diagnostics-5.18.1-headers.zip",
     ],
 )
 
-# Downloaded from http://devsite.ctr-electronics.com/maven/release/com/ctre/frcbeta/phoenix/diagnostics/5.17.1/.
+# Downloaded from http://devsite.ctr-electronics.com/maven/release/com/ctre/phoenix/diagnostics/5.18.1/.
 http_archive(
     name = "ctre_phoenix_diagnostics_athena",
     build_file_content = """
@@ -467,15 +468,19 @@
     visibility = ['//visibility:public'],
     srcs = ['linux/athena/static/libCTRE_PhoenixDiagnostics.a'],
     restricted_to = ['@//tools:roborio'],
+    deps = [
+      '@ctre_phoenix_core_headers//:core',
+      '@ctre_phoenix_core_athena//:core',
+    ],
 )
 """,
-    sha256 = "d59c3dd4d841d769ba509b0ce993355745eb6ca1c86a660b476bf5d9c2532a9e",
+    sha256 = "638a4a4d7400942baa040619ea6cde2bdef0e7721300a9427424a577ce0f56db",
     urls = [
-        "http://www.frc971.org/Build-Dependencies/diagnostics-5.17.1-linuxathenastatic.zip",
+        "http://www.frc971.org/Build-Dependencies/diagnostics-5.18.1-linuxathenastatic.zip",
     ],
 )
 
-# Downloaded from http://devsite.ctr-electronics.com/maven/release/com/ctre/frcbeta/phoenix/cci/5.17.1/.
+# Downloaded from http://devsite.ctr-electronics.com/maven/release/com/ctre/phoenix/cci/5.18.1/.
 http_archive(
     name = "ctre_phoenix_cci_headers",
     build_file_content = """
@@ -485,13 +490,13 @@
     hdrs = glob(['ctre/phoenix/**/*.h']),
 )
 """,
-    sha256 = "d43f6db7aa5165123e222568bdae794a182622d5a71181def355c7c08733dc7f",
+    sha256 = "31e4d8f7fd9612ba687661e19aabc3d89dc076f66756d4696aa7799f31bbc72f",
     urls = [
-        "http://www.frc971.org/Build-Dependencies/cci-5.17.1-headers.zip",
+        "http://www.frc971.org/Build-Dependencies/cci-5.18.1-headers.zip",
     ],
 )
 
-# Downloaded from http://devsite.ctr-electronics.com/maven/release/com/ctre/frcbeta/phoenix/cci/5.17.1/.
+# Downloaded from http://devsite.ctr-electronics.com/maven/release/com/ctre/phoenix/cci/5.18.1/.
 http_archive(
     name = "ctre_phoenix_cci_athena",
     build_file_content = """
@@ -502,13 +507,13 @@
     restricted_to = ['@//tools:roborio'],
 )
 """,
-    sha256 = "8dcf5a2b55747f8dc23556d61f1f6a7d5419e7c3336de97afa30dc89e07c6861",
+    sha256 = "e5d9b58072002dbd2daa8cc8d42e047e5c90d26bd5a2b1d63dc1b89112ac3837",
     urls = [
-        "http://www.frc971.org/Build-Dependencies/cci-5.17.1-linuxathenastatic.zip",
+        "http://www.frc971.org/Build-Dependencies/cci-5.18.1-linuxathenastatic.zip",
     ],
 )
 
-# Downloaded from http://devsite.ctr-electronics.com/maven/release/com/ctre/frcbeta/phoenix/core/5.17.1/.
+# Downloaded from http://devsite.ctr-electronics.com/maven/release/com/ctre/phoenix/core/5.18.1/.
 http_archive(
     name = "ctre_phoenix_core_headers",
     build_file_content = """
@@ -518,9 +523,26 @@
     hdrs = glob(['ctre/phoenix/**/*.h']),
 )
 """,
-    sha256 = "552589ce2aebea1c6112babe3dd7476611eab1d8a0e48f777bd5c421f76857df",
+    sha256 = "af2db0f9c3693cbb74216882ee140e4d6b722a416f2d384062378a8ae37f65ee",
     urls = [
-        "http://www.frc971.org/Build-Dependencies/core-5.17.1-headers.zip",
+        "http://www.frc971.org/Build-Dependencies/core-5.18.1-headers.zip",
+    ],
+)
+
+# Downloaded from http://devsite.ctr-electronics.com/maven/release/com/ctre/phoenix/core/5.18.1/.
+http_archive(
+    name = "ctre_phoenix_core_athena",
+    build_file_content = """
+cc_library(
+    name = 'core',
+    visibility = ['//visibility:public'],
+    srcs = ['linux/athena/static/libCTRE_PhoenixCore.a'],
+    restricted_to = ['@//tools:roborio'],
+)
+""",
+    sha256 = "cd827bc68c0f4ef2fe6c363a7f9f5a08f7d944b574c65a2c7fb823686501f43f",
+    urls = [
+        "http://www.frc971.org/Build-Dependencies/core-5.18.1-linuxathenastatic.zip",
     ],
 )
 
diff --git a/frc971/analysis/plot.py b/frc971/analysis/plot.py
index 1eba716..ecb23b3 100644
--- a/frc971/analysis/plot.py
+++ b/frc971/analysis/plot.py
@@ -28,36 +28,124 @@
         for channel in self.config.channel:
             if channel.alias in aliases:
                 raise ValueError("Duplicate alias " + channel.alias)
-            aliases.add(channel.alias)
             if not self.reader.subscribe(channel.name, channel.type):
-                raise ValueError("No such channel with name " + channel.name +
-                                 " and type " + channel.type)
+                print("Warning: No such channel with name " + channel.name +
+                      " and type " + channel.type)
+                continue
+            aliases.add(channel.alias)
 
         self.reader.process()
 
         for channel in self.config.channel:
             self.data[channel.alias] = []
+            if channel.alias not in aliases:
+                print("Unable to plot channel alias " + channel.alias)
+                continue
             for message in self.reader.get_data_for_channel(
                     channel.name, channel.type):
                 valid_json = message[2].replace('nan', '"nan"')
-                parsed_json = json.loads(valid_json)
-                self.data[channel.alias].append((message[0], message[1],
-                                                 parsed_json))
+                valid_json = valid_json.replace(' inf', ' "inf"')
+                valid_json = valid_json.replace('-inf', '"-inf"')
+                try:
+                    parsed_json = json.loads(valid_json)
+                except json.decoder.JSONDecodeError as ex:
+                    print("JSON Decode failed:")
+                    print(valid_json)
+                    raise ex
+                self.data[channel.alias].append(
+                    (message[0], message[1], parsed_json))
         self.calculate_signals()
 
+    def calculate_down_estimator_signals(self):
+        if 'DrivetrainStatus' in self.data:
+            # Calculates a rolling mean of the acceleration output from
+            # the down estimator.
+            entries = []
+            buffer_len = 100
+            last_100_accels = np.zeros((buffer_len, 3))
+            accels_next_row = 0
+            for entry in self.data['DrivetrainStatus']:
+                msg = entry[2]
+                new_msg = {}
+                if 'down_estimator' not in msg:
+                    continue
+                down_estimator = msg['down_estimator']
+                new_msg['down_estimator'] = {}
+                accel_x = 'accel_x'
+                accel_y = 'accel_y'
+                accel_z = 'accel_z'
+                if (accel_x in down_estimator and accel_y in down_estimator
+                        and accel_x in down_estimator):
+                    last_100_accels[accels_next_row, :] = [
+                        down_estimator[accel_x], down_estimator[accel_y],
+                        down_estimator[accel_z]
+                    ]
+
+                    accels_next_row += 1
+                    accels_next_row = accels_next_row % buffer_len
+                    mean_accels = np.mean(last_100_accels, axis=0)
+                    new_msg['down_estimator'][
+                        'accel_x_rolling_mean'] = mean_accels[0]
+                    new_msg['down_estimator'][
+                        'accel_y_rolling_mean'] = mean_accels[1]
+                    new_msg['down_estimator'][
+                        'accel_z_rolling_mean'] = mean_accels[2]
+                entries.append((entry[0], entry[1], new_msg))
+            if 'CalcDrivetrainStatus' in self.data:
+                raise RuntimeError(
+                    'CalcDrivetrainStatus is already a member of data.')
+            self.data['CalcDrivetrainStatus'] = entries
+
     def calculate_imu_signals(self):
         if 'IMU' in self.data:
             entries = []
+            # Calculates a rolling mean of the raw output from the IMU.
+            buffer_len = 1000
+            last_1000_accels = np.zeros((buffer_len, 3))
+            accels_next_row = 0
+            last_1000_gyros = np.zeros((buffer_len, 3))
+            gyros_next_row = 0
             for entry in self.data['IMU']:
                 accel_x = 'accelerometer_x'
                 accel_y = 'accelerometer_y'
                 accel_z = 'accelerometer_z'
+                gyro_x = 'gyro_x'
+                gyro_y = 'gyro_y'
+                gyro_z = 'gyro_z'
                 msg = entry[2]
                 new_msg = {}
                 if accel_x in msg and accel_y in msg and accel_x in msg:
-                    total_acceleration = np.sqrt(
-                        msg[accel_x]**2 + msg[accel_y]**2 + msg[accel_z]**2)
+                    last_1000_accels[accels_next_row, :] = [
+                        msg[accel_x], msg[accel_y], msg[accel_z]
+                    ]
+                    total_acceleration = np.linalg.norm(
+                        last_1000_accels[accels_next_row, :])
                     new_msg['total_acceleration'] = total_acceleration
+
+                    accels_next_row += 1
+                    accels_next_row = accels_next_row % buffer_len
+                    std_accels = np.std(last_1000_accels, axis=0)
+                    new_msg['accel_x_rolling_std'] = std_accels[0]
+                    new_msg['accel_y_rolling_std'] = std_accels[1]
+                    new_msg['accel_z_rolling_std'] = std_accels[2]
+                    mean_accels = np.mean(last_1000_accels, axis=0)
+                    new_msg['accel_x_rolling_mean'] = mean_accels[0]
+                    new_msg['accel_y_rolling_mean'] = mean_accels[1]
+                    new_msg['accel_z_rolling_mean'] = mean_accels[2]
+                if gyro_x in msg and gyro_y in msg and gyro_z in msg:
+                    last_1000_gyros[gyros_next_row, :] = [
+                        msg[gyro_x], msg[gyro_y], msg[gyro_z]
+                    ]
+                    gyros_next_row += 1
+                    gyros_next_row = gyros_next_row % buffer_len
+                    std_gyros = np.std(last_1000_gyros, axis=0)
+                    new_msg['gyro_x_rolling_std'] = std_gyros[0]
+                    new_msg['gyro_y_rolling_std'] = std_gyros[1]
+                    new_msg['gyro_z_rolling_std'] = std_gyros[2]
+                    mean_gyros = np.mean(last_1000_gyros, axis=0)
+                    new_msg['gyro_x_rolling_mean'] = mean_gyros[0]
+                    new_msg['gyro_y_rolling_mean'] = mean_gyros[1]
+                    new_msg['gyro_z_rolling_mean'] = mean_gyros[2]
                 timestamp = 'monotonic_timestamp_ns'
                 if timestamp in msg:
                     timestamp_sec = msg[timestamp] * 1e-9
@@ -80,6 +168,7 @@
         an overall magnitude for the accelerometer readings, which is helpful
         to understanding how some internal filters work."""
         self.calculate_imu_signals()
+        self.calculate_down_estimator_signals()
 
     def extract_field(self, message: dict, field: str):
         """Extracts a field with the given name from the message.
@@ -139,15 +228,20 @@
             fig = plt.figure()
             num_subplots = len(figure_config.axes)
             for ii in range(num_subplots):
-                axes = fig.add_subplot(
-                    num_subplots, 1, ii + 1, sharex=shared_axis)
-                shared_axis = shared_axis or axes
                 axes_config = figure_config.axes[ii]
+                share_axis = axes_config.share_x_axis
+                axes = fig.add_subplot(
+                    num_subplots,
+                    1,
+                    ii + 1,
+                    sharex=shared_axis if share_axis else None)
+                if share_axis and shared_axis is None:
+                    shared_axis = axes
                 for line in axes_config.line:
                     self.plot_line(axes, line)
                 # Make the legend transparent so we can see behind it.
-                legend = axes.legend(framealpha=0.5)
-                axes.set_xlabel("Monotonic Time (sec)")
+                legend = axes.legend(framealpha=0.5, loc='upper right')
+                axes.set_xlabel(axes_config.xlabel)
                 axes.grid(True)
                 if axes_config.HasField("ylabel"):
                     axes.set_ylabel(axes_config.ylabel)
@@ -156,21 +250,18 @@
 def main(argv):
     parser = argparse.ArgumentParser(
         description="Plot data from an aos logfile.")
-    parser.add_argument(
-        "--logfile",
-        type=str,
-        required=True,
-        help="Path to the logfile to parse.")
-    parser.add_argument(
-        "--config",
-        type=str,
-        required=True,
-        help="Name of the plot config to use.")
-    parser.add_argument(
-        "--config_dir",
-        type=str,
-        default="frc971/analysis/plot_configs",
-        help="Directory to look for plot configs in.")
+    parser.add_argument("--logfile",
+                        type=str,
+                        required=True,
+                        help="Path to the logfile to parse.")
+    parser.add_argument("--config",
+                        type=str,
+                        required=True,
+                        help="Name of the plot config to use.")
+    parser.add_argument("--config_dir",
+                        type=str,
+                        default="frc971/analysis/plot_configs",
+                        help="Directory to look for plot configs in.")
     args = parser.parse_args(argv[1:])
 
     if not os.path.isdir(args.config_dir):
diff --git a/frc971/analysis/plot_config.proto b/frc971/analysis/plot_config.proto
index c4c61c2..cd01f73 100644
--- a/frc971/analysis/plot_config.proto
+++ b/frc971/analysis/plot_config.proto
@@ -45,6 +45,9 @@
 message Axes {
   repeated Line line = 1;
   optional string ylabel = 2;
+  optional string xlabel = 3 [default = "Monotonic Time (sec)"];
+  // Whether to share an x-axis with all the other axes.
+  optional bool share_x_axis = 4 [default = true];
 }
 
 // Message representing a single pyplot figure.
diff --git a/frc971/analysis/plot_configs/down_estimator.pb b/frc971/analysis/plot_configs/down_estimator.pb
new file mode 100644
index 0000000..48cf2ef
--- /dev/null
+++ b/frc971/analysis/plot_configs/down_estimator.pb
@@ -0,0 +1,187 @@
+channel {
+  name: "/drivetrain"
+  type: "frc971.control_loops.drivetrain.Status"
+  alias: "DrivetrainStatus"
+}
+
+figure {
+  axes {
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.yaw"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.lateral_pitch"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.longitudinal_pitch"
+      }
+    }
+    ylabel: "rad"
+  }
+  axes {
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.quaternion_x"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.quaternion_y"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.quaternion_z"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.quaternion_w"
+      }
+    }
+  }
+  axes {
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.accel_x"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.accel_y"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.accel_z"
+      }
+    }
+    ylabel: "m/s/s"
+  }
+  axes {
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.velocity_x"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.velocity_y"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.velocity_z"
+      }
+    }
+  }
+  axes {
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.position_x"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.position_y"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.position_z"
+      }
+    }
+  }
+}
+
+figure {
+  axes {
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "zeroing.gyro_x_average"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "zeroing.gyro_y_average"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "zeroing.gyro_z_average"
+      }
+    }
+  }
+  axes {
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.expected_accel_x"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.expected_accel_y"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.expected_accel_z"
+      }
+    }
+  }
+  axes {
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.gravity_magnitude"
+      }
+    }
+  }
+  axes {
+    line {
+      y_signal {
+        channel: "CalcDrivetrainStatus"
+        field: "down_estimator.accel_x_rolling_mean"
+      }
+    }
+    line {
+      y_signal {
+        channel: "CalcDrivetrainStatus"
+        field: "down_estimator.accel_y_rolling_mean"
+      }
+    }
+    line {
+      y_signal {
+        channel: "CalcDrivetrainStatus"
+        field: "down_estimator.accel_z_rolling_mean"
+      }
+    }
+  }
+}
diff --git a/frc971/analysis/plot_configs/drivetrain.pb b/frc971/analysis/plot_configs/drivetrain.pb
index 0ac4fff..0335da8 100644
--- a/frc971/analysis/plot_configs/drivetrain.pb
+++ b/frc971/analysis/plot_configs/drivetrain.pb
@@ -294,4 +294,170 @@
       }
     }
   }
+  axes {
+    line {
+      y_signal {
+        channel: "Status"
+        field: "down_estimator.velocity_x"
+      }
+    }
+    line {
+      y_signal {
+        channel: "Status"
+        field: "down_estimator.velocity_y"
+      }
+    }
+  }
+  axes {
+    line {
+      y_signal {
+        channel: "Status"
+        field: "down_estimator.position_x"
+      }
+    }
+    line {
+      y_signal {
+        channel: "Status"
+        field: "down_estimator.position_y"
+      }
+    }
+  }
+}
+
+figure {
+  axes {
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "accel_x_rolling_mean"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "accel_y_rolling_mean"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "accel_z_rolling_mean"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    ylabel: "g"
+  }
+  axes {
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "accel_x_rolling_std"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "accel_y_rolling_std"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "accel_z_rolling_std"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    ylabel: "g"
+  }
+}
+
+figure {
+  axes {
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "gyro_x_rolling_mean"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "gyro_y_rolling_mean"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "gyro_z_rolling_mean"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    ylabel: "rad / sec"
+  }
+  axes {
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "gyro_x_rolling_std"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "gyro_y_rolling_std"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "gyro_z_rolling_std"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    ylabel: "rad / sec"
+  }
 }
diff --git a/frc971/analysis/plot_configs/gyro.pb b/frc971/analysis/plot_configs/gyro.pb
index 760752c..790fc55 100644
--- a/frc971/analysis/plot_configs/gyro.pb
+++ b/frc971/analysis/plot_configs/gyro.pb
@@ -82,3 +82,157 @@
     ylabel: "g"
   }
 }
+
+figure {
+  axes {
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "accel_x_rolling_mean"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "accel_y_rolling_mean"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "accel_z_rolling_mean"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    ylabel: "g"
+  }
+  axes {
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "accel_x_rolling_std"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "accel_y_rolling_std"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "accel_z_rolling_std"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    ylabel: "g"
+  }
+}
+
+figure {
+  axes {
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "gyro_x_rolling_mean"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "gyro_y_rolling_mean"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "gyro_z_rolling_mean"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    ylabel: "rad / sec"
+  }
+  axes {
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "gyro_x_rolling_std"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "gyro_y_rolling_std"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    line {
+      y_signal {
+        channel: "CalcIMU"
+        field: "gyro_z_rolling_std"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    ylabel: "rad / sec"
+  }
+}
+
+figure {
+  axes {
+    line {
+      y_signal {
+        channel: "IMU"
+        field: "temperature"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    ylabel: "C"
+  }
+}
diff --git a/frc971/analysis/plot_configs/gyro_temp.pb b/frc971/analysis/plot_configs/gyro_temp.pb
new file mode 100644
index 0000000..02c8c3b
--- /dev/null
+++ b/frc971/analysis/plot_configs/gyro_temp.pb
@@ -0,0 +1,21 @@
+channel {
+  name: "/drivetrain"
+  type: "frc971.IMUValues"
+  alias: "IMU"
+}
+
+figure {
+  axes {
+    line {
+      y_signal {
+        channel: "IMU"
+        field: "temperature"
+      }
+      x_signal {
+        channel: "CalcIMU"
+        field: "monotonic_timestamp_sec"
+      }
+    }
+    ylabel: "C"
+  }
+}
diff --git a/frc971/analysis/plot_configs/localizer.pb b/frc971/analysis/plot_configs/localizer.pb
new file mode 100644
index 0000000..f81ec89
--- /dev/null
+++ b/frc971/analysis/plot_configs/localizer.pb
@@ -0,0 +1,259 @@
+channel {
+  name: "/drivetrain"
+  type: "frc971.control_loops.drivetrain.Status"
+  alias: "DrivetrainStatus"
+}
+channel {
+  name: "/drivetrain/truth"
+  type: "frc971.control_loops.drivetrain.Status"
+  alias: "DrivetrainTruthStatus"
+}
+channel {
+  name: "/drivetrain"
+  type: "frc971.control_loops.drivetrain.Position"
+  alias: "DrivetrainPosition"
+}
+channel {
+  name: "/drivetrain"
+  type: "frc971.control_loops.drivetrain.Output"
+  alias: "DrivetrainOutput"
+}
+
+figure {
+  axes {
+    line {
+      y_signal {
+        channel: "DrivetrainTruthStatus"
+        field: "y"
+      }
+      x_signal {
+        channel: "DrivetrainTruthStatus"
+        field: "x"
+      }
+    }
+    share_x_axis: false
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "y"
+      }
+      x_signal {
+        channel: "DrivetrainStatus"
+        field: "x"
+      }
+    }
+    share_x_axis: false
+    xlabel: "x (m)"
+    ylabel: "y (m)"
+  }
+}
+
+figure {
+  axes {
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.yaw"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainTruthStatus"
+        field: "theta"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "localizer.theta"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.lateral_pitch"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.longitudinal_pitch"
+      }
+    }
+    ylabel: "rad"
+  }
+  axes {
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.position_x"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.position_y"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainTruthStatus"
+        field: "x"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainTruthStatus"
+        field: "y"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "localizer.x"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "localizer.y"
+      }
+    }
+    ylabel: "m"
+  }
+  axes {
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.velocity_x"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.velocity_y"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "localizer.longitudinal_velocity_offset"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "localizer.lateral_velocity"
+      }
+    }
+    ylabel: "m/s"
+  }
+  axes {
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.accel_x"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.accel_y"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "down_estimator.accel_z"
+      }
+    }
+    ylabel: "m/s/s"
+  }
+}
+
+figure {
+  axes {
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "localizer.left_velocity"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "localizer.right_velocity"
+      }
+    }
+    ylabel: "m/s"
+  }
+  axes {
+    line {
+      y_signal {
+        channel: "DrivetrainPosition"
+        field: "left_encoder"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainPosition"
+        field: "right_encoder"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "localizer.left_encoder"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "localizer.right_encoder"
+      }
+    }
+    ylabel: "m"
+  }
+  axes {
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "localizer.angular_error"
+      }
+    }
+    ylabel: "rad / sec"
+  }
+}
+
+figure {
+  axes {
+    line {
+      y_signal {
+        channel: "DrivetrainOutput"
+        field: "left_voltage"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainOutput"
+        field: "right_voltage"
+      }
+    }
+    ylabel: "V"
+  }
+  axes {
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "localizer.left_voltage_error"
+      }
+    }
+    line {
+      y_signal {
+        channel: "DrivetrainStatus"
+        field: "localizer.right_voltage_error"
+      }
+    }
+    ylabel: "V"
+  }
+}
diff --git a/frc971/control_loops/drivetrain/BUILD b/frc971/control_loops/drivetrain/BUILD
index 5350240..2c94473 100644
--- a/frc971/control_loops/drivetrain/BUILD
+++ b/frc971/control_loops/drivetrain/BUILD
@@ -98,6 +98,25 @@
 )
 
 aos_config(
+    name = "simulation_channels",
+    src = "drivetrain_simulation_channels.json",
+    flatbuffers = [
+        ":drivetrain_status_fbs",
+    ],
+    visibility = ["//visibility:public"],
+)
+
+aos_config(
+    name = "simulation_config",
+    src = "drivetrain_simulation_config.json",
+    visibility = ["//visibility:public"],
+    deps = [
+        ":config",
+        ":simulation_channels",
+    ],
+)
+
+aos_config(
     name = "config",
     src = "drivetrain_config.json",
     flatbuffers = [
@@ -395,6 +414,13 @@
     testonly = True,
     srcs = ["drivetrain_test_lib.cc"],
     hdrs = ["drivetrain_test_lib.h"],
+    defines =
+        cpu_select({
+            "amd64": [
+                "SUPPORT_PLOT=1",
+            ],
+            "arm": [],
+        }),
     deps = [
         ":drivetrain_config",
         ":drivetrain_goal_fbs",
@@ -410,7 +436,12 @@
         "//frc971/wpilib:imu_fbs",
         "//y2016:constants",
         "//y2016/control_loops/drivetrain:polydrivetrain_plants",
-    ],
+    ] + cpu_select({
+        "amd64": [
+            "//third_party/matplotlib-cpp",
+        ],
+        "arm": [],
+    }),
 )
 
 cc_test(
@@ -418,7 +449,7 @@
     srcs = [
         "drivetrain_lib_test.cc",
     ],
-    data = ["config.json"],
+    data = ["simulation_config.json"],
     defines =
         cpu_select({
             "amd64": [
@@ -437,6 +468,7 @@
         ":drivetrain_output_fbs",
         ":drivetrain_test_lib",
         "//aos/controls:control_loop_test",
+        "//aos/events/logging:logger",
         "//aos/testing:googletest",
         "//frc971/queues:gyro_fbs",
         "//frc971/wpilib:imu_fbs",
diff --git a/frc971/control_loops/drivetrain/drivetrain.cc b/frc971/control_loops/drivetrain/drivetrain.cc
index 094a947..1afc32d 100644
--- a/frc971/control_loops/drivetrain/drivetrain.cc
+++ b/frc971/control_loops/drivetrain/drivetrain.cc
@@ -147,6 +147,9 @@
   while (imu_values_fetcher_.FetchNext()) {
     imu_zeroer_.ProcessMeasurement(*imu_values_fetcher_);
     last_gyro_time_ = monotonic_now;
+    if (!imu_zeroer_.Zeroed()) {
+      continue;
+    }
     aos::monotonic_clock::time_point reading_time(std::chrono::nanoseconds(
         imu_values_fetcher_->monotonic_timestamp_ns()));
     if (last_imu_update_ == aos::monotonic_clock::min_time) {
diff --git a/frc971/control_loops/drivetrain/drivetrain_lib_test.cc b/frc971/control_loops/drivetrain/drivetrain_lib_test.cc
index e29fed9..f606495 100644
--- a/frc971/control_loops/drivetrain/drivetrain_lib_test.cc
+++ b/frc971/control_loops/drivetrain/drivetrain_lib_test.cc
@@ -6,6 +6,7 @@
 #include "aos/controls/control_loop_test.h"
 #include "aos/controls/polytope.h"
 #include "aos/events/event_loop.h"
+#include "aos/events/logging/logger.h"
 #include "aos/time/time.h"
 #include "gflags/gflags.h"
 #include "gtest/gtest.h"
@@ -21,6 +22,9 @@
 #include "frc971/control_loops/drivetrain/localizer_generated.h"
 #include "frc971/queues/gyro_generated.h"
 
+DEFINE_string(output_file, "",
+              "If set, logs all channels to the provided logfile.");
+
 namespace frc971 {
 namespace control_loops {
 namespace drivetrain {
@@ -34,7 +38,7 @@
   DrivetrainTest()
       : ::aos::testing::ControlLoopTest(
             aos::configuration::ReadConfig(
-                "frc971/control_loops/drivetrain/config.json"),
+                "frc971/control_loops/drivetrain/simulation_config.json"),
             GetTestDrivetrainConfig().dt),
         test_event_loop_(MakeEventLoop("test")),
         drivetrain_goal_sender_(
@@ -56,6 +60,18 @@
     // Too many tests care...
     set_send_delay(chrono::nanoseconds(0));
     set_battery_voltage(12.0);
+
+    if (!FLAGS_output_file.empty()) {
+      unlink(FLAGS_output_file.c_str());
+      log_buffer_writer_ = std::make_unique<aos::logger::DetachedBufferWriter>(
+          FLAGS_output_file);
+      logger_event_loop_ = MakeEventLoop("logger");
+      logger_ = std::make_unique<aos::logger::Logger>(log_buffer_writer_.get(),
+                                                      logger_event_loop_.get());
+    }
+
+    // Run for enough time to allow the gyro/imu zeroing code to run.
+    RunFor(std::chrono::seconds(10));
   }
   virtual ~DrivetrainTest() {}
 
@@ -148,12 +164,14 @@
 
   ::std::unique_ptr<::aos::EventLoop> drivetrain_plant_event_loop_;
   DrivetrainSimulation drivetrain_plant_;
+
+  std::unique_ptr<aos::EventLoop> logger_event_loop_;
+  std::unique_ptr<aos::logger::DetachedBufferWriter> log_buffer_writer_;
+  std::unique_ptr<aos::logger::Logger> logger_;
 };
 
 // Tests that the drivetrain converges on a goal.
 TEST_F(DrivetrainTest, ConvergesCorrectly) {
-  // Run for enough time to let the gyro zero.
-  RunFor(std::chrono::seconds(100));
   SetEnabled(true);
   {
     auto builder = drivetrain_goal_sender_.MakeBuilder();
@@ -312,7 +330,8 @@
     EXPECT_TRUE(builder.Send(goal_builder.Finish()));
   }
 
-  while (monotonic_now() < monotonic_clock::time_point(chrono::seconds(6))) {
+  const auto start_time = monotonic_now();
+  while (monotonic_now() < start_time + chrono::seconds(6)) {
     RunFor(dt());
     ASSERT_TRUE(drivetrain_output_fetcher_.Fetch());
     EXPECT_NEAR(drivetrain_output_fetcher_->left_voltage(),
@@ -353,7 +372,8 @@
     EXPECT_TRUE(builder.Send(goal_builder.Finish()));
   }
 
-  while (monotonic_now() < monotonic_clock::time_point(chrono::seconds(6))) {
+  const auto start_time = monotonic_now();
+  while (monotonic_now() < start_time + chrono::seconds(6)) {
     RunFor(dt());
     ASSERT_TRUE(drivetrain_output_fetcher_.Fetch());
     EXPECT_NEAR(drivetrain_output_fetcher_->left_voltage(),
@@ -394,7 +414,8 @@
     EXPECT_TRUE(builder.Send(goal_builder.Finish()));
   }
 
-  while (monotonic_now() < monotonic_clock::time_point(chrono::seconds(3))) {
+  const auto start_time = monotonic_now();
+  while (monotonic_now() < start_time + chrono::seconds(3)) {
     RunFor(dt());
     ASSERT_TRUE(drivetrain_output_fetcher_.Fetch());
   }
diff --git a/frc971/control_loops/drivetrain/drivetrain_simulation_channels.json b/frc971/control_loops/drivetrain/drivetrain_simulation_channels.json
new file mode 100644
index 0000000..b0fc03b
--- /dev/null
+++ b/frc971/control_loops/drivetrain/drivetrain_simulation_channels.json
@@ -0,0 +1,12 @@
+{
+  "channels":
+  [
+    {
+      /* Channel providing the true state of the drivetrain in simulation. */
+      "name": "/drivetrain/truth",
+      "type": "frc971.control_loops.drivetrain.Status",
+      "frequency": 200,
+      "max_size": 2000
+    }
+  ]
+}
diff --git a/frc971/control_loops/drivetrain/drivetrain_simulation_config.json b/frc971/control_loops/drivetrain/drivetrain_simulation_config.json
new file mode 100644
index 0000000..aa67f2e
--- /dev/null
+++ b/frc971/control_loops/drivetrain/drivetrain_simulation_config.json
@@ -0,0 +1,6 @@
+{
+  "imports": [
+    "drivetrain_config.json",
+    "drivetrain_simulation_channels.json"
+  ]
+}
diff --git a/frc971/control_loops/drivetrain/drivetrain_test_lib.cc b/frc971/control_loops/drivetrain/drivetrain_test_lib.cc
index 56e664b..8b4aab1 100644
--- a/frc971/control_loops/drivetrain/drivetrain_test_lib.cc
+++ b/frc971/control_loops/drivetrain/drivetrain_test_lib.cc
@@ -95,14 +95,16 @@
           event_loop_
               ->MakeSender<::frc971::control_loops::drivetrain::Position>(
                   "/drivetrain")),
+      drivetrain_truth_sender_(
+          event_loop_->MakeSender<::frc971::control_loops::drivetrain::Status>(
+              "/drivetrain/truth")),
       drivetrain_output_fetcher_(
           event_loop_->MakeFetcher<::frc971::control_loops::drivetrain::Output>(
               "/drivetrain")),
       drivetrain_status_fetcher_(
           event_loop_->MakeFetcher<::frc971::control_loops::drivetrain::Status>(
               "/drivetrain")),
-      imu_sender_(
-          event_loop->MakeSender<::frc971::IMUValues>("/drivetrain")),
+      imu_sender_(event_loop->MakeSender<::frc971::IMUValues>("/drivetrain")),
       dt_config_(dt_config),
       drivetrain_plant_(MakePlantFromConfig(dt_config_)),
       velocity_drivetrain_(
@@ -136,8 +138,12 @@
         }
         first_ = false;
         SendPositionMessage();
+        SendTruthMessage();
       },
       dt_config_.dt);
+
+  event_loop_->AddPhasedLoop([this](int) { SendImuMessage(); },
+                             std::chrono::microseconds(500));
 }
 
 void DrivetrainSimulation::Reinitialize() {
@@ -150,6 +156,16 @@
   last_right_position_ = drivetrain_plant_.Y(1, 0);
 }
 
+void DrivetrainSimulation::SendTruthMessage() {
+  auto builder = drivetrain_truth_sender_.MakeBuilder();
+  auto status_builder =
+      builder.MakeBuilder<frc971::control_loops::drivetrain::Status>();
+  status_builder.add_x(state_.x());
+  status_builder.add_y(state_.y());
+  status_builder.add_theta(state_(2));
+  builder.Send(status_builder.Finish());
+}
+
 void DrivetrainSimulation::SendPositionMessage() {
   const double left_encoder = GetLeftPosition();
   const double right_encoder = GetRightPosition();
@@ -165,27 +181,27 @@
     position_builder.add_right_shifter_position(right_gear_high_ ? 1.0 : 0.0);
     builder.Send(position_builder.Finish());
   }
+}
 
-  {
-    auto builder = imu_sender_.MakeBuilder();
-    frc971::IMUValues::Builder imu_builder =
-        builder.MakeBuilder<frc971::IMUValues>();
-    imu_builder.add_gyro_x(0.0);
-    imu_builder.add_gyro_y(0.0);
-    imu_builder.add_gyro_z(
-        (drivetrain_plant_.X(3, 0) - drivetrain_plant_.X(1, 0)) /
-        (dt_config_.robot_radius * 2.0));
-    // Acceleration due to gravity, in m/s/s.
-    constexpr double kG = 9.807;
-    imu_builder.add_accelerometer_x(last_acceleration_.x() / kG);
-    imu_builder.add_accelerometer_y(0.0);
-    imu_builder.add_accelerometer_z(1.0);
-    imu_builder.add_monotonic_timestamp_ns(
-        std::chrono::duration_cast<std::chrono::nanoseconds>(
-            event_loop_->monotonic_now().time_since_epoch())
-            .count());
-    builder.Send(imu_builder.Finish());
-  }
+void DrivetrainSimulation::SendImuMessage() {
+  auto builder = imu_sender_.MakeBuilder();
+  frc971::IMUValues::Builder imu_builder =
+      builder.MakeBuilder<frc971::IMUValues>();
+  imu_builder.add_gyro_x(0.0);
+  imu_builder.add_gyro_y(0.0);
+  imu_builder.add_gyro_z(
+      (drivetrain_plant_.X(3, 0) - drivetrain_plant_.X(1, 0)) /
+      (dt_config_.robot_radius * 2.0));
+  // Acceleration due to gravity, in m/s/s.
+  constexpr double kG = 9.807;
+  imu_builder.add_accelerometer_x(last_acceleration_.x() / kG);
+  imu_builder.add_accelerometer_y(last_acceleration_.y() / kG);
+  imu_builder.add_accelerometer_z(1.0);
+  imu_builder.add_monotonic_timestamp_ns(
+      std::chrono::duration_cast<std::chrono::nanoseconds>(
+          event_loop_->monotonic_now().time_since_epoch())
+          .count());
+  builder.Send(imu_builder.Finish());
 }
 
 // Simulates the drivetrain moving for one timestep.
@@ -230,12 +246,23 @@
         return ContinuousDynamics(velocity_drivetrain_->plant(),
                                   dt_config_.Tlr_to_la(), X, U);
       };
+  const Eigen::Matrix<double, 5, 1> last_state = state_;
   state_ = RungeKuttaU(dynamics, state_, U, dt_float);
-  const Eigen::Matrix<double, 5, 1> Xdot = dynamics(state_, U);
-  // TODO(james): Account for centripetal accelerations.
+  // Calculate Xdot from the actual state change rather than getting Xdot at the
+  // current state_.
+  // TODO(james): This seemed to help make the simulation perform better, but
+  // I'm not sure that it is actually helping. Regardless, we should be
+  // calculating Xdot at all the intermediate states at the 2 kHz that
+  // the IMU sends at, rather than doing a sample-and-hold like we do now.
+  const Eigen::Matrix<double, 5, 1> Xdot = (state_ - last_state) / dt_float;
+
+  const double yaw_rate = Xdot(2);
+  const double longitudinal_velocity =
+      (state_(4) + state_(3)) / 2.0;
+  const double centripetal_accel = yaw_rate * longitudinal_velocity;
   // TODO(james): Allow inputting arbitrary calibrations, e.g., for testing
   // situations where the IMU is not perfectly flat in the CG of the robot.
-  last_acceleration_ << (Xdot(3, 0) + Xdot(4, 0)) / 2.0, 0.0, 0.0;
+  last_acceleration_ << (Xdot(3, 0) + Xdot(4, 0)) / 2.0, centripetal_accel, 0.0;
 }
 
 void DrivetrainSimulation::MaybePlot() {
diff --git a/frc971/control_loops/drivetrain/drivetrain_test_lib.h b/frc971/control_loops/drivetrain/drivetrain_test_lib.h
index a906ae2..e8b513c 100644
--- a/frc971/control_loops/drivetrain/drivetrain_test_lib.h
+++ b/frc971/control_loops/drivetrain/drivetrain_test_lib.h
@@ -76,6 +76,10 @@
  private:
   // Sends out the position queue messages.
   void SendPositionMessage();
+  // Sends out the IMU messages.
+  void SendImuMessage();
+  // Sends out the "truth" status message.
+  void SendTruthMessage();
 
   // Simulates the drivetrain moving for one timestep.
   void Simulate();
@@ -85,6 +89,8 @@
 
   ::aos::Sender<::frc971::control_loops::drivetrain::Position>
       drivetrain_position_sender_;
+  ::aos::Sender<::frc971::control_loops::drivetrain::Status>
+      drivetrain_truth_sender_;
   ::aos::Fetcher<::frc971::control_loops::drivetrain::Output>
       drivetrain_output_fetcher_;
   ::aos::Fetcher<::frc971::control_loops::drivetrain::Status>
@@ -107,7 +113,7 @@
   Eigen::Matrix<double, 2, 1> last_U_;
 
   // Last robot acceleration, in m/s/s.
-  Eigen::Vector3d last_acceleration_;
+  Eigen::Vector3d last_acceleration_{0, 0, 1};
 
   bool left_gear_high_ = false;
   bool right_gear_high_ = false;
diff --git a/frc971/control_loops/pose.h b/frc971/control_loops/pose.h
index 13bd6e6..20672cf 100644
--- a/frc971/control_loops/pose.h
+++ b/frc971/control_loops/pose.h
@@ -9,6 +9,20 @@
 namespace frc971 {
 namespace control_loops {
 
+// Constructs a homogeneous transformation matrix for rotating about the Z axis.
+template <typename Scalar>
+Eigen::Matrix<Scalar, 4, 4> TransformationMatrixForYaw(Scalar yaw) {
+  Eigen::Matrix<Scalar, 4, 4> matrix;
+  matrix.setIdentity();
+  const Scalar stheta = std::sin(yaw);
+  const Scalar ctheta = std::cos(yaw);
+  matrix(0, 0) = ctheta;
+  matrix(1, 1) = ctheta;
+  matrix(0, 1) = -stheta;
+  matrix(1, 0) = stheta;
+  return matrix;
+}
+
 // Provides a representation of a transformation on the field.
 // Currently, this is heavily geared towards things that occur in a 2-D plane.
 // The Z-axis is rarely used (but still relevant; e.g., in 2019 some of the
@@ -41,7 +55,7 @@
   // The type that contains the translational (x, y, z) component of the Pose.
   typedef Eigen::Matrix<Scalar, 3, 1> Pos;
 
-  // Provide a default constructor that crease a pose at the origin.
+  // Provide a default constructor that creates a pose at the origin.
   TypedPose() : TypedPose({0.0, 0.0, 0.0}, 0.0) {}
 
   // Construct a Pose in the absolute frame with a particular position and yaw.
@@ -54,6 +68,15 @@
   TypedPose(const TypedPose<Scalar> *base, const Pos &rel_pos, Scalar rel_theta)
       : base_(base), pos_(rel_pos), theta_(rel_theta) {}
 
+  // Constructs a Pose from a homogeneous transformation matrix. Ignores the
+  // pitch/roll components of the rotation. Ignores the bottom row.
+  TypedPose(const Eigen::Matrix<Scalar, 4, 4> &H) {
+    pos_ = H.template block<3, 1>(0, 3);
+    const Eigen::Vector3d rotated_x =
+        H.template block<3, 3>(0, 0) * Eigen::Vector3d::UnitX();
+    theta_ = std::atan2(rotated_x.y(), rotated_x.x());
+  }
+
   // Calculate the current global position of this Pose.
   Pos abs_pos() const {
     if (base_ == nullptr) {
@@ -93,6 +116,15 @@
     return abs_pos().template topRows<2>();
   }
 
+  // Returns a transformation matrix representing this pose--note that this
+  // explicitly does not include the base position, so this is equivalent to a
+  // translation and rotation by rel_pos and rel_theta.
+  Eigen::Matrix<Scalar, 4, 4> AsTransformationMatrix() const {
+    Eigen::Matrix<Scalar, 4, 4> matrix = TransformationMatrixForYaw(theta_);
+    matrix.template block<3, 1>(0, 3) = pos_;
+    return matrix;
+  }
+
   // Provide a copy of this that is set to have the same
   // current absolute Pose as this, but have a different base.
   // This can be used, e.g., to compute a Pose for a vision target that is
@@ -104,6 +136,14 @@
   // the returned object (unless new_base == nullptr).
   TypedPose Rebase(const TypedPose<Scalar> *new_base) const;
 
+  // Convert this pose to the heading/distance/skew numbers that we
+  // traditionally use for EKF corrections.
+  Eigen::Matrix<Scalar, 3, 1> ToHeadingDistanceSkew() const {
+    const Scalar target_heading = heading();
+    return {target_heading, xy_norm(),
+            aos::math::NormalizeAngle(rel_theta() - target_heading)};
+  }
+
  private:
   // A rotation-matrix like representation of the rotation for a given angle.
   inline static Eigen::AngleAxis<Scalar> YawRotation(double theta) {
diff --git a/frc971/control_loops/pose_test.cc b/frc971/control_loops/pose_test.cc
index 4fd3f15..18486e3 100644
--- a/frc971/control_loops/pose_test.cc
+++ b/frc971/control_loops/pose_test.cc
@@ -83,6 +83,53 @@
   EXPECT_NEAR(rel1.abs_theta(), abs.rel_theta(), kEps);
 }
 
+// Tests that we can go between transformation matrices and Pose objects.
+TEST(PoseTest, TransformationMatrixTest) {
+  // First, sanity check the basic case.
+  Pose pose({0, 0, 0}, 0);
+  typedef Eigen::Matrix<double, 4, 4> TransformationMatrix;
+  ASSERT_EQ(TransformationMatrix::Identity(), pose.AsTransformationMatrix());
+  Pose reproduced_pose(pose.AsTransformationMatrix());
+  ASSERT_EQ(reproduced_pose.rel_pos(), pose.rel_pos());
+  ASSERT_EQ(reproduced_pose.rel_theta(), pose.rel_theta());
+  // Check a basic case of rotation + translation.
+  *pose.mutable_pos() << 1, 2, 3;
+  pose.set_theta(M_PI_2);
+  TransformationMatrix expected;
+  expected << 0, -1, 0, 1, 1, 0, 0, 2, 0, 0, 1, 3, 0, 0, 0, 1;
+  TransformationMatrix pose_transformation =
+      pose.AsTransformationMatrix();
+  ASSERT_LT((expected - pose_transformation).norm(), 1e-15)
+      << "expected:\n"
+      << expected << "\nBut got:\n"
+      << pose_transformation;
+  ASSERT_EQ(Eigen::Vector4d(1, 2, 3, 1),
+            pose_transformation * Eigen::Vector4d(0, 0, 0, 1));
+  ASSERT_LT((Eigen::Vector4d(0, 3, 3, 1) -
+             pose_transformation * Eigen::Vector4d(1, 1, 0, 1))
+                .norm(),
+            1e-15)
+      << "got " << pose_transformation * Eigen::Vector4d(1, 1, 0, 1);
+
+  // Also, confirm that setting a new base does not affect the pose.
+  Pose faux_base({1, 1, 1}, 1);
+  pose.set_base(&faux_base);
+
+  ASSERT_EQ(pose_transformation, pose.AsTransformationMatrix());
+
+  reproduced_pose = Pose(pose_transformation);
+  ASSERT_EQ(reproduced_pose.rel_pos(), pose.rel_pos());
+  ASSERT_EQ(reproduced_pose.rel_theta(), pose.rel_theta());
+  // And check that if we introduce a pitch to the transformation matrix that it
+  // does not impact the resulting Pose (which only has a yaw component).
+  pose_transformation.block<3, 3>(0, 0) =
+      Eigen::AngleAxis<double>(0.5, Eigen::Vector3d::UnitX()) *
+      pose_transformation.block<3, 3>(0, 0);
+  reproduced_pose = Pose(pose_transformation);
+  ASSERT_EQ(reproduced_pose.rel_pos(), pose.rel_pos());
+  ASSERT_EQ(reproduced_pose.rel_theta(), pose.rel_theta());
+}
+
 // Tests that basic accessors for LineSegment behave as expected.
 TEST(LineSegmentTest, BasicAccessorTest) {
   LineSegment l;
diff --git a/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem.h b/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem.h
index b220581..e115b0e 100644
--- a/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem.h
+++ b/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem.h
@@ -137,6 +137,8 @@
     Iterate(const StaticZeroingSingleDOFProfiledSubsystemGoal *goal,
             const typename ZeroingEstimator::Position *position, double *output,
             flatbuffers::FlatBufferBuilder *status_fbb) {
+  CHECK_NOTNULL(position);
+  CHECK_NOTNULL(status_fbb);
   bool disabled = output == nullptr;
   profiled_subsystem_.Correct(*position);
 
diff --git a/frc971/wpilib/ahal/TalonFX.cc b/frc971/wpilib/ahal/TalonFX.cc
new file mode 100644
index 0000000..93dc62e
--- /dev/null
+++ b/frc971/wpilib/ahal/TalonFX.cc
@@ -0,0 +1,42 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) FIRST 2008-2017. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "frc971/wpilib/ahal/TalonFX.h"
+
+#include "hal/HAL.h"
+
+using namespace frc;
+
+/**
+ * Constructor for a TalonFX.
+ *
+ * @param channel The PWM channel that the TalonFX is attached to. 0-9 are
+ *                on-board, 10-19 are on the MXP port
+ */
+TalonFX::TalonFX(int channel) : PWM(channel) {
+  /**
+   * Note that the TalonFX uses the following bounds for PWM values. These
+   * values should work reasonably well for most controllers, but if users
+   * experience issues such as asymmetric behavior around the deadband or
+   * inability to saturate the controller in either direction, calibration is
+   * recommended. The calibration procedure can be found in the TalonFX User
+   * Manual available from Vex.
+   *
+   *   2.004ms = full "forward"
+   *   1.52ms = the "high end" of the deadband range
+   *   1.50ms = center of the deadband range (off)
+   *   1.48ms = the "low end" of the deadband range
+   *   0.997ms = full "reverse"
+   */
+  SetBounds(2.004, 1.52, 1.50, 1.48, .997);
+  SetPeriodMultiplier(kPeriodMultiplier_1X);
+  SetSpeed(0.0);
+  SetZeroLatch();
+
+  HAL_Report(HALUsageReporting::kResourceType_VictorSP, GetChannel());
+}
+
diff --git a/frc971/wpilib/ahal/TalonFX.h b/frc971/wpilib/ahal/TalonFX.h
new file mode 100644
index 0000000..0de0019
--- /dev/null
+++ b/frc971/wpilib/ahal/TalonFX.h
@@ -0,0 +1,23 @@
+
+/* Copyright (c) FIRST 2008-2017. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#pragma once
+
+#include "frc971/wpilib/ahal/PWM.h"
+
+namespace frc {
+
+/**
+ * Vex Robotics Victor SP Speed Controller
+ */
+class TalonFX : public PWM {
+ public:
+  explicit TalonFX(int channel);
+  virtual ~TalonFX() = default;
+};
+
+}  // namespace frc
diff --git a/frc971/wpilib/sensor_reader.h b/frc971/wpilib/sensor_reader.h
index 3e24fd7..25e7592 100644
--- a/frc971/wpilib/sensor_reader.h
+++ b/frc971/wpilib/sensor_reader.h
@@ -166,6 +166,18 @@
     position->index_pulses = encoder.index_posedge_count();
   }
 
+  // Copies a relative encoder.
+  void CopyPosition(const ::frc::Encoder &encoder,
+                    ::frc971::RelativePositionT *position,
+                    double encoder_counts_per_revolution, double encoder_ratio,
+                    bool reverse) {
+    const double multiplier = reverse ? -1.0 : 1.0;
+    position->encoder =
+        multiplier * encoder_translate(encoder.GetRaw(),
+                                       encoder_counts_per_revolution,
+                                       encoder_ratio);
+  }
+
   double encoder_translate(int32_t value, double counts_per_revolution,
                            double ratio) {
     return static_cast<double>(value) / counts_per_revolution * ratio *
diff --git a/frc971/zeroing/BUILD b/frc971/zeroing/BUILD
index a2b908b..75d610a 100644
--- a/frc971/zeroing/BUILD
+++ b/frc971/zeroing/BUILD
@@ -35,6 +35,8 @@
     ],
     deps = [
         ":averager",
+        "//frc971/control_loops:control_loops_fbs",
+        "//frc971/control_loops/drivetrain:drivetrain_status_fbs",
         "//frc971/wpilib:imu_fbs",
         "@com_github_google_glog//:glog",
         "@org_tuxfamily_eigen//:eigen",
diff --git a/frc971/zeroing/averager.h b/frc971/zeroing/averager.h
index b6ff09d..23a1b06 100644
--- a/frc971/zeroing/averager.h
+++ b/frc971/zeroing/averager.h
@@ -19,6 +19,11 @@
 class Averager {
  public:
   typedef Eigen::Matrix<Scalar, rows_per_sample, 1> Vector;
+  Averager() {
+    for (Vector &datum : data_) {
+      datum.setZero();
+    }
+  }
   // Adds one data point to the set of data points to be averaged.
   // If more than "num_samples" samples are added, they will start overwriting
   // the oldest ones.
diff --git a/frc971/zeroing/imu_zeroer.cc b/frc971/zeroing/imu_zeroer.cc
index 25bf6f6..b13b43d 100644
--- a/frc971/zeroing/imu_zeroer.cc
+++ b/frc971/zeroing/imu_zeroer.cc
@@ -4,22 +4,30 @@
 
 ImuZeroer::ImuZeroer() {
   gyro_average_.setZero();
+  accel_average_.setZero();
   last_gyro_sample_.setZero();
   last_accel_sample_.setZero();
 }
 
-bool ImuZeroer::Zeroed() const { return zeroed_ || Faulted(); }
+bool ImuZeroer::Zeroed() const {
+  return num_zeroes_ > kRequiredZeroPoints || Faulted();
+}
 
 bool ImuZeroer::Faulted() const { return faulted_; }
 
 Eigen::Vector3d ImuZeroer::ZeroedGyro() const {
   return last_gyro_sample_ - gyro_average_;
 }
-Eigen::Vector3d ImuZeroer::ZeroedAccel() const { return last_accel_sample_; }
+Eigen::Vector3d ImuZeroer::ZeroedAccel() const {
+  return last_accel_sample_ - accel_average_;
+}
 Eigen::Vector3d ImuZeroer::GyroOffset() const { return gyro_average_; }
 
 bool ImuZeroer::GyroZeroReady() const {
-  return gyro_averager_.full() && gyro_averager_.GetRange() < kGyroMaxVariation;
+  return gyro_averager_.full() &&
+         gyro_averager_.GetRange() < kGyroMaxVariation &&
+         (last_gyro_sample_.lpNorm<Eigen::Infinity>() <
+          kGyroMaxZeroingMagnitude);
 }
 
 bool ImuZeroer::AccelZeroReady() const {
@@ -34,19 +42,45 @@
                            values.accelerometer_z();
   accel_averager_.AddData(last_accel_sample_);
   if (GyroZeroReady() && AccelZeroReady()) {
-    if (!zeroed_) {
-      zeroed_ = true;
-      gyro_average_ = gyro_averager_.GetAverage();
-    } else {
-      // If we got a new zero and it is substantially different from the
-      // original zero, fault.
-      if ((gyro_averager_.GetAverage() - gyro_average_).norm() >
-          kGyroFaultVariation) {
-        faulted_ = true;
+    ++good_iters_;
+    if (good_iters_ > kSamplesToAverage / 4) {
+      const Eigen::Vector3d current_gyro_average = gyro_averager_.GetAverage();
+      constexpr double kAverageUpdateWeight = 0.05;
+      if (num_zeroes_ > 0) {
+        gyro_average_ +=
+            (current_gyro_average - gyro_average_) * kAverageUpdateWeight;
+      } else {
+        gyro_average_ = current_gyro_average;
       }
+      if (num_zeroes_ > 0) {
+        // If we got a new zero and it is substantially different from the
+        // original zero, fault.
+        if ((current_gyro_average - gyro_average_).norm() >
+            kGyroFaultVariation) {
+          faulted_ = true;
+        }
+      }
+      ++num_zeroes_;
+      gyro_averager_.Reset();
     }
-    gyro_averager_.Reset();
+  } else {
+    good_iters_ = 0;
   }
 }
 
+flatbuffers::Offset<control_loops::drivetrain::ImuZeroerState>
+ImuZeroer::PopulateStatus(flatbuffers::FlatBufferBuilder *fbb) const {
+  control_loops::drivetrain::ImuZeroerState::Builder builder(*fbb);
+
+  builder.add_zeroed(Zeroed());
+  builder.add_faulted(Faulted());
+  builder.add_number_of_zeroes(num_zeroes_);
+
+  builder.add_gyro_x_average(GyroOffset().x());
+  builder.add_gyro_y_average(GyroOffset().y());
+  builder.add_gyro_z_average(GyroOffset().z());
+
+  return builder.Finish();
+}
+
 }  // namespace frc971::zeroing
diff --git a/frc971/zeroing/imu_zeroer.h b/frc971/zeroing/imu_zeroer.h
index 2662934..b55c6dd 100644
--- a/frc971/zeroing/imu_zeroer.h
+++ b/frc971/zeroing/imu_zeroer.h
@@ -1,6 +1,7 @@
 #ifndef FRC971_ZEROING_IMU_ZEROER_H_
 #define FRC971_ZEROING_IMU_ZEROER_H_
 
+#include "frc971/control_loops/drivetrain/drivetrain_status_generated.h"
 #include "frc971/wpilib/imu_generated.h"
 #include "frc971/zeroing/averager.h"
 
@@ -15,7 +16,8 @@
   // constant number of samples...
   // TODO(james): Run average and GetRange calculations over every sample on
   // every timestep, to provide consistent timing.
-  static constexpr size_t kSamplesToAverage = 1000.0;
+  static constexpr size_t kSamplesToAverage = 1000;
+  static constexpr size_t kRequiredZeroPoints = 10;
 
   ImuZeroer();
   bool Zeroed() const;
@@ -24,14 +26,22 @@
   Eigen::Vector3d GyroOffset() const;
   Eigen::Vector3d ZeroedGyro() const;
   Eigen::Vector3d ZeroedAccel() const;
+
+  flatbuffers::Offset<control_loops::drivetrain::ImuZeroerState> PopulateStatus(
+      flatbuffers::FlatBufferBuilder *fbb) const;
+
  private:
   // Max variation (difference between the maximum and minimum value) in a
   // kSamplesToAverage range before we allow using the samples for zeroing.
   // These values are currently based on looking at results from the ADIS16448.
-  static constexpr double kGyroMaxVariation = 0.02;     // rad / sec
+  static constexpr double kGyroMaxVariation = 0.02;        // rad / sec
+  // Maximum magnitude we allow the gyro zero to have--this is used to prevent
+  // us from zeroing the gyro if we just happen to be spinning at a very
+  // consistent non-zero rate. Currently this is only plausible in simulation.
+  static constexpr double kGyroMaxZeroingMagnitude = 0.1;  // rad / sec
   // Max variation in the range before we consider the accelerometer readings to
   // be steady.
-  static constexpr double kAccelMaxVariation = 0.02;    // g's
+  static constexpr double kAccelMaxVariation = 0.05;    // g's
   // If we ever are able to rezero and get a zero that is more than
   // kGyroFaultVariation away from the original zeroing, fault.
   static constexpr double kGyroFaultVariation = 0.005;  // rad / sec
@@ -46,12 +56,13 @@
   Averager<double, kSamplesToAverage, 3> accel_averager_;
   // The average zero position of the gyro.
   Eigen::Vector3d gyro_average_;
+  Eigen::Vector3d accel_average_;
   Eigen::Vector3d last_gyro_sample_;
   Eigen::Vector3d last_accel_sample_;
-  // Whether we have currently zeroed yet.
-  bool zeroed_ = false;
   // Whether the zeroing has faulted at any point thus far.
   bool faulted_ = false;
+  size_t good_iters_ = 0;
+  size_t num_zeroes_ = 0;
 };
 
 }  // namespace frc971::zeroing
diff --git a/frc971/zeroing/imu_zeroer_test.cc b/frc971/zeroing/imu_zeroer_test.cc
index 8eef149..eee3f57 100644
--- a/frc971/zeroing/imu_zeroer_test.cc
+++ b/frc971/zeroing/imu_zeroer_test.cc
@@ -4,6 +4,9 @@
 
 namespace frc971::zeroing {
 
+static constexpr int kMinSamplesToZero =
+    2 * ImuZeroer::kSamplesToAverage * ImuZeroer::kRequiredZeroPoints;
+
 aos::FlatbufferDetachedBuffer<IMUValues> MakeMeasurement(
     const Eigen::Vector3d &gyro, const Eigen::Vector3d &accel) {
   flatbuffers::FlatBufferBuilder fbb;
@@ -29,13 +32,14 @@
   ASSERT_EQ(0.0, zeroer.ZeroedAccel().norm());
   // A measurement before we are zeroed should just result in the measurement
   // being passed through without modification.
-  zeroer.ProcessMeasurement(MakeMeasurement({1, 2, 3}, {4, 5, 6}).message());
+  zeroer.ProcessMeasurement(
+      MakeMeasurement({0.01, 0.02, 0.03}, {4, 5, 6}).message());
   ASSERT_FALSE(zeroer.Zeroed());
   ASSERT_FALSE(zeroer.Faulted());
   ASSERT_EQ(0.0, zeroer.GyroOffset().norm());
-  ASSERT_EQ(1.0, zeroer.ZeroedGyro().x());
-  ASSERT_EQ(2.0, zeroer.ZeroedGyro().y());
-  ASSERT_EQ(3.0, zeroer.ZeroedGyro().z());
+  ASSERT_FLOAT_EQ(0.01, zeroer.ZeroedGyro().x());
+  ASSERT_FLOAT_EQ(0.02, zeroer.ZeroedGyro().y());
+  ASSERT_FLOAT_EQ(0.03, zeroer.ZeroedGyro().z());
   ASSERT_EQ(4.0, zeroer.ZeroedAccel().x());
   ASSERT_EQ(5.0, zeroer.ZeroedAccel().y());
   ASSERT_EQ(6.0, zeroer.ZeroedAccel().z());
@@ -45,16 +49,16 @@
 TEST(ImuZeroerTest, ZeroOnConstantData) {
   ImuZeroer zeroer;
   ASSERT_FALSE(zeroer.Zeroed());
-  for (size_t ii = 0; ii < ImuZeroer::kSamplesToAverage; ++ii) {
-    ASSERT_FALSE(zeroer.Zeroed());
-    zeroer.ProcessMeasurement(MakeMeasurement({1, 2, 3}, {4, 5, 6}).message());
+  for (size_t ii = 0; ii < kMinSamplesToZero; ++ii) {
+    zeroer.ProcessMeasurement(
+        MakeMeasurement({0.01, 0.02, 0.03}, {4, 5, 6}).message());
   }
   ASSERT_TRUE(zeroer.Zeroed());
   ASSERT_FALSE(zeroer.Faulted());
   // Gyro should be zeroed to {1, 2, 3}.
-  ASSERT_EQ(1.0, zeroer.GyroOffset().x());
-  ASSERT_EQ(2.0, zeroer.GyroOffset().y());
-  ASSERT_EQ(3.0, zeroer.GyroOffset().z());
+  ASSERT_FLOAT_EQ(0.01, zeroer.GyroOffset().x());
+  ASSERT_FLOAT_EQ(0.02, zeroer.GyroOffset().y());
+  ASSERT_FLOAT_EQ(0.03, zeroer.GyroOffset().z());
   ASSERT_EQ(0.0, zeroer.ZeroedGyro().x());
   ASSERT_EQ(0.0, zeroer.ZeroedGyro().y());
   ASSERT_EQ(0.0, zeroer.ZeroedGyro().z());
@@ -64,11 +68,25 @@
   ASSERT_EQ(6.0, zeroer.ZeroedAccel().z());
   // If we get another measurement offset by {1, 1, 1} we should read the result
   // as {1, 1, 1}.
-  zeroer.ProcessMeasurement(MakeMeasurement({2, 3, 4}, {0, 0, 0}).message());
+  zeroer.ProcessMeasurement(
+      MakeMeasurement({0.02, 0.03, 0.04}, {0, 0, 0}).message());
   ASSERT_FALSE(zeroer.Faulted());
-  ASSERT_EQ(1.0, zeroer.ZeroedGyro().x());
-  ASSERT_EQ(1.0, zeroer.ZeroedGyro().y());
-  ASSERT_EQ(1.0, zeroer.ZeroedGyro().z());
+  ASSERT_FLOAT_EQ(0.01, zeroer.ZeroedGyro().x());
+  ASSERT_FLOAT_EQ(0.01, zeroer.ZeroedGyro().y());
+  ASSERT_FLOAT_EQ(0.01, zeroer.ZeroedGyro().z());
+}
+
+// Tests that we do not zero if the gyro is producing particularly high
+// magnitude results.
+TEST(ImuZeroerTest, NoZeroOnHighMagnitudeGyro) {
+  ImuZeroer zeroer;
+  ASSERT_FALSE(zeroer.Zeroed());
+  for (size_t ii = 0; ii < kMinSamplesToZero; ++ii) {
+    zeroer.ProcessMeasurement(
+        MakeMeasurement({0.1, 0.2, 0.3}, {4, 5, 6}).message());
+    ASSERT_FALSE(zeroer.Zeroed());
+  }
+  ASSERT_FALSE(zeroer.Faulted());
 }
 
 // Tests that we tolerate small amounts of noise in the incoming data and can
@@ -76,29 +94,27 @@
 TEST(ImuZeroerTest, ZeroOnLowNoiseData) {
   ImuZeroer zeroer;
   ASSERT_FALSE(zeroer.Zeroed());
-  for (size_t ii = 0; ii < ImuZeroer::kSamplesToAverage; ++ii) {
-    ASSERT_FALSE(zeroer.Zeroed());
+  for (size_t ii = 0; ii < kMinSamplesToZero; ++ii) {
     const double offset =
-        (static_cast<double>(ii) / (ImuZeroer::kSamplesToAverage - 1) - 0.5) *
-        0.01;
+        (static_cast<double>(ii) / (kMinSamplesToZero - 1) - 0.5) * 0.001;
     zeroer.ProcessMeasurement(
-        MakeMeasurement({1 + offset, 2 + offset, 3 + offset},
+        MakeMeasurement({0.01 + offset, 0.02 + offset, 0.03 + offset},
                         {4 + offset, 5 + offset, 6 + offset})
             .message());
   }
   ASSERT_TRUE(zeroer.Zeroed());
   ASSERT_FALSE(zeroer.Faulted());
-  // Gyro should be zeroed to {1, 2, 3}.
-  ASSERT_NEAR(1.0, zeroer.GyroOffset().x(), 1e-8);
-  ASSERT_NEAR(2.0, zeroer.GyroOffset().y(), 1e-8);
-  ASSERT_NEAR(3.0, zeroer.GyroOffset().z(), 1e-8);
-  // If we get another measurement offset by {1, 1, 1} we should read the result
-  // as {1, 1, 1}.
-  zeroer.ProcessMeasurement(MakeMeasurement({2, 3, 4}, {0, 0, 0}).message());
+  ASSERT_NEAR(0.01, zeroer.GyroOffset().x(), 1e-3);
+  ASSERT_NEAR(0.02, zeroer.GyroOffset().y(), 1e-3);
+  ASSERT_NEAR(0.03, zeroer.GyroOffset().z(), 1e-3);
+  // If we get another measurement offset by {0.01, 0.01, 0.01} we should read
+  // the result as {0.01, 0.01, 0.01}.
+  zeroer.ProcessMeasurement(
+      MakeMeasurement({0.02, 0.03, 0.04}, {0, 0, 0}).message());
   ASSERT_FALSE(zeroer.Faulted());
-  ASSERT_NEAR(1.0, zeroer.ZeroedGyro().x(), 1e-8);
-  ASSERT_NEAR(1.0, zeroer.ZeroedGyro().y(), 1e-8);
-  ASSERT_NEAR(1.0, zeroer.ZeroedGyro().z(), 1e-8);
+  ASSERT_NEAR(0.01, zeroer.ZeroedGyro().x(), 1e-3);
+  ASSERT_NEAR(0.01, zeroer.ZeroedGyro().y(), 1e-3);
+  ASSERT_NEAR(0.01, zeroer.ZeroedGyro().z(), 1e-3);
   ASSERT_EQ(0.0, zeroer.ZeroedAccel().x());
   ASSERT_EQ(0.0, zeroer.ZeroedAccel().y());
   ASSERT_EQ(0.0, zeroer.ZeroedAccel().z());
@@ -108,13 +124,12 @@
 TEST(ImuZeroerTest, NoZeroOnHighNoiseData) {
   ImuZeroer zeroer;
   ASSERT_FALSE(zeroer.Zeroed());
-  for (size_t ii = 0; ii < ImuZeroer::kSamplesToAverage; ++ii) {
+  for (size_t ii = 0; ii < kMinSamplesToZero; ++ii) {
     ASSERT_FALSE(zeroer.Zeroed());
     const double offset =
-        (static_cast<double>(ii) / (ImuZeroer::kSamplesToAverage - 1) - 0.5) *
-        1.0;
+        (static_cast<double>(ii) / (kMinSamplesToZero - 1) - 0.5) * 1.0;
     zeroer.ProcessMeasurement(
-        MakeMeasurement({1 + offset, 2 + offset, 3 + offset},
+        MakeMeasurement({0.01 + offset, 0.02 + offset, 0.03 + offset},
                         {4 + offset, 5 + offset, 6 + offset})
             .message());
   }
@@ -127,15 +142,16 @@
 TEST(ImuZeroerTest, FaultOnNewZero) {
   ImuZeroer zeroer;
   ASSERT_FALSE(zeroer.Zeroed());
-  for (size_t ii = 0; ii < ImuZeroer::kSamplesToAverage; ++ii) {
-    ASSERT_FALSE(zeroer.Zeroed());
-    zeroer.ProcessMeasurement(MakeMeasurement({1, 2, 3}, {4, 5, 6}).message());
+  for (size_t ii = 0; ii < kMinSamplesToZero; ++ii) {
+    zeroer.ProcessMeasurement(
+        MakeMeasurement({0.01, 0.02, 0.03}, {4, 5, 6}).message());
   }
   ASSERT_TRUE(zeroer.Zeroed());
-  for (size_t ii = 0; ii < ImuZeroer::kSamplesToAverage; ++ii) {
-    ASSERT_FALSE(zeroer.Faulted())
-        << "We should not fault until we complete a second cycle of zeroing.";
-    zeroer.ProcessMeasurement(MakeMeasurement({1, 5, 3}, {4, 5, 6}).message());
+  ASSERT_FALSE(zeroer.Faulted())
+      << "We should not fault until we complete a second cycle of zeroing.";
+  for (size_t ii = 0; ii < kMinSamplesToZero; ++ii) {
+    zeroer.ProcessMeasurement(
+        MakeMeasurement({0.01, 0.05, 0.03}, {4, 5, 6}).message());
   }
   ASSERT_TRUE(zeroer.Faulted());
 }
@@ -144,14 +160,14 @@
 TEST(ImuZeroerTest, NoFaultOnSimilarZero) {
   ImuZeroer zeroer;
   ASSERT_FALSE(zeroer.Zeroed());
-  for (size_t ii = 0; ii < ImuZeroer::kSamplesToAverage; ++ii) {
-    ASSERT_FALSE(zeroer.Zeroed());
-    zeroer.ProcessMeasurement(MakeMeasurement({1, 2, 3}, {4, 5, 6}).message());
+  for (size_t ii = 0; ii < kMinSamplesToZero; ++ii) {
+    zeroer.ProcessMeasurement(
+        MakeMeasurement({0.01, 0.02, 0.03}, {4, 5, 6}).message());
   }
   ASSERT_TRUE(zeroer.Zeroed());
-  for (size_t ii = 0; ii < ImuZeroer::kSamplesToAverage; ++ii) {
+  for (size_t ii = 0; ii < kMinSamplesToZero; ++ii) {
     zeroer.ProcessMeasurement(
-        MakeMeasurement({1, 2.0001, 3}, {4, 5, 6}).message());
+        MakeMeasurement({0.01, 0.020001, 0.03}, {4, 5, 6}).message());
   }
   ASSERT_FALSE(zeroer.Faulted());
 }
diff --git a/y2019/control_loops/drivetrain/BUILD b/y2019/control_loops/drivetrain/BUILD
index aabd3e5..cd3a793 100644
--- a/y2019/control_loops/drivetrain/BUILD
+++ b/y2019/control_loops/drivetrain/BUILD
@@ -1,4 +1,5 @@
 load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library")
+load("//aos:config.bzl", "aos_config")
 load("//tools/build_rules:select.bzl", "compiler_select", "cpu_select")
 
 genrule(
@@ -159,7 +160,7 @@
             "arm": [],
         }),
     linkstatic = True,
-    shard_count = 5,
+    shard_count = 8,
     deps = [
         ":localizer",
         ":drivetrain_base",
@@ -178,10 +179,20 @@
     }),
 )
 
+aos_config(
+    name = "simulation_config",
+    src = "drivetrain_simulation_config.json",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//frc971/control_loops/drivetrain:simulation_channels",
+        "//y2019:config",
+    ],
+)
+
 cc_test(
     name = "localized_drivetrain_test",
     srcs = ["localized_drivetrain_test.cc"],
-    data = ["//y2019:config.json"],
+    data = [":simulation_config.json"],
     deps = [
         ":camera_fbs",
         ":drivetrain_base",
diff --git a/y2019/control_loops/drivetrain/drivetrain_simulation_config.json b/y2019/control_loops/drivetrain/drivetrain_simulation_config.json
new file mode 100644
index 0000000..57ff027
--- /dev/null
+++ b/y2019/control_loops/drivetrain/drivetrain_simulation_config.json
@@ -0,0 +1,6 @@
+{
+  "imports": [
+    "../../y2019.json",
+    "../../../frc971/control_loops/drivetrain/drivetrain_simulation_channels.json"
+  ]
+}
diff --git a/y2019/control_loops/drivetrain/localized_drivetrain_test.cc b/y2019/control_loops/drivetrain/localized_drivetrain_test.cc
index cc3daac..9b237f4 100644
--- a/y2019/control_loops/drivetrain/localized_drivetrain_test.cc
+++ b/y2019/control_loops/drivetrain/localized_drivetrain_test.cc
@@ -43,7 +43,8 @@
   // with shifting:
   LocalizedDrivetrainTest()
       : ::aos::testing::ControlLoopTest(
-            aos::configuration::ReadConfig("y2019/config.json"),
+            aos::configuration::ReadConfig(
+                "y2019/control_loops/drivetrain/simulation_config.json"),
             GetTest2019DrivetrainConfig().dt),
         test_event_loop_(MakeEventLoop("test")),
         drivetrain_goal_sender_(
@@ -54,8 +55,7 @@
             test_event_loop_->MakeSender<LocalizerControl>("/drivetrain")),
         drivetrain_event_loop_(MakeEventLoop("drivetrain")),
         dt_config_(GetTest2019DrivetrainConfig()),
-        camera_sender_(
-            test_event_loop_->MakeSender<CameraFrame>("/camera")),
+        camera_sender_(test_event_loop_->MakeSender<CameraFrame>("/camera")),
         localizer_(drivetrain_event_loop_.get(), dt_config_),
         drivetrain_(dt_config_, drivetrain_event_loop_.get(), &localizer_),
         drivetrain_plant_event_loop_(MakeEventLoop("plant")),
@@ -82,6 +82,9 @@
             }
           }
         });
+
+    // Run for enough time to allow the gyro/imu zeroing code to run.
+    RunFor(std::chrono::seconds(10));
   }
 
   virtual ~LocalizedDrivetrainTest() {}
@@ -96,19 +99,20 @@
     localizer_.Reset(monotonic_now(), localizer_state, 0.0);
   }
 
-  void VerifyNearGoal() {
+  void VerifyNearGoal(double eps = 1e-3) {
     drivetrain_goal_fetcher_.Fetch();
     EXPECT_NEAR(drivetrain_goal_fetcher_->left_goal(),
-                drivetrain_plant_.GetLeftPosition(), 1e-3);
+                drivetrain_plant_.GetLeftPosition(), eps);
     EXPECT_NEAR(drivetrain_goal_fetcher_->right_goal(),
-                drivetrain_plant_.GetRightPosition(), 1e-3);
+                drivetrain_plant_.GetRightPosition(), eps);
   }
 
   void VerifyEstimatorAccurate(double eps) {
     const Eigen::Matrix<double, 5, 1> true_state = drivetrain_plant_.state();
     EXPECT_NEAR(localizer_.x(), true_state(0, 0), eps);
     EXPECT_NEAR(localizer_.y(), true_state(1, 0), eps);
-    EXPECT_NEAR(localizer_.theta(), true_state(2, 0), eps);
+    // TODO(james): Uncomment this.
+    //EXPECT_NEAR(localizer_.theta(), true_state(2, 0), 2.0 * eps);
     EXPECT_NEAR(localizer_.left_velocity(), true_state(3, 0), eps);
     EXPECT_NEAR(localizer_.right_velocity(), true_state(4, 0), eps);
   }
@@ -204,7 +208,7 @@
 TEST_F(LocalizedDrivetrainTest, NoCameraUpdate) {
   SetEnabled(true);
   set_enable_cameras(false);
-  VerifyEstimatorAccurate(1e-10);
+  VerifyEstimatorAccurate(1e-7);
   {
     auto builder = drivetrain_goal_sender_.MakeBuilder();
 
@@ -218,7 +222,7 @@
   }
   RunFor(chrono::seconds(3));
   VerifyNearGoal();
-  VerifyEstimatorAccurate(1e-7);
+  VerifyEstimatorAccurate(5e-4);
 }
 
 // Bad camera updates (NaNs) should have no effect.
@@ -239,7 +243,7 @@
   }
   RunFor(chrono::seconds(3));
   VerifyNearGoal();
-  VerifyEstimatorAccurate(1e-7);
+  VerifyEstimatorAccurate(5e-4);
 }
 
 // Tests that camera udpates with a perfect models results in no errors.
@@ -259,7 +263,7 @@
   }
   RunFor(chrono::seconds(3));
   VerifyNearGoal();
-  VerifyEstimatorAccurate(1e-7);
+  VerifyEstimatorAccurate(5e-4);
 }
 
 // Tests that not having cameras with an initial disturbance results in
@@ -286,9 +290,10 @@
   // Everything but X-value should be correct:
   EXPECT_NEAR(true_state.x(), localizer_.x() + 0.05, 1e-5);
   EXPECT_NEAR(true_state.y(), localizer_.y(), 1e-5);
-  EXPECT_NEAR(true_state(2, 0), localizer_.theta(), 1e-5);
-  EXPECT_NEAR(true_state(3, 0), localizer_.left_velocity(), 1e-5);
-  EXPECT_NEAR(true_state(4, 0), localizer_.right_velocity(), 1e-5);
+  // TODO(james): Uncomment this.
+  //EXPECT_NEAR(true_state(2, 0), localizer_.theta(), 1e-3);
+  EXPECT_NEAR(true_state(3, 0), localizer_.left_velocity(), 1e-4);
+  EXPECT_NEAR(true_state(4, 0), localizer_.right_velocity(), 1e-4);
 }
 
 // Tests that when we reset the position of the localizer via the queue to
@@ -323,7 +328,7 @@
   }
   RunFor(chrono::seconds(3));
   VerifyNearGoal();
-  VerifyEstimatorAccurate(1e-5);
+  VerifyEstimatorAccurate(1e-3);
 }
 
 // Tests that, when a small error in X is introduced, the camera corrections do
@@ -332,7 +337,7 @@
   SetEnabled(true);
   set_enable_cameras(true);
   SetStartingPosition({4.0, 0.5, 0.0});
-  (*drivetrain_plant_.mutable_state())(0, 0) += 0.05;
+  (*drivetrain_plant_.mutable_state())(0, 0) += 0.1;
   {
     auto builder = drivetrain_goal_sender_.MakeBuilder();
 
@@ -345,8 +350,8 @@
     EXPECT_TRUE(builder.Send(drivetrain_builder.Finish()));
   }
   RunFor(chrono::seconds(5));
-  VerifyNearGoal();
-  VerifyEstimatorAccurate(5e-3);
+  VerifyNearGoal(4e-3);
+  VerifyEstimatorAccurate(5e-2);
 }
 
 namespace {
@@ -357,9 +362,11 @@
 
 // Tests that using the line following drivetrain and just driving straight
 // forward from roughly the right spot gets us to the HP slot.
+// Note: Due to some changes in the localizer in 2020, the allowable error
+// margins have been cranked up severely on this test.
 TEST_F(LocalizedDrivetrainTest, LineFollowToHPSlot) {
   SetEnabled(true);
-  set_enable_cameras(false);
+  set_enable_cameras(true);
   SetStartingPosition({4, HPSlotLeft().abs_pos().y(), M_PI});
   {
     auto builder = drivetrain_goal_sender_.MakeBuilder();
@@ -373,14 +380,15 @@
   }
   RunFor(chrono::seconds(6));
 
-  VerifyEstimatorAccurate(1e-8);
+  VerifyEstimatorAccurate(0.2);
   // Due to the fact that we aren't modulating the throttle, we don't try to hit
   // the target exactly. Instead, just run slightly past the target:
-  EXPECT_LT(
-      ::std::abs(::aos::math::DiffAngle(M_PI, drivetrain_plant_.state()(2, 0))),
-      1e-5);
+  // TODO(james): Uncomment this.
+  //EXPECT_LT(
+  //    ::std::abs(::aos::math::DiffAngle(M_PI, drivetrain_plant_.state()(2, 0))),
+  //    0.5);
   EXPECT_GT(HPSlotLeft().abs_pos().x(), drivetrain_plant_.state().x());
-  EXPECT_NEAR(HPSlotLeft().abs_pos().y(), drivetrain_plant_.state().y(), 1e-4);
+  EXPECT_NEAR(HPSlotLeft().abs_pos().y(), drivetrain_plant_.state().y(), 0.2);
 }
 
 }  // namespace testing
diff --git a/y2020/BUILD b/y2020/BUILD
index c32cd10..f0f87d2 100644
--- a/y2020/BUILD
+++ b/y2020/BUILD
@@ -34,6 +34,9 @@
         "//frc971/control_loops:static_zeroing_single_dof_profiled_subsystem",
         "//y2020/control_loops/drivetrain:polydrivetrain_plants",
         "//y2020/control_loops/superstructure/hood:hood_plants",
+        "//y2020/control_loops/superstructure/intake:intake_plants",
+        "//y2020/control_loops/superstructure/turret:turret_plants",
+	"//y2020/control_loops/superstructure/control_panel:control_panel_plants",
         "@com_google_absl//absl/base",
     ],
 )
@@ -133,8 +136,8 @@
     data = [
         ":config.json",
         "//aos/network:web_proxy_main",
-        "//y2020/www:main_bundle",
         "//y2020/www:files",
         "//y2020/www:flatbuffers",
+        "//y2020/www:main_bundle",
     ],
 )
diff --git a/y2020/constants.cc b/y2020/constants.cc
index a16b2f1..cc5c1fb 100644
--- a/y2020/constants.cc
+++ b/y2020/constants.cc
@@ -12,8 +12,10 @@
 #include "aos/logging/logging.h"
 #include "aos/mutex/mutex.h"
 #include "aos/network/team_number.h"
-
+#include "y2020/control_loops/superstructure/control_panel/integral_control_panel_plant.h"
 #include "y2020/control_loops/superstructure/hood/integral_hood_plant.h"
+#include "y2020/control_loops/superstructure/intake/integral_intake_plant.h"
+#include "y2020/control_loops/superstructure/turret/integral_turret_plant.h"
 
 namespace y2020 {
 namespace constants {
@@ -48,6 +50,48 @@
   hood->zeroing_constants.allowable_encoder_error = 0.9;
   hood->zeroing_constants.middle_position = Values::kHoodRange().middle();
 
+  ::frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemParams<
+      ::frc971::zeroing::AbsoluteEncoderZeroingEstimator> *const intake =
+      &r->intake;
+
+  // Intake constants.
+  intake->zeroing_voltage = 3.0;
+  intake->operating_voltage = 12.0;
+  intake->zeroing_profile_params = {0.5, 3.0};
+  intake->default_profile_params = {6.0, 30.0};
+  intake->range = Values::kIntakeRange();
+  intake->make_integral_loop =
+      control_loops::superstructure::intake::MakeIntegralIntakeLoop;
+  intake->zeroing_constants.average_filter_size = Values::kZeroingSampleSize;
+  intake->zeroing_constants.one_revolution_distance =
+      M_PI * 2.0 * constants::Values::kIntakeEncoderRatio();
+  intake->zeroing_constants.zeroing_threshold = 0.0005;
+  intake->zeroing_constants.moving_buffer_size = 20;
+  intake->zeroing_constants.allowable_encoder_error = 0.9;
+  intake->zeroing_constants.middle_position = Values::kIntakeRange().middle();
+
+  Values::PotAndAbsEncoderConstants *const turret = &r->turret;
+  ::frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemParams<
+      ::frc971::zeroing::PotAndAbsoluteEncoderZeroingEstimator>
+      *const turret_params = &turret->subsystem_params;
+
+  // Turret Constants
+  turret_params->zeroing_voltage = 4.0;
+  turret_params->operating_voltage = 12.0;
+  // TODO(austin): Tune these.
+  turret_params->zeroing_profile_params = {0.5, 2.0};
+  turret_params->default_profile_params = {15.0, 40.0};
+  turret_params->range = Values::kTurretRange();
+  turret_params->make_integral_loop =
+      &control_loops::superstructure::turret::MakeIntegralTurretLoop;
+  turret_params->zeroing_constants.average_filter_size =
+      Values::kZeroingSampleSize;
+  turret_params->zeroing_constants.one_revolution_distance =
+      M_PI * 2.0 * constants::Values::kTurretEncoderRatio();
+  turret_params->zeroing_constants.zeroing_threshold = 0.0005;
+  turret_params->zeroing_constants.moving_buffer_size = 20;
+  turret_params->zeroing_constants.allowable_encoder_error = 0.9;
+
   switch (team) {
     // A set of constants for tests.
     case 1:
@@ -55,14 +99,29 @@
 
     case kCompTeamNumber:
       hood->zeroing_constants.measured_absolute_position = 0.0;
+
+      intake->zeroing_constants.measured_absolute_position = 0.0;
+
+      turret->potentiometer_offset = 0.0;
+      turret_params->zeroing_constants.measured_absolute_position = 0.0;
       break;
 
     case kPracticeTeamNumber:
       hood->zeroing_constants.measured_absolute_position = 0.0;
+
+      intake->zeroing_constants.measured_absolute_position = 0.0;
+
+      turret->potentiometer_offset = 0.0;
+      turret_params->zeroing_constants.measured_absolute_position = 0.0;
       break;
 
     case kCodingRobotTeamNumber:
       hood->zeroing_constants.measured_absolute_position = 0.0;
+
+      intake->zeroing_constants.measured_absolute_position = 0.0;
+
+      turret->potentiometer_offset = 0.0;
+      turret_params->zeroing_constants.measured_absolute_position = 0.0;
       break;
 
     default:
@@ -91,8 +150,8 @@
   static ::aos::Mutex mutex;
   ::aos::MutexLocker locker(&mutex);
 
-  // IMPORTANT: This declaration has to stay after the mutex is locked to avoid
-  // race conditions.
+  // IMPORTANT: This declaration has to stay after the mutex is locked to
+  // avoid race conditions.
   static ::std::map<uint16_t, const Values *> values;
 
   if (values.count(team_number) == 0) {
diff --git a/y2020/constants.h b/y2020/constants.h
index 47b8543..07f75cf 100644
--- a/y2020/constants.h
+++ b/y2020/constants.h
@@ -10,7 +10,10 @@
 #include "frc971/control_loops/pose.h"
 #include "frc971/control_loops/static_zeroing_single_dof_profiled_subsystem.h"
 #include "y2020/control_loops/drivetrain/drivetrain_dog_motor_plant.h"
+#include "y2020/control_loops/superstructure/control_panel/control_panel_plant.h"
 #include "y2020/control_loops/superstructure/hood/hood_plant.h"
+#include "y2020/control_loops/superstructure/intake/intake_plant.h"
+#include "y2020/control_loops/superstructure/turret/turret_plant.h"
 
 namespace y2020 {
 namespace constants {
@@ -33,7 +36,7 @@
   // Hood
   static constexpr double kHoodEncoderCountsPerRevolution() { return 4096.0; }
 
-  //TODO(sabina): Update constants
+  // TODO(sabina): Update constants
   static constexpr double kHoodEncoderRatio() { return 1.0; }
 
   static constexpr double kMaxHoodEncoderPulsesPerSecond() {
@@ -55,14 +58,93 @@
   ::frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemParams<
       ::frc971::zeroing::AbsoluteEncoderZeroingEstimator>
       hood;
+
+  // Intake
+  static constexpr double kIntakeEncoderCountsPerRevolution() { return 4096.0; }
+
+  static constexpr double kIntakeEncoderRatio() { return (16.0 / 32.0); }
+
+  static constexpr double kMaxIntakeEncoderPulsesPerSecond() {
+    return control_loops::superstructure::intake::kFreeSpeed *
+           control_loops::superstructure::intake::kOutputRatio /
+           kIntakeEncoderRatio() / (2.0 * M_PI) *
+           kIntakeEncoderCountsPerRevolution();
+  }
+
+  // TODO(sabina): update range
+  static constexpr ::frc971::constants::Range kIntakeRange() {
+    return ::frc971::constants::Range{
+        -1,     // Back Hard
+        1,      // Front Hard
+        -0.95,  // Back Soft
+        0.95    // Front Soft
+    };
+  }
+
+  ::frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemParams<
+      ::frc971::zeroing::AbsoluteEncoderZeroingEstimator>
+      intake;
+
+  // Turret
+  static constexpr double kTurretEncoderCountsPerRevolution() { return 4096.0; }
+
+  static constexpr double kTurretEncoderRatio() {
+    return 1.0;  // TODO (Kai): Get Gear Ratios when ready
+  }
+
+  static constexpr double kMaxTurretEncoderPulsesPerSecond() {
+    return control_loops::superstructure::turret::kFreeSpeed *
+           control_loops::superstructure::turret::kOutputRatio /
+           kTurretEncoderRatio() / (2.0 * M_PI) *
+           kTurretEncoderCountsPerRevolution();
+  }
+
+  // TODO(austin): Figure out the actual constant here.
+  static constexpr double kTurretPotRatio() { return 1.0; }
+
+  static constexpr ::frc971::constants::Range kTurretRange() {
+    return ::frc971::constants::Range{
+        // TODO (Kai): Placeholders right now.
+        -3.2,   // Back Hard
+        3.2,    // Front Hard
+        -3.14,  // Back Soft
+        3.14    // Front Soft
+    };
+  }
+
+  struct PotAndAbsEncoderConstants {
+    ::frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemParams<
+        ::frc971::zeroing::PotAndAbsoluteEncoderZeroingEstimator>
+        subsystem_params;
+    double potentiometer_offset;
+  };
+
+  PotAndAbsEncoderConstants turret;
+
+  // Control Panel
+
+  // Mag encoder
+  static constexpr double kControlPanelEncoderCountsPerRevolution() {
+    return 4096.0;
+  }
+
+  // Ratio is encoder to output
+  static constexpr double kControlPanelEncoderRatio() { return (56.0 / 28.0); }
+
+  static constexpr double kMaxControlPanelEncoderPulsesPerSecond() {
+    return control_loops::superstructure::control_panel::kFreeSpeed *
+           control_loops::superstructure::control_panel::kOutputRatio /
+           kControlPanelEncoderRatio() / (2.0 * M_PI) *
+           kControlPanelEncoderCountsPerRevolution();
+  }
 };
 
 // Creates (once) a Values instance for ::aos::network::GetTeamNumber() and
 // returns a reference to it.
 const Values &GetValues();
 
-// Creates Values instances for each team number it is called with and returns
-// them.
+// Creates Values instances for each team number it is called with and
+// returns them.
 const Values &GetValuesForTeam(uint16_t team_number);
 
 }  // namespace constants
diff --git a/y2020/control_loops/python/BUILD b/y2020/control_loops/python/BUILD
index 2d1af66..923d239 100644
--- a/y2020/control_loops/python/BUILD
+++ b/y2020/control_loops/python/BUILD
@@ -97,6 +97,64 @@
     ],
 )
 
+py_binary(
+    name = "control_panel",
+    srcs = [
+        "control_panel.py",
+    ],
+    legacy_create_init = False,
+    restricted_to = ["//tools:k8"],
+    deps = [
+        ":python_init",
+        "//external:python-gflags",
+        "//external:python-glog",
+        "//frc971/control_loops/python:angular_system",
+        "//frc971/control_loops/python:controls",
+    ],
+)
+
+py_library(
+    name = "flywheel",
+    srcs = [
+        "flywheel.py",
+    ],
+    restricted_to = ["//tools:k8"],
+    deps = [
+        "//frc971/control_loops/python:controls",
+        "@matplotlib_repo//:matplotlib2.7",
+    ],
+)
+
+py_binary(
+    name = "accelerator",
+    srcs = [
+        "accelerator.py",
+    ],
+    legacy_create_init = False,
+    restricted_to = ["//tools:k8"],
+    deps = [
+        ":python_init",
+        ":flywheel",
+        "//external:python-gflags",
+        "//external:python-glog",
+    ],
+)
+
+py_binary(
+    name = "finisher",
+    srcs = [
+        "finisher.py",
+    ],
+    legacy_create_init = False,
+    restricted_to = ["//tools:k8"],
+    deps = [
+        ":python_init",
+        ":flywheel",
+        "//external:python-gflags",
+        "//external:python-glog",
+    ],
+)
+
 py_library(
     name = "python_init",
     srcs = ["__init__.py"],
diff --git a/y2020/control_loops/python/accelerator.py b/y2020/control_loops/python/accelerator.py
new file mode 100644
index 0000000..257ed23
--- /dev/null
+++ b/y2020/control_loops/python/accelerator.py
@@ -0,0 +1,46 @@
+#!/usr/bin/python
+
+from frc971.control_loops.python import control_loop
+from y2020.control_loops.python import flywheel
+import numpy
+
+import sys
+
+import gflags
+import glog
+
+FLAGS = gflags.FLAGS
+
+gflags.DEFINE_bool('plot', False, 'If true, plot the loop response.')
+
+kAccelerator = flywheel.FlywheelParams(
+    name='Accelerator',
+    motor=control_loop.Falcon(),
+    G=1.0,
+    J=0.006,
+    q_pos=0.08,
+    q_vel=4.00,
+    q_voltage=0.3,
+    r_pos=0.05,
+    controller_poles=[.87],
+    dt=0.00505)
+
+
+def main(argv):
+    if FLAGS.plot:
+        R = numpy.matrix([[0.0], [100.0], [0.0]])
+        flywheel.PlotSpinup(kAccelerator, goal=R, iterations=200)
+        return 0
+
+    if len(argv) != 5:
+        glog.fatal('Expected .h file name and .cc file name')
+    else:
+        namespaces = [
+            'y2020', 'control_loops', 'superstructure', 'accelerator'
+        ]
+        flywheel.WriteFlywheel(kAccelerator, argv[1:3], argv[3:5], namespaces)
+
+
+if __name__ == '__main__':
+    argv = FLAGS(sys.argv)
+    sys.exit(main(argv))
diff --git a/y2020/control_loops/python/control_panel.py b/y2020/control_loops/python/control_panel.py
new file mode 100644
index 0000000..206a39e
--- /dev/null
+++ b/y2020/control_loops/python/control_panel.py
@@ -0,0 +1,54 @@
+#!/usr/bin/python
+
+from aos.util.trapezoid_profile import TrapezoidProfile
+from frc971.control_loops.python import control_loop
+from frc971.control_loops.python import angular_system
+from frc971.control_loops.python import controls
+import numpy
+import sys
+from matplotlib import pylab
+import gflags
+import glog
+
+FLAGS = gflags.FLAGS
+
+try:
+    gflags.DEFINE_bool('plot', False, 'If true, plot the loop response.')
+except gflags.DuplicateFlagError:
+    pass
+
+#TODO(sabina): update moment
+kControlPanel = angular_system.AngularSystemParams(
+    name='ControlPanel',
+    motor=control_loop.BAG(),
+    G=(1.0),
+    J=0.3,
+    q_pos=0.20,
+    q_vel=5.0,
+    kalman_q_pos=0.12,
+    kalman_q_vel=2.0,
+    kalman_q_voltage=4.0,
+    kalman_r_position=0.05)
+
+
+def main(argv):
+    if FLAGS.plot:
+        R = numpy.matrix([[numpy.pi / 2.0], [0.0]])
+        angular_system.PlotKick(kControlPanel, R)
+        angular_system.PlotMotion(kControlPanel, R)
+
+    # Write the generated constants out to a file.
+    if len(argv) != 5:
+        glog.fatal(
+            'Expected .h file name and .cc file name for the control_panel and integral control_panel.'
+        )
+    else:
+        namespaces = ['y2020', 'control_loops', 'superstructure', 'control_panel']
+        angular_system.WriteAngularSystem(kControlPanel, argv[1:3], argv[3:5],
+                                          namespaces)
+
+
+if __name__ == '__main__':
+    argv = FLAGS(sys.argv)
+    glog.init()
+    sys.exit(main(argv))
diff --git a/y2020/control_loops/python/finisher.py b/y2020/control_loops/python/finisher.py
new file mode 100644
index 0000000..0b0fbb4
--- /dev/null
+++ b/y2020/control_loops/python/finisher.py
@@ -0,0 +1,44 @@
+#!/usr/bin/python
+
+from frc971.control_loops.python import control_loop
+from y2020.control_loops.python import flywheel
+import numpy
+
+import sys
+
+import gflags
+import glog
+
+FLAGS = gflags.FLAGS
+
+gflags.DEFINE_bool('plot', False, 'If true, plot the loop response.')
+
+kFinisher = flywheel.FlywheelParams(
+    name='Finisher',
+    motor=control_loop.Falcon(),
+    G=1.0,
+    J=0.006,
+    q_pos=0.08,
+    q_vel=4.00,
+    q_voltage=0.3,
+    r_pos=0.05,
+    controller_poles=[.87],
+    dt=0.00505)
+
+
+def main(argv):
+    if FLAGS.plot:
+        R = numpy.matrix([[0.0], [100.0], [0.0]])
+        flywheel.PlotSpinup(params=kFinisher, goal=R, iterations=200)
+        return 0
+
+    if len(argv) != 5:
+        glog.fatal('Expected .h file name and .cc file name')
+    else:
+        namespaces = ['y2020', 'control_loops', 'superstructure', 'finisher']
+        flywheel.WriteFlywheel(kFinisher, argv[1:3], argv[3:5], namespaces)
+
+
+if __name__ == '__main__':
+    argv = FLAGS(sys.argv)
+    sys.exit(main(argv))
diff --git a/y2020/control_loops/python/flywheel.py b/y2020/control_loops/python/flywheel.py
new file mode 100755
index 0000000..9a6fc46
--- /dev/null
+++ b/y2020/control_loops/python/flywheel.py
@@ -0,0 +1,285 @@
+from frc971.control_loops.python import control_loop
+from frc971.control_loops.python import controls
+import numpy
+from matplotlib import pylab
+
+import glog
+
+
+class FlywheelParams(object):
+    def __init__(self,
+                 name,
+                 motor,
+                 G,
+                 J,
+                 q_pos,
+                 q_vel,
+                 q_voltage,
+                 r_pos,
+                 controller_poles,
+                 dt=0.00505):
+        self.name = name
+        self.motor = motor
+        self.G = G
+        self.J = J
+        self.q_pos = q_pos
+        self.q_vel = q_vel
+        self.q_voltage = q_voltage
+        self.r_pos = r_pos
+        self.dt = dt
+        self.controller_poles = controller_poles
+
+
+class VelocityFlywheel(control_loop.ControlLoop):
+    def __init__(self, params, name="Flywheel"):
+        super(VelocityFlywheel, self).__init__(name=name)
+        self.params = params
+        # Set Motor
+        self.motor = self.params.motor
+        # Moment of inertia of the flywheel wheel in kg m^2
+        self.J = self.params.J
+        # Gear ratio
+        self.G = self.params.G
+        # Control loop time step
+        self.dt = self.params.dt
+
+        # State feedback matrices
+        # [angular velocity]
+        self.A_continuous = numpy.matrix([[
+            -self.motor.Kt / self.motor.Kv /
+            (self.J * self.G * self.G * self.motor.resistance)
+        ]])
+        self.B_continuous = numpy.matrix(
+            [[self.motor.Kt / (self.J * self.G * self.motor.resistance)]])
+        self.C = numpy.matrix([[1]])
+        self.D = numpy.matrix([[0]])
+
+        self.A, self.B = self.ContinuousToDiscrete(self.A_continuous,
+                                                   self.B_continuous, self.dt)
+
+        self.PlaceControllerPoles(self.params.controller_poles)
+
+        # Generated controller not used.
+        self.PlaceObserverPoles([0.3])
+
+        self.U_max = numpy.matrix([[12.0]])
+        self.U_min = numpy.matrix([[-12.0]])
+
+        qff_vel = 8.0
+        self.Qff = numpy.matrix([[1.0 / (qff_vel**2.0)]])
+
+        self.Kff = controls.TwoStateFeedForwards(self.B, self.Qff)
+
+
+class Flywheel(VelocityFlywheel):
+    def __init__(self, params, name="Flywheel"):
+        super(Flywheel, self).__init__(params, name=name)
+
+        self.A_continuous_unaugmented = self.A_continuous
+        self.B_continuous_unaugmented = self.B_continuous
+
+        self.A_continuous = numpy.matrix(numpy.zeros((2, 2)))
+        self.A_continuous[1:2, 1:2] = self.A_continuous_unaugmented
+        self.A_continuous[0, 1] = 1
+
+        self.B_continuous = numpy.matrix(numpy.zeros((2, 1)))
+        self.B_continuous[1:2, 0] = self.B_continuous_unaugmented
+
+        # State feedback matrices
+        # [position, angular velocity]
+        self.C = numpy.matrix([[1, 0]])
+        self.D = numpy.matrix([[0]])
+
+        self.A, self.B = self.ContinuousToDiscrete(self.A_continuous,
+                                                   self.B_continuous, self.dt)
+
+        rpl = 0.45
+        ipl = 0.07
+        self.PlaceObserverPoles([rpl + 1j * ipl, rpl - 1j * ipl])
+
+        self.K_unaugmented = self.K
+        self.K = numpy.matrix(numpy.zeros((1, 2)))
+        self.K[0, 1:2] = self.K_unaugmented
+        self.Kff_unaugmented = self.Kff
+        self.Kff = numpy.matrix(numpy.zeros((1, 2)))
+        self.Kff[0, 1:2] = self.Kff_unaugmented
+
+        self.InitializeState()
+
+
+class IntegralFlywheel(Flywheel):
+    def __init__(self, params, name="IntegralFlywheel"):
+        super(IntegralFlywheel, self).__init__(params, name=name)
+
+        self.A_continuous_unaugmented = self.A_continuous
+        self.B_continuous_unaugmented = self.B_continuous
+
+        self.A_continuous = numpy.matrix(numpy.zeros((3, 3)))
+        self.A_continuous[0:2, 0:2] = self.A_continuous_unaugmented
+        self.A_continuous[0:2, 2] = self.B_continuous_unaugmented
+
+        self.B_continuous = numpy.matrix(numpy.zeros((3, 1)))
+        self.B_continuous[0:2, 0] = self.B_continuous_unaugmented
+
+        # states
+        # [position, velocity, voltage_error]
+        self.C_unaugmented = self.C
+        self.C = numpy.matrix(numpy.zeros((1, 3)))
+        self.C[0:1, 0:2] = self.C_unaugmented
+
+        self.A, self.B = self.ContinuousToDiscrete(self.A_continuous,
+                                                   self.B_continuous, self.dt)
+
+        q_pos = self.params.q_pos
+        q_vel = self.params.q_vel
+        q_voltage = self.params.q_voltage
+        self.Q = numpy.matrix([[(q_pos**2.0), 0.0, 0.0],
+                               [0.0, (q_vel**2.0), 0.0],
+                               [0.0, 0.0, (q_voltage**2.0)]])
+
+        r_pos = self.params.r_pos
+        self.R = numpy.matrix([[(r_pos**2.0)]])
+
+        self.KalmanGain, self.Q_steady = controls.kalman(
+            A=self.A, B=self.B, C=self.C, Q=self.Q, R=self.R)
+        self.L = self.A * self.KalmanGain
+
+        self.K_unaugmented = self.K
+        self.K = numpy.matrix(numpy.zeros((1, 3)))
+        self.K[0, 0:2] = self.K_unaugmented
+        self.K[0, 2] = 1
+        self.Kff_unaugmented = self.Kff
+        self.Kff = numpy.matrix(numpy.zeros((1, 3)))
+        self.Kff[0, 0:2] = self.Kff_unaugmented
+
+        self.InitializeState()
+
+
+def PlotSpinup(params, goal, iterations=200):
+    """Runs the flywheel plant with an initial condition and goal.
+
+    Args:
+        flywheel: Flywheel object to use.
+        goal: goal state.
+        iterations: Number of timesteps to run the model for.
+        controller_flywheel: Flywheel object to get K from, or None if we should
+             use flywheel.
+        observer_flywheel: Flywheel object to use for the observer, or None if we
+            should use the actual state.
+    """
+
+    # Various lists for graphing things.
+    t = []
+    x = []
+    v = []
+    a = []
+    x_hat = []
+    u = []
+    offset = []
+
+    flywheel = Flywheel(params, params.name)
+    controller_flywheel = IntegralFlywheel(params, params.name)
+    observer_flywheel = IntegralFlywheel(params, params.name)
+    vbat = 12.0
+
+    if t:
+        initial_t = t[-1] + flywheel.dt
+    else:
+        initial_t = 0
+
+    for i in xrange(iterations):
+        X_hat = flywheel.X
+
+        if observer_flywheel is not None:
+            X_hat = observer_flywheel.X_hat
+            x_hat.append(observer_flywheel.X_hat[1, 0])
+
+        ff_U = controller_flywheel.Kff * (goal - observer_flywheel.A * goal)
+
+        U = controller_flywheel.K * (goal - X_hat) + ff_U
+        U[0, 0] = numpy.clip(U[0, 0], -vbat, vbat)
+        x.append(flywheel.X[0, 0])
+
+        if v:
+            last_v = v[-1]
+        else:
+            last_v = 0
+
+        v.append(flywheel.X[1, 0])
+        a.append((v[-1] - last_v) / flywheel.dt)
+
+        if observer_flywheel is not None:
+            observer_flywheel.Y = flywheel.Y
+            observer_flywheel.CorrectObserver(U)
+            offset.append(observer_flywheel.X_hat[2, 0])
+
+        applied_U = U.copy()
+        if i > 30:
+            applied_U += 2
+        flywheel.Update(applied_U)
+
+        if observer_flywheel is not None:
+            observer_flywheel.PredictObserver(U)
+
+        t.append(initial_t + i * flywheel.dt)
+        u.append(U[0, 0])
+
+        glog.debug('Time: %f', t[-1])
+    pylab.subplot(3, 1, 1)
+    pylab.plot(t, v, label='x')
+    pylab.plot(t, x_hat, label='x_hat')
+    pylab.legend()
+
+    pylab.subplot(3, 1, 2)
+    pylab.plot(t, u, label='u')
+    pylab.plot(t, offset, label='voltage_offset')
+    pylab.legend()
+
+    pylab.subplot(3, 1, 3)
+    pylab.plot(t, a, label='a')
+    pylab.legend()
+
+    pylab.show()
+
+
+def WriteFlywheel(params, plant_files, controller_files, namespace):
+    """Writes out the constants for a flywheel to a file.
+
+    Args:
+      params: list of Flywheel Params, the
+        parameters defining the system.
+      plant_files: list of strings, the cc and h files for the plant.
+      controller_files: list of strings, the cc and h files for the integral
+        controller.
+      namespaces: list of strings, the namespace list to use.
+    """
+    # Write the generated constants out to a file.
+    flywheels = []
+    integral_flywheels = []
+
+    if type(params) is list:
+        name = params[0].name
+        for index, param in enumerate(params):
+            flywheels.append(Flywheel(param, name=param.name + str(index)))
+            integral_flywheels.append(
+                IntegralFlywheel(
+                    param, name='Integral' + param.name + str(index)))
+    else:
+        name = params.name
+        flywheels.append(Flywheel(params, params.name))
+        integral_flywheels.append(
+            IntegralFlywheel(params, name='Integral' + params.name))
+
+    loop_writer = control_loop.ControlLoopWriter(
+        name, flywheels, namespaces=namespace)
+    loop_writer.AddConstant(
+        control_loop.Constant('kOutputRatio' + params.name, '%f',
+                              flywheels[0].G))
+    loop_writer.AddConstant(
+        control_loop.Constant('kFreeSpeed' + params.name, '%f',
+                              flywheels[0].motor.free_speed))
+    loop_writer.Write(plant_files[0], plant_files[1])
+
+    integral_loop_writer = control_loop.ControlLoopWriter(
+        'Integral' + name, integral_flywheels, namespaces=namespace)
+    integral_loop_writer.Write(controller_files[0], controller_files[1])
diff --git a/y2020/control_loops/superstructure/BUILD b/y2020/control_loops/superstructure/BUILD
index 88d574d..87ee689 100644
--- a/y2020/control_loops/superstructure/BUILD
+++ b/y2020/control_loops/superstructure/BUILD
@@ -55,6 +55,7 @@
         "superstructure.h",
     ],
     deps = [
+        ":climber",
         ":superstructure_goal_fbs",
         ":superstructure_output_fbs",
         ":superstructure_position_fbs",
@@ -100,5 +101,23 @@
         "//frc971/control_loops:team_number_test_environment",
         "//frc971/control_loops/drivetrain:drivetrain_status_fbs",
         "//y2020/control_loops/superstructure/hood:hood_plants",
+        "//y2020/control_loops/superstructure/intake:intake_plants",
+    ],
+)
+
+cc_library(
+    name = "climber",
+    srcs = [
+        "climber.cc",
+    ],
+    hdrs = [
+        "climber.h",
+    ],
+    deps = [
+        ":superstructure_goal_fbs",
+        ":superstructure_output_fbs",
+        "//aos/controls:control_loop",
+        "//frc971/control_loops:control_loops_fbs",
+        "//frc971/control_loops:profiled_subsystem_fbs",
     ],
 )
diff --git a/y2020/control_loops/superstructure/accelerator/BUILD b/y2020/control_loops/superstructure/accelerator/BUILD
new file mode 100644
index 0000000..2c680bf
--- /dev/null
+++ b/y2020/control_loops/superstructure/accelerator/BUILD
@@ -0,0 +1,17 @@
+package(default_visibility = ["//visibility:public"])
+
+load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library")
+
+genrule(
+    name = "genrule_accelerator",
+    outs = [
+        "accelerator_plant.h",
+        "accelerator_plant.cc",
+        "accelerator_integral_plant.h",
+        "accelerator_integral_plant.cc",
+    ],
+    cmd = "$(location //y2020/control_loops/python:accelerator) $(OUTS)",
+    tools = [
+        "//y2020/control_loops/python:accelerator",
+    ],
+)
diff --git a/y2020/control_loops/superstructure/climber.cc b/y2020/control_loops/superstructure/climber.cc
new file mode 100644
index 0000000..bb449da
--- /dev/null
+++ b/y2020/control_loops/superstructure/climber.cc
@@ -0,0 +1,23 @@
+#include "y2020/control_loops/superstructure/climber.h"
+
+#include <algorithm>
+
+#include "y2020/control_loops/superstructure/superstructure_goal_generated.h"
+#include "y2020/control_loops/superstructure/superstructure_output_generated.h"
+
+namespace y2020 {
+namespace control_loops {
+namespace superstructure {
+
+void Climber::Iterate(const Goal *unsafe_goal, OutputT *output) {
+  if (unsafe_goal && output) {
+    // Pass through the voltage request from the user.  Backwards isn't
+    // supported, so prevent that.
+    output->climber_voltage =
+        std::clamp(unsafe_goal->climber_voltage(), 0.0f, 12.0f);
+  }
+}
+
+}  // namespace superstructure
+}  // namespace control_loops
+}  // namespace y2020
diff --git a/y2020/control_loops/superstructure/climber.h b/y2020/control_loops/superstructure/climber.h
new file mode 100644
index 0000000..29b7e51
--- /dev/null
+++ b/y2020/control_loops/superstructure/climber.h
@@ -0,0 +1,21 @@
+#ifndef Y2020_CONTROL_LOOPS_SUPERSTRUCTURE_CLIMBER_H_
+#define Y2020_CONTROL_LOOPS_SUPERSTRUCTURE_CLIMBER_H_
+
+#include "y2020/control_loops/superstructure/superstructure_goal_generated.h"
+#include "y2020/control_loops/superstructure/superstructure_output_generated.h"
+
+namespace y2020 {
+namespace control_loops {
+namespace superstructure {
+
+// Class to encapsulate the climber logic.
+class Climber {
+ public:
+  void Iterate(const Goal *unsafe_goal, OutputT *output);
+};
+
+}  // namespace superstructure
+}  // namespace control_loops
+}  // namespace y2020
+
+#endif  // Y2020_CONTROL_LOOPS_SUPERSTRUCTURE_CLIMBER_H_
diff --git a/y2020/control_loops/superstructure/control_panel/BUILD b/y2020/control_loops/superstructure/control_panel/BUILD
new file mode 100644
index 0000000..75619ca
--- /dev/null
+++ b/y2020/control_loops/superstructure/control_panel/BUILD
@@ -0,0 +1,32 @@
+package(default_visibility = ["//y2020:__subpackages__"])
+
+genrule(
+    name = "genrule_control_panel",
+    outs = [
+        "control_panel_plant.h",
+        "control_panel_plant.cc",
+        "integral_control_panel_plant.h",
+        "integral_control_panel_plant.cc",
+    ],
+    cmd = "$(location //y2020/control_loops/python:control_panel) $(OUTS)",
+    tools = [
+        "//y2020/control_loops/python:control_panel",
+    ],
+)
+
+cc_library(
+    name = "control_panel_plants",
+    srcs = [
+        "control_panel_plant.cc",
+        "integral_control_panel_plant.cc",
+    ],
+    hdrs = [
+        "control_panel_plant.h",
+        "integral_control_panel_plant.h",
+    ],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//frc971/control_loops:hybrid_state_feedback_loop",
+        "//frc971/control_loops:state_feedback_loop",
+    ],
+)
diff --git a/y2020/control_loops/superstructure/finisher/BUILD b/y2020/control_loops/superstructure/finisher/BUILD
new file mode 100644
index 0000000..b9bfc4f
--- /dev/null
+++ b/y2020/control_loops/superstructure/finisher/BUILD
@@ -0,0 +1,17 @@
+package(default_visibility = ["//visibility:public"])
+
+load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library")
+
+genrule(
+    name = "genrule_finisher",
+    outs = [
+        "finisher_plant.h",
+        "finisher_plant.cc",
+        "finisher_integral_plant.h",
+        "finisher_integral_plant.cc",
+    ],
+    cmd = "$(location //y2020/control_loops/python:finisher) $(OUTS)",
+    tools = [
+        "//y2020/control_loops/python:finisher",
+    ],
+)
diff --git a/y2020/control_loops/superstructure/shooter/BUILD b/y2020/control_loops/superstructure/shooter/BUILD
new file mode 100644
index 0000000..d63231b
--- /dev/null
+++ b/y2020/control_loops/superstructure/shooter/BUILD
@@ -0,0 +1,58 @@
+package(default_visibility = ["//visibility:public"])
+
+load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library")
+
+cc_library(
+    name = "shooter_plants",
+    srcs = [
+        "//y2020/control_loops/superstructure/accelerator:accelerator_integral_plant.cc",
+        "//y2020/control_loops/superstructure/accelerator:accelerator_plant.cc",
+        "//y2020/control_loops/superstructure/finisher:finisher_integral_plant.cc",
+        "//y2020/control_loops/superstructure/finisher:finisher_plant.cc",
+    ],
+    hdrs = [
+        "//y2020/control_loops/superstructure/accelerator:accelerator_integral_plant.h",
+        "//y2020/control_loops/superstructure/accelerator:accelerator_plant.h",
+        "//y2020/control_loops/superstructure/finisher:finisher_integral_plant.h",
+        "//y2020/control_loops/superstructure/finisher:finisher_plant.h",
+    ],
+    deps = [
+        "//frc971/control_loops:state_feedback_loop",
+    ],
+)
+
+cc_library(
+    name = "shooter",
+    srcs = [
+        "shooter.cc",
+    ],
+    hdrs = [
+        "shooter.h",
+    ],
+    deps = [
+        ":flywheel_controller",
+        "//aos/controls:control_loop",
+        "//frc971/control_loops:profiled_subsystem",
+        "//y2020/control_loops/superstructure:superstructure_goal_fbs",
+        "//y2020/control_loops/superstructure:superstructure_output_fbs",
+        "//y2020/control_loops/superstructure:superstructure_position_fbs",
+        "//y2020/control_loops/superstructure:superstructure_status_fbs",
+    ],
+)
+
+cc_library(
+    name = "flywheel_controller",
+    srcs = [
+        "flywheel_controller.cc",
+    ],
+    hdrs = [
+        "flywheel_controller.h",
+    ],
+    deps = [
+        ":shooter_plants",
+        "//aos/controls:control_loop",
+        "//frc971/control_loops:profiled_subsystem",
+        "//y2020/control_loops/superstructure:superstructure_goal_fbs",
+        "//y2020/control_loops/superstructure:superstructure_status_fbs",
+    ],
+)
diff --git a/y2020/control_loops/superstructure/shooter/flywheel_controller.cc b/y2020/control_loops/superstructure/shooter/flywheel_controller.cc
new file mode 100644
index 0000000..0f920a4
--- /dev/null
+++ b/y2020/control_loops/superstructure/shooter/flywheel_controller.cc
@@ -0,0 +1,70 @@
+#include "y2020/control_loops/superstructure/shooter/flywheel_controller.h"
+
+#include <chrono>
+
+#include "aos/logging/logging.h"
+#include "y2020/control_loops/superstructure/accelerator/accelerator_plant.h"
+#include "y2020/control_loops/superstructure/finisher/finisher_plant.h"
+
+namespace y2020 {
+namespace control_loops {
+namespace superstructure {
+namespace shooter {
+
+FlywheelController::FlywheelController(StateFeedbackLoop<3, 1, 1> &&loop)
+    : loop_(new StateFeedbackLoop<3, 1, 1>(std::move(loop))) {
+  history_.fill(0);
+  Y_.setZero();
+}
+
+void FlywheelController::set_goal(double angular_velocity_goal) {
+  loop_->mutable_next_R() << 0.0, angular_velocity_goal, 0.0;
+  last_goal_ = angular_velocity_goal;
+}
+
+void FlywheelController::set_position(double current_position) {
+  // Update position in the model.
+  Y_ << current_position;
+
+  // Add the position to the history.
+  history_[history_position_] = current_position;
+  history_position_ = (history_position_ + 1) % kHistoryLength;
+}
+
+double FlywheelController::voltage() const { return loop_->U(0, 0); }
+
+void FlywheelController::Update(bool disabled) {
+  loop_->mutable_R() = loop_->next_R();
+  if (loop_->R(1, 0) < 1.0) {
+    // Kill power at low angular velocities.
+    disabled = true;
+  }
+
+  loop_->Correct(Y_);
+  loop_->Update(disabled);
+}
+
+flatbuffers::Offset<FlywheelControllerStatus> FlywheelController::SetStatus(
+    flatbuffers::FlatBufferBuilder *fbb) {
+  // Compute the oldest point in the history.
+  const int oldest_history_position =
+      ((history_position_ == 0) ? kHistoryLength : history_position_) - 1;
+
+  // Compute the distance moved over that time period.
+  const double avg_angular_velocity =
+      (history_[oldest_history_position] - history_[history_position_]) /
+      (::aos::time::DurationInSeconds(::aos::controls::kLoopFrequency) *
+       static_cast<double>(kHistoryLength - 1));
+
+  FlywheelControllerStatusBuilder builder(*fbb);
+
+  builder.add_avg_angular_velocity(avg_angular_velocity);
+  builder.add_angular_velocity(loop_->X_hat(1, 0));
+  builder.add_angular_velocity_goal(last_goal_);
+  return builder.Finish();
+}
+
+}  // namespace shooter
+}  // namespace superstructure
+}  // namespace control_loops
+}  // namespace y2020
diff --git a/y2020/control_loops/superstructure/shooter/flywheel_controller.h b/y2020/control_loops/superstructure/shooter/flywheel_controller.h
new file mode 100644
index 0000000..20cd681
--- /dev/null
+++ b/y2020/control_loops/superstructure/shooter/flywheel_controller.h
@@ -0,0 +1,61 @@
+#ifndef Y2020_CONTROL_LOOPS_SHOOTER_FLYWHEEL_CONTROLLER_H_
+#define Y2020_CONTROL_LOOPS_SHOOTER_FLYWHEEL_CONTROLLER_H_
+
+#include <memory>
+
+#include "aos/controls/control_loop.h"
+#include "aos/time/time.h"
+#include "frc971/control_loops/state_feedback_loop.h"
+#include "y2020/control_loops/superstructure/accelerator/accelerator_integral_plant.h"
+#include "y2020/control_loops/superstructure/finisher/finisher_integral_plant.h"
+#include "y2020/control_loops/superstructure/superstructure_status_generated.h"
+
+namespace y2020 {
+namespace control_loops {
+namespace superstructure {
+namespace shooter {
+
+// Handles the velocity control of each flywheel.
+class FlywheelController {
+ public:
+  FlywheelController(StateFeedbackLoop<3, 1, 1> &&loop);
+
+  // Sets the velocity goal in radians/sec
+  void set_goal(double angular_velocity_goal);
+  // Sets the current encoder position in radians
+  void set_position(double current_position);
+
+  // Populates the status structure.
+  flatbuffers::Offset<FlywheelControllerStatus> SetStatus(
+      flatbuffers::FlatBufferBuilder *fbb);
+
+  // Returns the control loop calculated voltage.
+  double voltage() const;
+
+  // Returns the instantaneous velocity.
+  double velocity() const { return loop_->X_hat(1, 0); }
+
+  // Executes the control loop for a cycle.
+  void Update(bool disabled);
+
+ private:
+  // The current sensor measurement.
+  Eigen::Matrix<double, 1, 1> Y_;
+  // The control loop.
+  ::std::unique_ptr<StateFeedbackLoop<3, 1, 1>> loop_;
+
+  // History array for calculating a filtered angular velocity.
+  static constexpr int kHistoryLength = 10;
+  ::std::array<double, kHistoryLength> history_;
+  ptrdiff_t history_position_ = 0;
+  double last_goal_ = 0;
+
+  DISALLOW_COPY_AND_ASSIGN(FlywheelController);
+};
+
+}  // namespace shooter
+}  // namespace superstructure
+}  // namespace control_loops
+}  // namespace y2020
+
+#endif  // Y2020_CONTROL_LOOPS_SHOOTER_FLYWHEEL_CONTROLLER_H_
diff --git a/y2020/control_loops/superstructure/shooter/shooter.cc b/y2020/control_loops/superstructure/shooter/shooter.cc
new file mode 100644
index 0000000..6a1d201
--- /dev/null
+++ b/y2020/control_loops/superstructure/shooter/shooter.cc
@@ -0,0 +1,62 @@
+#include "y2020/control_loops/superstructure/shooter/shooter.h"
+
+#include <chrono>
+
+#include "aos/logging/logging.h"
+#include "y2020/control_loops/superstructure/accelerator/accelerator_plant.h"
+#include "y2020/control_loops/superstructure/finisher/finisher_plant.h"
+
+namespace y2020 {
+namespace control_loops {
+namespace superstructure {
+namespace shooter {
+
+Shooter::Shooter()
+    : finisher_(finisher::MakeIntegralFinisherLoop()),
+      accelerator_left_(accelerator::MakeIntegralAcceleratorLoop()),
+      accelerator_right_(accelerator::MakeIntegralAcceleratorLoop()) {}
+
+flatbuffers::Offset<ShooterStatus> Shooter::RunIteration(
+    const ShooterGoal *goal, const ShooterPosition *position,
+    flatbuffers::FlatBufferBuilder *fbb, OutputT *output) {
+  if (goal) {
+    // Update position/goal for our two shooter sides.
+    finisher_.set_goal(goal->velocity_finisher());
+    accelerator_left_.set_goal(goal->velocity_accelerator());
+    accelerator_right_.set_goal(goal->velocity_accelerator());
+  }
+
+  finisher_.set_position(position->theta_finisher());
+  accelerator_left_.set_position(position->theta_accelerator_left());
+  accelerator_right_.set_position(position->theta_accelerator_right());
+
+  finisher_.Update(output == nullptr);
+  accelerator_left_.Update(output == nullptr);
+  accelerator_right_.Update(output == nullptr);
+
+  flatbuffers::Offset<FlywheelControllerStatus> finisher_status_offset =
+      finisher_.SetStatus(fbb);
+  flatbuffers::Offset<FlywheelControllerStatus> accelerator_left_status_offset =
+      accelerator_left_.SetStatus(fbb);
+  flatbuffers::Offset<FlywheelControllerStatus>
+      accelerator_right_status_offset = accelerator_right_.SetStatus(fbb);
+
+  ShooterStatusBuilder status_builder(*fbb);
+
+  status_builder.add_finisher(finisher_status_offset);
+  status_builder.add_accelerator_left(accelerator_left_status_offset);
+  status_builder.add_accelerator_right(accelerator_right_status_offset);
+
+  if (output) {
+    output->finisher_voltage = finisher_.voltage();
+    output->accelerator_left_voltage = accelerator_left_.voltage();
+    output->accelerator_right_voltage = accelerator_right_.voltage();
+  }
+
+  return status_builder.Finish();
+}
+
+}  // namespace shooter
+}  // namespace superstructure
+}  // namespace control_loops
+}  // namespace y2020
diff --git a/y2020/control_loops/superstructure/shooter/shooter.h b/y2020/control_loops/superstructure/shooter/shooter.h
new file mode 100644
index 0000000..88bcb3b
--- /dev/null
+++ b/y2020/control_loops/superstructure/shooter/shooter.h
@@ -0,0 +1,37 @@
+#ifndef Y2020_CONTROL_LOOPS_SHOOTER_SHOOTER_H_
+#define Y2020_CONTROL_LOOPS_SHOOTER_SHOOTER_H_
+
+#include "aos/controls/control_loop.h"
+#include "frc971/control_loops/state_feedback_loop.h"
+#include "y2020/control_loops/superstructure/shooter/flywheel_controller.h"
+#include "y2020/control_loops/superstructure/superstructure_goal_generated.h"
+#include "y2020/control_loops/superstructure/superstructure_output_generated.h"
+#include "y2020/control_loops/superstructure/superstructure_position_generated.h"
+#include "y2020/control_loops/superstructure/superstructure_status_generated.h"
+
+namespace y2020 {
+namespace control_loops {
+namespace superstructure {
+namespace shooter {
+
+// Handles all flywheels together.
+class Shooter {
+ public:
+  Shooter();
+
+  flatbuffers::Offset<ShooterStatus> RunIteration(
+      const ShooterGoal *goal, const ShooterPosition *position,
+      flatbuffers::FlatBufferBuilder *fbb, OutputT *output);
+
+ private:
+  FlywheelController finisher_, accelerator_left_, accelerator_right_;
+
+  DISALLOW_COPY_AND_ASSIGN(Shooter);
+};
+
+}  // namespace shooter
+}  // namespace superstructure
+}  // namespace control_loops
+}  // namespace y2020
+
+#endif  // Y2020_CONTROL_LOOPS_SHOOTER_SHOOTER_H_
diff --git a/y2020/control_loops/superstructure/superstructure.cc b/y2020/control_loops/superstructure/superstructure.cc
index 91a7586..c2512db 100644
--- a/y2020/control_loops/superstructure/superstructure.cc
+++ b/y2020/control_loops/superstructure/superstructure.cc
@@ -13,7 +13,9 @@
                                const ::std::string &name)
     : aos::controls::ControlLoop<Goal, Position, Status, Output>(event_loop,
                                                                  name),
-      hood_(constants::GetValues().hood) {
+      hood_(constants::GetValues().hood),
+      intake_joint_(constants::GetValues().intake),
+      turret_(constants::GetValues().turret.subsystem_params) {
   event_loop->SetRuntimeRealtimePriority(30);
 }
 
@@ -24,6 +26,8 @@
   if (WasReset()) {
     AOS_LOG(ERROR, "WPILib reset, restarting\n");
     hood_.Reset();
+    intake_joint_.Reset();
+    turret_.Reset();
   }
 
   OutputT output_struct;
@@ -34,16 +38,39 @@
                     output != nullptr ? &(output_struct.hood_voltage) : nullptr,
                     status->fbb());
 
+  flatbuffers::Offset<AbsoluteEncoderProfiledJointStatus> intake_status_offset =
+      intake_joint_.Iterate(
+          unsafe_goal != nullptr ? unsafe_goal->intake() : nullptr,
+          position->intake_joint(),
+          output != nullptr ? &(output_struct.intake_joint_voltage) : nullptr,
+          status->fbb());
+
+  flatbuffers::Offset<PotAndAbsoluteEncoderProfiledJointStatus>
+      turret_status_offset = turret_.Iterate(
+          unsafe_goal != nullptr ? unsafe_goal->turret() : nullptr,
+          position->turret(),
+          output != nullptr ? &(output_struct.turret_voltage) : nullptr,
+          status->fbb());
+
+  climber_.Iterate(unsafe_goal, output != nullptr ? &(output_struct) : nullptr);
+
   bool zeroed;
   bool estopped;
 
-  const AbsoluteEncoderProfiledJointStatus *hood_status =
-      GetMutableTemporaryPointer(*status->fbb(), hood_status_offset);
-  zeroed = hood_status->zeroed();
-  estopped = hood_status->estopped();
+  {
+    const AbsoluteEncoderProfiledJointStatus *const hood_status =
+        GetMutableTemporaryPointer(*status->fbb(), hood_status_offset);
 
-  if (output != nullptr) {
-    output->Send(Output::Pack(*output->fbb(), &output_struct));
+    const AbsoluteEncoderProfiledJointStatus *const intake_status =
+        GetMutableTemporaryPointer(*status->fbb(), intake_status_offset);
+
+    const PotAndAbsoluteEncoderProfiledJointStatus *const turret_status =
+        GetMutableTemporaryPointer(*status->fbb(), turret_status_offset);
+
+    zeroed = hood_status->zeroed() && intake_status->zeroed() &&
+             turret_status->zeroed();
+    estopped = hood_status->estopped() || intake_status->estopped() ||
+               turret_status->estopped();
   }
 
   Status::Builder status_builder = status->MakeBuilder<Status>();
@@ -52,8 +79,19 @@
   status_builder.add_estopped(estopped);
 
   status_builder.add_hood(hood_status_offset);
+  status_builder.add_intake(intake_status_offset);
+  status_builder.add_turret(turret_status_offset);
 
   status->Send(status_builder.Finish());
+
+  if (output != nullptr) {
+    if (unsafe_goal) {
+      output_struct.intake_roller_voltage = unsafe_goal->roller_voltage();
+    } else {
+      output_struct.intake_roller_voltage = 0.0;
+    }
+    output->Send(Output::Pack(*output->fbb(), &output_struct));
+  }
 }
 
 }  // namespace superstructure
diff --git a/y2020/control_loops/superstructure/superstructure.h b/y2020/control_loops/superstructure/superstructure.h
index dbc1ccd..2bc0cda 100644
--- a/y2020/control_loops/superstructure/superstructure.h
+++ b/y2020/control_loops/superstructure/superstructure.h
@@ -8,6 +8,7 @@
 #include "y2020/control_loops/superstructure/superstructure_output_generated.h"
 #include "y2020/control_loops/superstructure/superstructure_position_generated.h"
 #include "y2020/control_loops/superstructure/superstructure_status_generated.h"
+#include "y2020/control_loops/superstructure/climber.h"
 
 namespace y2020 {
 namespace control_loops {
@@ -19,12 +20,18 @@
   explicit Superstructure(::aos::EventLoop *event_loop,
                           const ::std::string &name = "/superstructure");
 
+  using PotAndAbsoluteEncoderSubsystem =
+      ::frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystem<
+          ::frc971::zeroing::PotAndAbsoluteEncoderZeroingEstimator,
+          ::frc971::control_loops::PotAndAbsoluteEncoderProfiledJointStatus>;
   using AbsoluteEncoderSubsystem =
       ::frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystem<
           ::frc971::zeroing::AbsoluteEncoderZeroingEstimator,
           ::frc971::control_loops::AbsoluteEncoderProfiledJointStatus>;
 
   const AbsoluteEncoderSubsystem &hood() const { return hood_; }
+  const AbsoluteEncoderSubsystem &intake_joint() const { return intake_joint_; }
+  const PotAndAbsoluteEncoderSubsystem &turret() const { return turret_; }
 
  protected:
   virtual void RunIteration(const Goal *unsafe_goal, const Position *position,
@@ -33,7 +40,10 @@
 
  private:
   AbsoluteEncoderSubsystem hood_;
+  AbsoluteEncoderSubsystem intake_joint_;
+  PotAndAbsoluteEncoderSubsystem turret_;
 
+  Climber climber_;
   DISALLOW_COPY_AND_ASSIGN(Superstructure);
 };
 
diff --git a/y2020/control_loops/superstructure/superstructure_goal.fbs b/y2020/control_loops/superstructure/superstructure_goal.fbs
index 4b1d8e9..f066ab0 100644
--- a/y2020/control_loops/superstructure/superstructure_goal.fbs
+++ b/y2020/control_loops/superstructure/superstructure_goal.fbs
@@ -5,10 +5,10 @@
 table ShooterGoal {
   // Angular velocity in rad/s of the slowest (lowest) wheel in the kicker.
   // Positive is shooting the ball.
-  velocity_kicker:double;
+  velocity_accelerator:double;
 
   // Angular velocity in rad/s of the flywheel. Positive is shooting.
-  velocity_flywheel:double;
+  velocity_finisher:double;
 }
 
 table Goal {
@@ -16,11 +16,15 @@
   // Only applies if hood_tracking = false.
   hood:frc971.control_loops.StaticZeroingSingleDOFProfiledSubsystemGoal;
 
-  //0 = Linkage on sprocket is pointing straight up
-  //Positive = forward
+  // Positive = counterclockwise from above; rotates Wheel of Fortune clockwise
+  // Zero is relative to start.
+  control_panel:frc971.control_loops.StaticZeroingSingleDOFProfiledSubsystemGoal;
+
+  // 0 = Linkage on sprocket is pointing straight up
+  // Positive = forward
   intake:frc971.control_loops.StaticZeroingSingleDOFProfiledSubsystemGoal;
 
-  //Positive is rollers intaking to Washing Machine.
+  // Positive is rollers intaking to Washing Machine.
   roller_voltage:float;
 
   // 0 = facing the front of the robot. Positive rotates counterclockwise.
@@ -43,6 +47,9 @@
 
   // Whether the kicker and flywheel should choose a velocity automatically.
   shooter_tracking:bool;
+
+  // Positive is deploying climber and to climb; cannot run in reverse
+  climber_voltage:float;
 }
 
 root_type Goal;
diff --git a/y2020/control_loops/superstructure/superstructure_lib_test.cc b/y2020/control_loops/superstructure/superstructure_lib_test.cc
index 468e0c9..458a771 100644
--- a/y2020/control_loops/superstructure/superstructure_lib_test.cc
+++ b/y2020/control_loops/superstructure/superstructure_lib_test.cc
@@ -10,6 +10,7 @@
 #include "gtest/gtest.h"
 #include "y2020/constants.h"
 #include "y2020/control_loops/superstructure/hood/hood_plant.h"
+#include "y2020/control_loops/superstructure/intake/intake_plant.h"
 #include "y2020/control_loops/superstructure/superstructure.h"
 
 namespace y2020 {
@@ -30,6 +31,8 @@
 using ::frc971::control_loops::PositionSensorSimulator;
 using ::frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemGoal;
 typedef Superstructure::AbsoluteEncoderSubsystem AbsoluteEncoderSubsystem;
+typedef Superstructure::PotAndAbsoluteEncoderSubsystem
+    PotAndAbsoluteEncoderSubsystem;
 
 // Class which simulates the superstructure and sends out queue messages with
 // the position.
@@ -47,8 +50,17 @@
 
         hood_plant_(new CappedTestPlant(hood::MakeHoodPlant())),
         hood_encoder_(constants::GetValues()
-                          .hood.zeroing_constants.one_revolution_distance) {
+                          .hood.zeroing_constants.one_revolution_distance),
+        intake_plant_(new CappedTestPlant(intake::MakeIntakePlant())),
+        intake_encoder_(constants::GetValues()
+                            .intake.zeroing_constants.one_revolution_distance),
+        turret_plant_(new CappedTestPlant(turret::MakeTurretPlant())),
+        turret_encoder_(constants::GetValues()
+                            .turret.subsystem_params.zeroing_constants
+                            .one_revolution_distance) {
     InitializeHoodPosition(constants::Values::kHoodRange().upper);
+    InitializeIntakePosition(constants::Values::kIntakeRange().upper);
+    InitializeTurretPosition(constants::Values::kTurretRange().middle());
 
     phased_loop_handle_ = event_loop_->AddPhasedLoop(
         [this](int) {
@@ -72,6 +84,26 @@
             .hood.zeroing_constants.measured_absolute_position);
   }
 
+  void InitializeIntakePosition(double start_pos) {
+    intake_plant_->mutable_X(0, 0) = start_pos;
+    intake_plant_->mutable_X(1, 0) = 0.0;
+
+    intake_encoder_.Initialize(
+        start_pos, kNoiseScalar, 0.0,
+        constants::GetValues()
+            .intake.zeroing_constants.measured_absolute_position);
+  }
+
+  void InitializeTurretPosition(double start_pos) {
+    turret_plant_->mutable_X(0, 0) = start_pos;
+    turret_plant_->mutable_X(1, 0) = 0.0;
+
+    turret_encoder_.Initialize(start_pos, kNoiseScalar, 0.0,
+                               constants::GetValues()
+                                   .turret.subsystem_params.zeroing_constants
+                                   .measured_absolute_position);
+  }
+
   // Sends a queue message with the position of the superstructure.
   void SendPositionMessage() {
     ::aos::Sender<Position>::Builder builder =
@@ -82,9 +114,21 @@
     flatbuffers::Offset<frc971::AbsolutePosition> hood_offset =
         hood_encoder_.GetSensorValues(&hood_builder);
 
+    frc971::AbsolutePosition::Builder intake_builder =
+        builder.MakeBuilder<frc971::AbsolutePosition>();
+    flatbuffers::Offset<frc971::AbsolutePosition> intake_offset =
+        intake_encoder_.GetSensorValues(&intake_builder);
+
+    frc971::PotAndAbsolutePosition::Builder turret_builder =
+        builder.MakeBuilder<frc971::PotAndAbsolutePosition>();
+    flatbuffers::Offset<frc971::PotAndAbsolutePosition> turret_offset =
+        turret_encoder_.GetSensorValues(&turret_builder);
+
     Position::Builder position_builder = builder.MakeBuilder<Position>();
 
     position_builder.add_hood(hood_offset);
+    position_builder.add_intake_joint(intake_offset);
+    position_builder.add_turret(turret_offset);
 
     builder.Send(position_builder.Finish());
   }
@@ -92,9 +136,17 @@
   double hood_position() const { return hood_plant_->X(0, 0); }
   double hood_velocity() const { return hood_plant_->X(1, 0); }
 
+  double intake_position() const { return intake_plant_->X(0, 0); }
+  double intake_velocity() const { return intake_plant_->X(1, 0); }
+
+  double turret_position() const { return turret_plant_->X(0, 0); }
+  double turret_velocity() const { return turret_plant_->X(1, 0); }
+
   // Simulates the superstructure for a single timestep.
   void Simulate() {
     const double last_hood_velocity = hood_velocity();
+    const double last_intake_velocity = intake_velocity();
+    const double last_turret_velocity = turret_velocity();
 
     EXPECT_TRUE(superstructure_output_fetcher_.Fetch());
     EXPECT_TRUE(superstructure_status_fetcher_.Fetch());
@@ -109,35 +161,105 @@
     EXPECT_NEAR(superstructure_output_fetcher_->hood_voltage(), 0.0,
                 voltage_check_hood);
 
+    const double voltage_check_intake =
+        (static_cast<AbsoluteEncoderSubsystem::State>(
+             superstructure_status_fetcher_->intake()->state()) ==
+         AbsoluteEncoderSubsystem::State::RUNNING)
+            ? constants::GetValues().intake.operating_voltage
+            : constants::GetValues().intake.zeroing_voltage;
+
+    EXPECT_NEAR(superstructure_output_fetcher_->intake_joint_voltage(), 0.0,
+                voltage_check_intake);
+
+    const double voltage_check_turret =
+        (static_cast<PotAndAbsoluteEncoderSubsystem::State>(
+             superstructure_status_fetcher_->turret()->state()) ==
+         PotAndAbsoluteEncoderSubsystem::State::RUNNING)
+            ? constants::GetValues().turret.subsystem_params.operating_voltage
+            : constants::GetValues().turret.subsystem_params.zeroing_voltage;
+
+    EXPECT_NEAR(superstructure_output_fetcher_->turret_voltage(), 0.0,
+                voltage_check_turret);
+
     ::Eigen::Matrix<double, 1, 1> hood_U;
     hood_U << superstructure_output_fetcher_->hood_voltage() +
                   hood_plant_->voltage_offset();
 
+    ::Eigen::Matrix<double, 1, 1> intake_U;
+    intake_U << superstructure_output_fetcher_->intake_joint_voltage() +
+                    intake_plant_->voltage_offset();
+
+    ::Eigen::Matrix<double, 1, 1> turret_U;
+    turret_U << superstructure_output_fetcher_->turret_voltage() +
+                    turret_plant_->voltage_offset();
+
     hood_plant_->Update(hood_U);
+    intake_plant_->Update(intake_U);
+    turret_plant_->Update(turret_U);
 
     const double position_hood = hood_plant_->Y(0, 0);
+    const double position_intake = intake_plant_->Y(0, 0);
+    const double position_turret = turret_plant_->Y(0, 0);
 
     hood_encoder_.MoveTo(position_hood);
+    intake_encoder_.MoveTo(position_intake);
+    turret_encoder_.MoveTo(position_turret);
 
     EXPECT_GE(position_hood, constants::Values::kHoodRange().lower_hard);
     EXPECT_LE(position_hood, constants::Values::kHoodRange().upper_hard);
 
+    EXPECT_GE(position_intake, constants::Values::kIntakeRange().lower_hard);
+    EXPECT_LE(position_intake, constants::Values::kIntakeRange().upper_hard);
+
+    EXPECT_GE(position_turret, constants::Values::kTurretRange().lower_hard);
+    EXPECT_LE(position_turret, constants::Values::kTurretRange().upper_hard);
+
     const double loop_time = ::aos::time::DurationInSeconds(dt_);
 
     const double hood_acceleration =
         (hood_velocity() - last_hood_velocity) / loop_time;
 
+    const double intake_acceleration =
+        (intake_velocity() - last_intake_velocity) / loop_time;
+
+    const double turret_acceleration =
+        (turret_velocity() - last_turret_velocity) / loop_time;
+
     EXPECT_GE(peak_hood_acceleration_, hood_acceleration);
     EXPECT_LE(-peak_hood_acceleration_, hood_acceleration);
     EXPECT_GE(peak_hood_velocity_, hood_velocity());
     EXPECT_LE(-peak_hood_velocity_, hood_velocity());
+
+    EXPECT_GE(peak_intake_acceleration_, intake_acceleration);
+    EXPECT_LE(-peak_intake_acceleration_, intake_acceleration);
+    EXPECT_GE(peak_intake_velocity_, intake_velocity());
+    EXPECT_LE(-peak_intake_velocity_, intake_velocity());
+
+    EXPECT_GE(peak_turret_acceleration_, turret_acceleration);
+    EXPECT_LE(-peak_turret_acceleration_, turret_acceleration);
+    EXPECT_GE(peak_turret_velocity_, turret_velocity());
+    EXPECT_LE(-peak_turret_velocity_, turret_velocity());
+
+    climber_voltage_ = superstructure_output_fetcher_->climber_voltage();
   }
 
+  float climber_voltage() const { return climber_voltage_; }
+
   void set_peak_hood_acceleration(double value) {
     peak_hood_acceleration_ = value;
   }
   void set_peak_hood_velocity(double value) { peak_hood_velocity_ = value; }
 
+  void set_peak_intake_acceleration(double value) {
+    peak_intake_acceleration_ = value;
+  }
+  void set_peak_intake_velocity(double value) { peak_intake_velocity_ = value; }
+
+  void set_peak_turret_acceleration(double value) {
+    peak_turret_acceleration_ = value;
+  }
+  void set_peak_turret_velocity(double value) { peak_turret_velocity_ = value; }
+
  private:
   ::aos::EventLoop *event_loop_;
   const chrono::nanoseconds dt_;
@@ -152,11 +274,23 @@
   ::std::unique_ptr<CappedTestPlant> hood_plant_;
   PositionSensorSimulator hood_encoder_;
 
+  ::std::unique_ptr<CappedTestPlant> intake_plant_;
+  PositionSensorSimulator intake_encoder_;
+
+  ::std::unique_ptr<CappedTestPlant> turret_plant_;
+  PositionSensorSimulator turret_encoder_;
+
   // The acceleration limits to check for while moving.
   double peak_hood_acceleration_ = 1e10;
+  double peak_intake_acceleration_ = 1e10;
+  double peak_turret_acceleration_ = 1e10;
 
   // The velocity limits to check for while moving.
   double peak_hood_velocity_ = 1e10;
+  double peak_intake_velocity_ = 1e10;
+  double peak_turret_velocity_ = 1e10;
+
+  float climber_voltage_ = 0.0f;
 };
 
 class SuperstructureTest : public ::aos::testing::ControlLoopTest {
@@ -187,13 +321,26 @@
     superstructure_goal_fetcher_.Fetch();
     superstructure_status_fetcher_.Fetch();
 
-    EXPECT_NEAR(superstructure_goal_fetcher_->hood()->unsafe_goal(),
-                superstructure_status_fetcher_->hood()->position(), 0.001);
+    // Only check the goal if there is one.
+    if (superstructure_goal_fetcher_->has_hood()) {
+      EXPECT_NEAR(superstructure_goal_fetcher_->hood()->unsafe_goal(),
+                  superstructure_status_fetcher_->hood()->position(), 0.001);
+    }
+
+    if (superstructure_goal_fetcher_->has_intake()) {
+      EXPECT_NEAR(superstructure_goal_fetcher_->intake()->unsafe_goal(),
+                  superstructure_status_fetcher_->intake()->position(), 0.001);
+    }
+
+    if (superstructure_goal_fetcher_->has_turret()) {
+      EXPECT_NEAR(superstructure_goal_fetcher_->turret()->unsafe_goal(),
+                  superstructure_status_fetcher_->turret()->position(), 0.001);
+    }
   }
 
   void CheckIfZeroed() {
-      superstructure_status_fetcher_.Fetch();
-      ASSERT_TRUE(superstructure_status_fetcher_.get()->zeroed());
+    superstructure_status_fetcher_.Fetch();
+    ASSERT_TRUE(superstructure_status_fetcher_.get()->zeroed());
   }
 
   void WaitUntilZeroed() {
@@ -230,7 +377,10 @@
 // Tests that the superstructure does nothing when the goal is to remain still.
 TEST_F(SuperstructureTest, DoesNothing) {
   SetEnabled(true);
-  superstructure_plant_.InitializeHoodPosition(0.77);
+  superstructure_plant_.InitializeHoodPosition(
+      constants::Values::kHoodRange().middle());
+  superstructure_plant_.InitializeIntakePosition(
+      constants::Values::kIntakeRange().middle());
 
   WaitUntilZeroed();
 
@@ -239,11 +389,21 @@
 
     flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
         hood_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
-            *builder.fbb(), 0.77);
+            *builder.fbb(), constants::Values::kHoodRange().middle());
+
+    flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
+        intake_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+            *builder.fbb(), constants::Values::kIntakeRange().middle());
+
+    flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
+        turret_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+            *builder.fbb(), constants::Values::kTurretRange().middle() + 1.0);
 
     Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
 
     goal_builder.add_hood(hood_offset);
+    goal_builder.add_intake(intake_offset);
+    goal_builder.add_turret(turret_offset);
 
     ASSERT_TRUE(builder.Send(goal_builder.Finish()));
   }
@@ -259,6 +419,7 @@
   // Set a reasonable goal.
 
   superstructure_plant_.InitializeHoodPosition(0.7);
+  superstructure_plant_.InitializeIntakePosition(0.7);
 
   WaitUntilZeroed();
   {
@@ -269,9 +430,20 @@
             *builder.fbb(), 0.2,
             CreateProfileParameters(*builder.fbb(), 1.0, 0.2));
 
+    flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
+        intake_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+            *builder.fbb(), 0.2,
+            CreateProfileParameters(*builder.fbb(), 1.0, 0.2));
+
+    flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
+        turret_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+            *builder.fbb(), constants::Values::kTurretRange().middle() + 1.0);
+
     Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
 
     goal_builder.add_hood(hood_offset);
+    goal_builder.add_intake(intake_offset);
+    goal_builder.add_turret(turret_offset);
 
     ASSERT_TRUE(builder.Send(goal_builder.Finish()));
   }
@@ -283,8 +455,6 @@
 }
 // Makes sure that the voltage on a motor is properly pulled back after
 // saturation such that we don't get weird or bad (e.g. oscillating) behaviour.
-//
-// We are going to disable collision detection to make this easier to implement.
 TEST_F(SuperstructureTest, SaturationTest) {
   SetEnabled(true);
   // Zero it before we move.
@@ -296,9 +466,19 @@
         hood_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
             *builder.fbb(), constants::Values::kHoodRange().upper);
 
+    flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
+        intake_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+            *builder.fbb(), constants::Values::kIntakeRange().upper);
+
+    flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
+        turret_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+            *builder.fbb(), constants::Values::kTurretRange().middle() + 1.0);
+
     Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
 
     goal_builder.add_hood(hood_offset);
+    goal_builder.add_intake(intake_offset);
+    goal_builder.add_turret(turret_offset);
 
     ASSERT_TRUE(builder.Send(goal_builder.Finish()));
   }
@@ -315,16 +495,34 @@
             *builder.fbb(), constants::Values::kHoodRange().lower,
             CreateProfileParameters(*builder.fbb(), 20.0, 0.1));
 
+    flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
+        intake_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+            *builder.fbb(), constants::Values::kIntakeRange().lower,
+            CreateProfileParameters(*builder.fbb(), 20.0, 0.1));
+
+    flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
+        turret_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+            *builder.fbb(), constants::Values::kTurretRange().middle() + 1.0);
+
     Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
 
     goal_builder.add_hood(hood_offset);
+    goal_builder.add_intake(intake_offset);
+    goal_builder.add_turret(turret_offset);
 
     ASSERT_TRUE(builder.Send(goal_builder.Finish()));
   }
   superstructure_plant_.set_peak_hood_velocity(23.0);
   superstructure_plant_.set_peak_hood_acceleration(0.2);
 
-  RunFor(chrono::seconds(8));
+  superstructure_plant_.set_peak_intake_velocity(23.0);
+  superstructure_plant_.set_peak_intake_acceleration(0.2);
+
+  superstructure_plant_.set_peak_turret_velocity(23.0);
+  superstructure_plant_.set_peak_turret_acceleration(0.2);
+
+  // Intake needs over 8 seconds to reach the goal
+  RunFor(chrono::seconds(9));
   VerifyNearGoal();
 }
 
@@ -335,6 +533,12 @@
   RunFor(chrono::seconds(2));
   EXPECT_EQ(AbsoluteEncoderSubsystem::State::RUNNING,
             superstructure_.hood().state());
+
+  EXPECT_EQ(AbsoluteEncoderSubsystem::State::RUNNING,
+            superstructure_.intake_joint().state());
+
+  EXPECT_EQ(PotAndAbsoluteEncoderSubsystem::State::RUNNING,
+            superstructure_.turret().state());
 }
 
 // Tests that running disabled works
@@ -343,6 +547,47 @@
   CheckIfZeroed();
 }
 
+// Tests that the climber passes through per the design.
+TEST_F(SuperstructureTest, Climber) {
+  SetEnabled(true);
+  // Set a reasonable goal.
+
+  superstructure_plant_.InitializeHoodPosition(0.7);
+  superstructure_plant_.InitializeIntakePosition(0.7);
+
+  WaitUntilZeroed();
+  {
+    auto builder = superstructure_goal_sender_.MakeBuilder();
+
+    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+    goal_builder.add_climber_voltage(-10.0);
+
+    ASSERT_TRUE(builder.Send(goal_builder.Finish()));
+  }
+
+  // Give it time to stabilize.
+  RunFor(chrono::seconds(1));
+
+  // Can't go backwards.
+  EXPECT_EQ(superstructure_plant_.climber_voltage(), 0.0);
+
+  {
+    auto builder = superstructure_goal_sender_.MakeBuilder();
+
+    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+    goal_builder.add_climber_voltage(10.0);
+
+    ASSERT_TRUE(builder.Send(goal_builder.Finish()));
+  }
+  RunFor(chrono::seconds(1));
+  // But forwards works.
+  EXPECT_EQ(superstructure_plant_.climber_voltage(), 10.0);
+
+  VerifyNearGoal();
+}
+
 }  // namespace testing
 }  // namespace superstructure
 }  // namespace control_loops
diff --git a/y2020/control_loops/superstructure/superstructure_output.fbs b/y2020/control_loops/superstructure/superstructure_output.fbs
index 9048b33..2583106 100644
--- a/y2020/control_loops/superstructure/superstructure_output.fbs
+++ b/y2020/control_loops/superstructure/superstructure_output.fbs
@@ -23,11 +23,17 @@
   washing_machine_spinner_voltage:double;
 
   // Voltage sent to the kicker. Positive is shooting.
-  kicker_left_voltage:double;
-  kicker_right_voltage:double;
+  accelerator_left_voltage:double;
+  accelerator_right_voltage:double;
 
   // Voltage sent to the flywheel. Positive is shooting.
-  flywheel_voltage:double;
+  finisher_voltage:double;
+
+  // Voltage sent to the motor driving the control panel. Positive is counterclockwise from above.
+  control_panel_voltage:double;
+
+  // Positive is deploying climber and to climb; cannot run in reverse
+  climber_voltage:double;
 }
 
 root_type Output;
diff --git a/y2020/control_loops/superstructure/superstructure_position.fbs b/y2020/control_loops/superstructure/superstructure_position.fbs
index ed73e52..ed4ba18 100644
--- a/y2020/control_loops/superstructure/superstructure_position.fbs
+++ b/y2020/control_loops/superstructure/superstructure_position.fbs
@@ -4,16 +4,16 @@
 
 table ShooterPosition {
   // Flywheel angle in radians, positive is shooting.
-  theta_flywheel:double;
+  theta_finisher:double;
 
   // Kicker angle in radians of the slowest (lowest) wheel, positive is
   // accelerating the ball toward the shooter.
-  theta_kicker_left:double;
-  theta_kicker_right:double;
+  theta_accelerator_left:double;
+  theta_accelerator_right:double;
 }
 
 table Position {
-  // Zero is at the horizontal, positive towards the front (meters on the lead screw).
+  // Zero is at the horizontal, positive towards the front (radians).
   hood:frc971.AbsolutePosition;
 
   // Position of the intake. 0 when four-bar is vertical, positive extended.
@@ -24,6 +24,9 @@
 
   // Position of the kicker and flywheel
   shooter:ShooterPosition;
+
+   // Position of the control panel, relative to start, positive counterclockwise from above.
+  control_panel:frc971.RelativePosition;
 }
 
 root_type Position;
diff --git a/y2020/control_loops/superstructure/superstructure_status.fbs b/y2020/control_loops/superstructure/superstructure_status.fbs
index 45dbba8..023a242 100644
--- a/y2020/control_loops/superstructure/superstructure_status.fbs
+++ b/y2020/control_loops/superstructure/superstructure_status.fbs
@@ -3,7 +3,7 @@
 
 namespace y2020.control_loops.superstructure;
 
-table ShooterSegmentStatus {
+table FlywheelControllerStatus {
   // The current average velocity in radians/second over the last kHistoryLength
   // in shooter.h
   avg_angular_velocity:double;
@@ -18,19 +18,19 @@
 
 table ShooterStatus {
   // The final wheel shooting the ball
-  flywheel:ShooterSegmentStatus;
+  finisher:FlywheelControllerStatus;
 
-  // The subsystem to accelerate the ball before the flywheel
+  // The subsystem to accelerate the ball before the finisher
   // Velocity is the slowest (lowest) wheel
-  kicker_left:ShooterSegmentStatus;
-  kicker_right:ShooterSegmentStatus;
+  accelerator_left:FlywheelControllerStatus;
+  accelerator_right:FlywheelControllerStatus;
 }
 
 table Status {
   // All subsystems know their location.
   zeroed:bool;
 
-  // If true, we have aborted. This is the or of all subsystem estops.
+  // If true, we have aborted. This is if one or all subsystem estops.
   estopped:bool;
 
   //Subsystem status.
@@ -40,6 +40,9 @@
 
   // Shooter subsystem status.
   shooter:ShooterStatus;
+
+  // Status of the control_panel
+  control_panel:frc971.control_loops.RelativeEncoderProfiledJointStatus;
 }
 
 root_type Status;
diff --git a/y2020/vision/camera_reader.cc b/y2020/vision/camera_reader.cc
index de4dfb7..b8e84c0 100644
--- a/y2020/vision/camera_reader.cc
+++ b/y2020/vision/camera_reader.cc
@@ -57,6 +57,21 @@
                const std::vector<cv::KeyPoint> &keypoints,
                const cv::Mat &descriptors);
 
+  // Returns the 3D location for the specified training feature.
+  cv::Point3f Training3dPoint(int training_image_index, int feature_index) {
+    const sift::KeypointFieldLocation *const location =
+        training_data_->images()
+            ->Get(training_image_index)
+            ->features()
+            ->Get(feature_index)
+            ->field_location();
+    return cv::Point3f(location->x(), location->y(), location->z());
+  }
+
+  int number_training_images() const {
+    return training_data_->images()->size();
+  }
+
   aos::EventLoop *const event_loop_;
   const sift::TrainingData *const training_data_;
   V4L2Reader *const reader_;
@@ -144,29 +159,27 @@
     const std::vector<std::vector<cv::DMatch>> &matches) {
   // First, we need to pull out all the matches for each image. Might as well
   // build up the Match tables at the same time.
-  std::vector<std::vector<flatbuffers::Offset<sift::Match>>> per_image_matches;
+  std::vector<std::vector<sift::Match>> per_image_matches(
+      number_training_images());
   for (const std::vector<cv::DMatch> &image_matches : matches) {
     for (const cv::DMatch &image_match : image_matches) {
-      sift::Match::Builder match_builder(*fbb);
-      match_builder.add_query_feature(image_match.queryIdx);
-      match_builder.add_train_feature(image_match.trainIdx);
-      if (per_image_matches.size() <= static_cast<size_t>(image_match.imgIdx)) {
-        per_image_matches.resize(image_match.imgIdx + 1);
-      }
-      per_image_matches[image_match.imgIdx].emplace_back(
-          match_builder.Finish());
+      CHECK_LT(image_match.imgIdx, number_training_images());
+      per_image_matches[image_match.imgIdx].emplace_back();
+      sift::Match *const match = &per_image_matches[image_match.imgIdx].back();
+      match->mutate_query_feature(image_match.queryIdx);
+      match->mutate_train_feature(image_match.trainIdx);
+      match->mutate_distance(image_match.distance);
     }
   }
 
   // Then, we need to build up each ImageMatch table.
   std::vector<flatbuffers::Offset<sift::ImageMatch>> image_match_tables;
   for (size_t i = 0; i < per_image_matches.size(); ++i) {
-    const std::vector<flatbuffers::Offset<sift::Match>> &this_image_matches =
-        per_image_matches[i];
+    const std::vector<sift::Match> &this_image_matches = per_image_matches[i];
     if (this_image_matches.empty()) {
       continue;
     }
-    const auto vector_offset = fbb->CreateVector(this_image_matches);
+    const auto vector_offset = fbb->CreateVectorOfStructs(this_image_matches);
     sift::ImageMatch::Builder image_builder(*fbb);
     image_builder.add_train_image(i);
     image_builder.add_matches(vector_offset);
diff --git a/y2020/vision/sift/sift.fbs b/y2020/vision/sift/sift.fbs
index 5a1384e..77d0dc6 100644
--- a/y2020/vision/sift/sift.fbs
+++ b/y2020/vision/sift/sift.fbs
@@ -1,5 +1,12 @@
 namespace frc971.vision.sift;
 
+// Represents the location of a keypoint in field coordinates.
+struct KeypointFieldLocation {
+  x:float;
+  y:float;
+  z:float;
+}
+
 // Represents a single feature extracted from an image.
 table Feature {
   // Contains the descriptor data.
@@ -28,14 +35,20 @@
 
   // Which octave this keypoint is from.
   octave:int;
+
+  // Where this feature's keypoint is on the field. This will only be filled out
+  // for training features, not ones extracted from query images.
+  field_location:KeypointFieldLocation;
 }
 
 // Represents a single match between a training image and a query image.
-table Match {
+struct Match {
   // The index of the feature for the query image.
   query_feature:int;
   // The index of the feature for the training image.
   train_feature:int;
+  // How "good" the match is.
+  distance:float;
 }
 
 // Represents all the matches between a single training image and a query
@@ -51,11 +64,49 @@
   data:[double];
 }
 
-// Contains the information the EKF wants from an image.
+// Calibration information for a given camera on a given robot.
+table CameraCalibration {
+  // The name of the camera node which this calibration data applies to.
+  node_name:string;
+  // The team number of the robot which this calibration data applies to.
+  team_number:int;
+
+  // Intrinsics for the camera.
+  //
+  // This is the standard OpenCV intrinsics matrix in row major order (3x3).
+  intrinsics:[float];
+
+  // Fixed extrinsics for the camera. This transforms from camera coordinates to
+  // robot coordinates. For example: multiplying (0, 0, 0, 1) by this results in
+  // the position of the camera aperature in robot coordinates.
+  fixed_extrinsics:TransformationMatrix;
+
+  // Extrinsics for a camera on a turret. This will only be filled out for
+  // applicable cameras. For turret-mounted cameras, fixed_extrinsics defines
+  // a position for the center of rotation of the turret, and this field defines
+  // a position for the camera on the turret.
+  //
+  // The combination of the two transformations is underdefined, so nothing can
+  // distinguish between the two parts of the final extrinsics for a given
+  // turret position.
+  //
+  // To get the final extrinsics for a camera using this transformation,
+  // multiply (in order):
+  //   fixed_extrinsics
+  //   rotation around the Z axis by the turret angle
+  //   turret_extrinsics
+  turret_extrinsics:TransformationMatrix;
+}
+
+// Contains the information the EKF wants from an image matched against a single
+// training image.
 //
 // This is represented as a transformation to a target in field coordinates.
 table CameraPose {
   // Transformation matrix from the target to the camera's origin.
+  // (0, 0, 0) is the aperture of the camera (we pretend it's an ideal pinhole
+  // camera). Positive Z is out of the camera. Positive X and Y are right
+  // handed, but which way they face depends on the camera extrinsics.
   camera_to_target:TransformationMatrix;
 
   // Field coordinates of the target, represented as a transformation matrix
@@ -74,6 +125,7 @@
 
 table ImageMatchResult {
   // The matches from this image to each of the training images which matched.
+  // Each member is against the same captured image.
   image_matches:[ImageMatch];
   // The transformations for this image for each of the training images which
   // matched.
@@ -85,6 +137,9 @@
 
   // Timestamp when the frame was captured.
   image_monotonic_timestamp_ns:long;
+
+  // Information about the camera which took this image.
+  camera_calibration:CameraCalibration;
 }
 
 root_type ImageMatchResult;
diff --git a/y2020/vision/sift/sift_training.fbs b/y2020/vision/sift/sift_training.fbs
index 2af0233..5e82e9f 100644
--- a/y2020/vision/sift/sift_training.fbs
+++ b/y2020/vision/sift/sift_training.fbs
@@ -18,6 +18,9 @@
 // Represents the information used to match incoming images against.
 table TrainingData {
   images:[TrainingImage];
+
+  // Calibration information for all the cameras we know about.
+  camera_calibrations:[CameraCalibration];
 }
 
 root_type TrainingData;