Allow constructing control loops from flatbuffers

The core changes here are to:
* Allow constructing StateFeedbackLoop's from flatbuffers using the
  code in *state_feedback_loop_converters.*
* Add constructors to the single-dof subsystem class to make use of
  this.
* Add code to control_loops.py to generate JSON files with the requisite
  constants (these end up containing identical information to the
  generated .cc files).
* Add interfaces to actually support the new JSON codegen to single-dof
  subsystem classes.
* Convert all of the drivetrains over to generating these. This I mostly
  do so that I can write a test where Iconfirm that the .cc files and
  the JSON files generate exactly the same content.

Change-Id: Iceac48f25ecac96200b7bf992c8f34a15fe6800c
Signed-off-by: James Kuszmaul <jabukuszmaul+collab@gmail.com>
diff --git a/frc971/control_loops/BUILD b/frc971/control_loops/BUILD
index 53d6b27..1c1b797 100644
--- a/frc971/control_loops/BUILD
+++ b/frc971/control_loops/BUILD
@@ -392,6 +392,7 @@
     ],
     deps = [
         ":control_loops_ts_fbs",
+        ":state_feedback_loop_ts_fbs",
     ],
 )
 
@@ -403,6 +404,7 @@
     target_compatible_with = ["@platforms//os:linux"],
     deps = [
         ":control_loops_fbs",
+        ":state_feedback_loop_fbs",
     ],
 )
 
@@ -420,6 +422,8 @@
         ":profiled_subsystem_fbs",
         ":simple_capped_state_feedback_loop",
         ":state_feedback_loop",
+        ":state_feedback_loop_converters",
+        "//aos:flatbuffer_merge",
         "//aos/util:trapezoid_profile",
         "//frc971/control_loops:control_loop",
         "//frc971/zeroing",
@@ -549,8 +553,10 @@
     outs = [
         "static_zeroing_single_dof_profiled_subsystem_test_plant.h",
         "static_zeroing_single_dof_profiled_subsystem_test_plant.cc",
+        "static_zeroing_single_dof_profiled_subsystem_test_plant.json",
         "static_zeroing_single_dof_profiled_subsystem_test_integral_plant.h",
         "static_zeroing_single_dof_profiled_subsystem_test_integral_plant.cc",
+        "static_zeroing_single_dof_profiled_subsystem_test_integral_plant.json",
     ],
     cmd = "$(location //frc971/control_loops/python:static_zeroing_single_dof_profiled_subsystem_test) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
@@ -686,3 +692,36 @@
     ],
     target_compatible_with = ["@platforms//os:linux"],
 )
+
+static_flatbuffer(
+    name = "state_feedback_loop_fbs",
+    srcs = ["state_feedback_loop.fbs"],
+    deps = ["//frc971/math:matrix_fbs"],
+)
+
+flatbuffer_ts_library(
+    name = "state_feedback_loop_ts_fbs",
+    srcs = ["state_feedback_loop.fbs"],
+    deps = ["//frc971/math:matrix_ts_fbs"],
+)
+
+cc_library(
+    name = "hybrid_state_feedback_loop_converters",
+    srcs = ["hybrid_state_feedback_loop_converters.h"],
+    deps = [
+        ":hybrid_state_feedback_loop",
+        ":state_feedback_loop_converters",
+        ":state_feedback_loop_fbs",
+        "//frc971/math:flatbuffers_matrix",
+    ],
+)
+
+cc_library(
+    name = "state_feedback_loop_converters",
+    srcs = ["state_feedback_loop_converters.h"],
+    deps = [
+        ":state_feedback_loop",
+        ":state_feedback_loop_fbs",
+        "//frc971/math:flatbuffers_matrix",
+    ],
+)
diff --git a/frc971/control_loops/hybrid_state_feedback_loop_converters.h b/frc971/control_loops/hybrid_state_feedback_loop_converters.h
new file mode 100644
index 0000000..588a4d5
--- /dev/null
+++ b/frc971/control_loops/hybrid_state_feedback_loop_converters.h
@@ -0,0 +1,105 @@
+#ifndef FRC971_CONTROL_LOOPS_HYBRID_STATE_FEEDBACK_LOOP_CONVERTERS_H_
+#define FRC971_CONTROL_LOOPS_HYBRID_STATE_FEEDBACK_LOOP_CONVERTERS_H_
+
+#include "frc971/control_loops/hybrid_state_feedback_loop.h"
+#include "frc971/control_loops/state_feedback_loop_converters.h"
+#include "frc971/control_loops/state_feedback_loop_static.h"
+
+namespace frc971::control_loops {
+// These converters are used for constructing a hybrid StateFeedbackLoop
+// object from a flatbuffer.
+// The only method that should typically be called by users is
+// MakeHybridStateFeedbackLoop().
+template <int number_of_states, int number_of_inputs, int number_of_outputs>
+std::unique_ptr<StateFeedbackHybridPlantCoefficients<
+    number_of_states, number_of_inputs, number_of_outputs>>
+MakeStateFeedbackHybridPlantCoefficients(
+    const fbs::StateFeedbackHybridPlantCoefficients &coefficients) {
+  return std::make_unique<StateFeedbackHybridPlantCoefficients<
+      number_of_states, number_of_inputs, number_of_outputs>>(
+      ToEigenOrDie<number_of_states, number_of_states>(
+          *CHECK_NOTNULL(coefficients.a_continuous())),
+      ToEigenOrDie<number_of_states, number_of_inputs>(
+          *CHECK_NOTNULL(coefficients.b_continuous())),
+      ToEigenOrDie<number_of_outputs, number_of_states>(
+          *CHECK_NOTNULL(coefficients.c())),
+      ToEigenOrDie<number_of_outputs, number_of_inputs>(
+          *CHECK_NOTNULL(coefficients.d())),
+      ToEigenOrDie<number_of_inputs, 1>(*CHECK_NOTNULL(coefficients.u_max())),
+      ToEigenOrDie<number_of_inputs, 1>(*CHECK_NOTNULL(coefficients.u_min())),
+      ToEigenOrDie<number_of_inputs, number_of_states>(
+          *CHECK_NOTNULL(coefficients.u_limit_coefficient())),
+      ToEigenOrDie<number_of_inputs, 1>(
+          *CHECK_NOTNULL(coefficients.u_limit_constant())),
+      coefficients.delayed_u());
+}
+
+template <int number_of_states, int number_of_inputs, int number_of_outputs>
+std::unique_ptr<HybridKalmanCoefficients<number_of_states, number_of_inputs,
+                                         number_of_outputs>>
+MakeHybridKalmanCoefficients(
+    const fbs::HybridKalmanCoefficients &coefficients) {
+  return std::make_unique<HybridKalmanCoefficients<
+      number_of_states, number_of_inputs, number_of_outputs>>(
+      ToEigenOrDie<number_of_states, number_of_states>(
+          *CHECK_NOTNULL(coefficients.q_continuous())),
+      ToEigenOrDie<number_of_outputs, number_of_outputs>(
+          *CHECK_NOTNULL(coefficients.r_continuous())),
+      ToEigenOrDie<number_of_states, number_of_states>(
+          *CHECK_NOTNULL(coefficients.p_steady_state())),
+      coefficients.delayed_u());
+}
+template <int number_of_states, int number_of_inputs, int number_of_outputs>
+StateFeedbackLoop<
+    number_of_states, number_of_inputs, number_of_outputs, double,
+    StateFeedbackHybridPlant<number_of_states, number_of_inputs,
+                             number_of_outputs>,
+    HybridKalman<number_of_states, number_of_inputs, number_of_outputs>>
+MakeHybridStateFeedbackLoop(
+    const flatbuffers::Vector<
+        flatbuffers::Offset<fbs::StateFeedbackHybridLoopCoefficients>>
+        &coefficients) {
+  CHECK_LE(1u, coefficients.size());
+  std::vector<std::unique_ptr<StateFeedbackHybridPlantCoefficients<
+      number_of_states, number_of_inputs, number_of_outputs>>>
+      plant_coefficients;
+  std::vector<std::unique_ptr<StateFeedbackControllerCoefficients<
+      number_of_states, number_of_inputs, number_of_outputs>>>
+      controller_coefficients;
+  std::vector<std::unique_ptr<HybridKalmanCoefficients<
+      number_of_states, number_of_inputs, number_of_outputs>>>
+      observer_coefficients;
+  for (const fbs::StateFeedbackHybridLoopCoefficients *loop : coefficients) {
+    CHECK(loop->has_plant());
+    CHECK(loop->has_controller());
+    CHECK(loop->has_observer());
+    plant_coefficients.emplace_back(
+        MakeStateFeedbackHybridPlantCoefficients<
+            number_of_states, number_of_inputs, number_of_outputs>(
+            *loop->plant()));
+    controller_coefficients.emplace_back(
+        MakeStateFeedbackControllerCoefficients<
+            number_of_states, number_of_inputs, number_of_outputs>(
+            *loop->controller()));
+    observer_coefficients.emplace_back(
+        MakeHybridKalmanCoefficients<number_of_states, number_of_inputs,
+                                     number_of_outputs>(*loop->observer()));
+  }
+
+  return StateFeedbackLoop<
+      number_of_states, number_of_inputs, number_of_outputs, double,
+      StateFeedbackHybridPlant<number_of_states, number_of_inputs,
+                               number_of_outputs>,
+      HybridKalman<number_of_states, number_of_inputs, number_of_outputs>>(
+      StateFeedbackHybridPlant<number_of_states, number_of_inputs,
+                               number_of_outputs>(
+          std::move(plant_coefficients)),
+      StateFeedbackController<number_of_states, number_of_inputs,
+                              number_of_outputs>(
+          std::move(controller_coefficients)),
+      HybridKalman<number_of_states, number_of_inputs, number_of_outputs>(
+          std::move(observer_coefficients)));
+}
+}  // namespace frc971::control_loops
+
+#endif  // FRC971_CONTROL_LOOPS_HYBRID_STATE_FEEDBACK_LOOP_CONVERTERS_H_
diff --git a/frc971/control_loops/profiled_subsystem.fbs b/frc971/control_loops/profiled_subsystem.fbs
index 049b52f..d4f64f8 100644
--- a/frc971/control_loops/profiled_subsystem.fbs
+++ b/frc971/control_loops/profiled_subsystem.fbs
@@ -1,4 +1,5 @@
 include "frc971/control_loops/control_loops.fbs";
+include "frc971/control_loops/state_feedback_loop.fbs";
 
 namespace frc971.control_loops;
 
@@ -287,3 +288,15 @@
   // the goal + goal velocity directly to the control loop.
   ignore_profile:bool (id: 3);
 }
+
+// Stores everything but the zeroing information for a single-dof subsystem.
+// Because the subsystem will be templated on the zeroing information, it is
+// passed in separately (see types in //frc971/zeroing:constants.fbs).
+table StaticZeroingSingleDOFProfiledSubsystemCommonParams {
+  zeroing_voltage:double (id: 0);
+  operating_voltage:double (id: 1);
+  zeroing_profile_params:frc971.ProfileParameters (id: 2);
+  default_profile_params:frc971.ProfileParameters (id: 3);
+  range:frc971.Range (id: 4);
+  loop:[frc971.control_loops.fbs.StateFeedbackLoopCoefficients] (id: 5);
+}
diff --git a/frc971/control_loops/python/angular_system.py b/frc971/control_loops/python/angular_system.py
index c6ffb50..551f3d0 100755
--- a/frc971/control_loops/python/angular_system.py
+++ b/frc971/control_loops/python/angular_system.py
@@ -461,7 +461,8 @@
     loop_writer.AddConstant(
         control_loop.Constant('kFreeSpeed', '%f',
                               angular_systems[0].motor.free_speed))
-    loop_writer.Write(plant_files[0], plant_files[1])
+    loop_writer.Write(plant_files[0], plant_files[1],
+                      None if len(plant_files) < 3 else plant_files[2])
 
     integral_loop_writer = control_loop.ControlLoopWriter(
         'Integral' + name,
@@ -469,4 +470,6 @@
         namespaces=year_namespaces,
         plant_type=plant_type,
         observer_type=observer_type)
-    integral_loop_writer.Write(controller_files[0], controller_files[1])
+    integral_loop_writer.Write(
+        controller_files[0], controller_files[1],
+        None if len(controller_files) < 3 else controller_files[2])
diff --git a/frc971/control_loops/python/angular_system_current.py b/frc971/control_loops/python/angular_system_current.py
index 0dc9595..585f54a 100755
--- a/frc971/control_loops/python/angular_system_current.py
+++ b/frc971/control_loops/python/angular_system_current.py
@@ -501,7 +501,8 @@
     loop_writer.AddConstant(
         control_loop.Constant('kFreeSpeed', '%f',
                               angular_systems[0].motor.free_speed))
-    loop_writer.Write(plant_files[0], plant_files[1])
+    loop_writer.Write(plant_files[0], plant_files[1],
+                      None if len(plant_files) < 3 else plant_files[2])
 
     integral_loop_writer = control_loop.ControlLoopWriter(
         'Integral' + name,
@@ -509,4 +510,6 @@
         namespaces=year_namespaces,
         plant_type=plant_type,
         observer_type=observer_type)
-    integral_loop_writer.Write(controller_files[0], controller_files[1])
+    integral_loop_writer.Write(
+        controller_files[0], controller_files[1],
+        None if len(controller_files) < 3 else controller_files[2])
diff --git a/frc971/control_loops/python/control_loop.py b/frc971/control_loops/python/control_loop.py
index e179193..864a4e4 100644
--- a/frc971/control_loops/python/control_loop.py
+++ b/frc971/control_loops/python/control_loop.py
@@ -1,6 +1,7 @@
 import frc971.control_loops.python.controls as controls
 import numpy
 import os
+import json
 
 
 class Constant(object):
@@ -25,6 +26,16 @@
             (self.comment, typestring, self.name, self.value)
 
 
+def MatrixToJson(matrix):
+    """Returns JSON representation of a numpy matrix."""
+    return {
+        "rows": matrix.shape[0],
+        "cols": matrix.shape[1],
+        "storage_order": "ColMajor",
+        "data": numpy.array(matrix).flatten(order='F').tolist()
+    }
+
+
 class ControlLoopWriter(object):
 
     def __init__(self,
@@ -83,10 +94,12 @@
                 os.path.basename(header_file).upper().replace(
                     '.', '_').replace('/', '_') + '_')
 
-    def Write(self, header_file, cc_file):
+    def Write(self, header_file, cc_file, json_file=None):
         """Writes the loops to the specified files."""
         self.WriteHeader(header_file)
         self.WriteCC(os.path.basename(header_file), cc_file)
+        if json_file is not None:
+            self.WriteJson(json_file)
 
     def _GenericType(self, typename, extra_args=None):
         """Returns a loop template using typename for the type."""
@@ -262,6 +275,19 @@
             fd.write(self._namespace_end)
             fd.write('\n')
 
+    def WriteJson(self, json_file):
+        """Writes a JSON file of the loop constants to the specified json_file."""
+        loops = []
+        for loop in self._loops:
+            loop_json = {}
+            loop_json["plant"] = loop.DumpPlantJson(self._PlantCoeffType())
+            loop_json["controller"] = loop.DumpControllerJson()
+            loop_json["observer"] = loop.DumbObserverJson(
+                self._ObserverCoeffType())
+            loops.append(loop_json)
+        with open(json_file, 'w') as f:
+            f.write(json.dumps(loops))
+
 
 class ControlLoop(object):
 
@@ -389,6 +415,27 @@
         return '%s Make%sPlantCoefficients();\n' % (plant_coefficient_type,
                                                     self._name)
 
+    def DumpPlantJson(self, plant_coefficient_type):
+        result = {
+            "c": MatrixToJson(self.C),
+            "d": MatrixToJson(self.D),
+            "u_min": MatrixToJson(self.U_min),
+            "u_max": MatrixToJson(self.U_max),
+            "u_limit_coefficient": MatrixToJson(self.U_limit_coefficient),
+            "u_limit_constant": MatrixToJson(self.U_limit_constant),
+            "delayed_u": self.delayed_u
+        }
+        if plant_coefficient_type.startswith('StateFeedbackPlant'):
+            result["a"] = MatrixToJson(self.A)
+            result["b"] = MatrixToJson(self.B)
+            result["dt"] = int(self.dt * 1e9)
+        elif plant_coefficient_type.startswith('StateFeedbackHybridPlant'):
+            result["a_continuous"] = MatrixToJson(self.A_continuous)
+            result["b_continuous"] = MatrixToJson(self.B_continuous)
+        else:
+            glog.fatal('Unsupported plant type %s', plant_coefficient_type)
+        return result
+
     def DumpPlant(self, plant_coefficient_type, scalar_type):
         """Writes out a c++ function which will create a PlantCoefficients object.
 
@@ -475,6 +522,10 @@
             num_states, num_inputs, num_outputs, scalar_type,
             self.ControllerFunction())
 
+    def DumpControllerJson(self):
+        result = {"k": MatrixToJson(self.K), "kff": MatrixToJson(self.Kff)}
+        return result
+
     def DumpController(self, scalar_type):
         """Returns a c++ function which will create a Controller object.
 
@@ -511,6 +562,32 @@
         return '%s %s;\n' % (observer_coefficient_type,
                              self.ObserverFunction())
 
+    def GetObserverCoefficients(self):
+        if hasattr(self, 'KalmanGain'):
+            KalmanGain = self.KalmanGain
+            Q = self.Q
+            R = self.R
+        else:
+            KalmanGain = numpy.linalg.inv(self.A) * self.L
+            Q = numpy.zeros(self.A.shape)
+            R = numpy.zeros((self.C.shape[0], self.C.shape[0]))
+        return (KalmanGain, Q, R)
+
+    def DumbObserverJson(self, observer_coefficient_type):
+        result = {"delayed_u": self.delayed_u}
+        if observer_coefficient_type.startswith('StateFeedbackObserver'):
+            KalmanGain, Q, R = self.GetObserverCoefficients()
+            result["kalman_gain"] = MatrixToJson(KalmanGain)
+            result["q"] = MatrixToJson(Q)
+            result["r"] = MatrixToJson(R)
+        elif observer_coefficient_type.startswith('HybridKalman'):
+            result["q_continuous"] = MatrixToJson(self.Q_continuous)
+            result["r_continuous"] = MatrixToJson(self.R_continuous)
+            result["p_steady_state"] = MatrixToJson(self.P_steady_state)
+        else:
+            glog.fatal('Unsupported plant type %s', observer_coefficient_type)
+        return result
+
     def DumpObserver(self, observer_coefficient_type, scalar_type):
         """Returns a c++ function which will create a Observer object.
 
@@ -523,14 +600,7 @@
 
         delayed_u_string = str(self.delayed_u)
         if observer_coefficient_type.startswith('StateFeedbackObserver'):
-            if hasattr(self, 'KalmanGain'):
-                KalmanGain = self.KalmanGain
-                Q = self.Q
-                R = self.R
-            else:
-                KalmanGain = numpy.linalg.inv(self.A) * self.L
-                Q = numpy.zeros(self.A.shape)
-                R = numpy.zeros((self.C.shape[0], self.C.shape[0]))
+            KalmanGain, Q, R = self.GetObserverCoefficients()
             ans.append(self._DumpMatrix('KalmanGain', KalmanGain, scalar_type))
             ans.append(self._DumpMatrix('Q', Q, scalar_type))
             ans.append(self._DumpMatrix('R', R, scalar_type))
diff --git a/frc971/control_loops/python/drivetrain.py b/frc971/control_loops/python/drivetrain.py
index 5ff4404..65b30c0 100644
--- a/frc971/control_loops/python/drivetrain.py
+++ b/frc971/control_loops/python/drivetrain.py
@@ -525,7 +525,8 @@
             "kHighOutputRatio", "%f",
             drivetrain_high_high.G_high * drivetrain_high_high.r))
 
-    dog_loop_writer.Write(drivetrain_files[0], drivetrain_files[1])
+    dog_loop_writer.Write(drivetrain_files[0], drivetrain_files[1],
+                          drivetrain_files[2])
 
     kf_loop_writer = control_loop.ControlLoopWriter("KFDrivetrain", [
         kf_drivetrain_low_low, kf_drivetrain_low_high, kf_drivetrain_high_low,
@@ -533,7 +534,8 @@
     ],
                                                     namespaces=namespaces,
                                                     scalar_type=scalar_type)
-    kf_loop_writer.Write(kf_drivetrain_files[0], kf_drivetrain_files[1])
+    kf_loop_writer.Write(kf_drivetrain_files[0], kf_drivetrain_files[1],
+                         kf_drivetrain_files[2])
 
 
 def PlotDrivetrainSprint(drivetrain_params):
diff --git a/frc971/control_loops/python/linear_system.py b/frc971/control_loops/python/linear_system.py
index 3bda4e1..e28c422 100755
--- a/frc971/control_loops/python/linear_system.py
+++ b/frc971/control_loops/python/linear_system.py
@@ -422,8 +422,11 @@
                               linear_systems[0].G * linear_systems[0].radius))
     loop_writer.AddConstant(
         control_loop.Constant('kRadius', '%f', linear_systems[0].radius))
-    loop_writer.Write(plant_files[0], plant_files[1])
+    loop_writer.Write(plant_files[0], plant_files[1],
+                      None if len(plant_files) < 3 else plant_files[2])
 
     integral_loop_writer = control_loop.ControlLoopWriter(
         'Integral' + name, integral_linear_systems, namespaces=year_namespaces)
-    integral_loop_writer.Write(controller_files[0], controller_files[1])
+    integral_loop_writer.Write(
+        controller_files[0], controller_files[1],
+        None if len(controller_files) < 3 else controller_files[2])
diff --git a/frc971/control_loops/python/polydrivetrain.py b/frc971/control_loops/python/polydrivetrain.py
index a45d738..c3d47a6 100644
--- a/frc971/control_loops/python/polydrivetrain.py
+++ b/frc971/control_loops/python/polydrivetrain.py
@@ -490,7 +490,8 @@
                                                      namespaces=namespaces,
                                                      scalar_type=scalar_type)
 
-    dog_loop_writer.Write(drivetrain_files[0], drivetrain_files[1])
+    dog_loop_writer.Write(drivetrain_files[0], drivetrain_files[1],
+                          drivetrain_files[2])
 
     hybrid_loop_writer = control_loop.ControlLoopWriter(
         "HybridVelocityDrivetrain", [
@@ -504,12 +505,12 @@
         plant_type='StateFeedbackHybridPlant',
         observer_type='HybridKalman')
 
-    hybrid_loop_writer.Write(hybrid_files[0], hybrid_files[1])
+    hybrid_loop_writer.Write(hybrid_files[0], hybrid_files[1], hybrid_files[2])
 
     cim_writer = control_loop.ControlLoopWriter("CIM", [CIM()],
                                                 scalar_type=scalar_type)
 
-    cim_writer.Write(motor_files[0], motor_files[1])
+    cim_writer.Write(motor_files[0], motor_files[1], motor_files[2])
 
 
 def PlotPolyDrivetrainMotions(drivetrain_params):
diff --git a/frc971/control_loops/python/static_zeroing_single_dof_profiled_subsystem_test.py b/frc971/control_loops/python/static_zeroing_single_dof_profiled_subsystem_test.py
index 8080aff..92bed42 100644
--- a/frc971/control_loops/python/static_zeroing_single_dof_profiled_subsystem_test.py
+++ b/frc971/control_loops/python/static_zeroing_single_dof_profiled_subsystem_test.py
@@ -38,13 +38,14 @@
         linear_system.PlotMotion(kIntake, 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 \
+    if len(argv) != 7:
+        glog.fatal(
+            'Expected .h, .cc, and .json filenames and .json file name for the \
             static_zeroing_single_dof_profiled_subsystem_test and integral \
             static_zeroing_single_dof_profiled_subsystem_test.')
     else:
         namespaces = ['frc971', 'control_loops']
-        linear_system.WriteLinearSystem(kIntake, argv[1:3], argv[3:5],
+        linear_system.WriteLinearSystem(kIntake, argv[1:4], argv[4:7],
                                         namespaces)
 
 
diff --git a/frc971/control_loops/python/test_drivetrain/BUILD b/frc971/control_loops/python/test_drivetrain/BUILD
new file mode 100644
index 0000000..63cc2a9
--- /dev/null
+++ b/frc971/control_loops/python/test_drivetrain/BUILD
@@ -0,0 +1,117 @@
+py_binary(
+    name = "drivetrain",
+    srcs = [
+        "drivetrain.py",
+    ],
+    legacy_create_init = False,
+    target_compatible_with = ["@platforms//cpu:x86_64"],
+    deps = [
+        ":python_init",
+        "//frc971/control_loops/python:drivetrain",
+        "@pip//glog",
+        "@pip//python_gflags",
+    ],
+)
+
+py_binary(
+    name = "polydrivetrain",
+    srcs = [
+        "drivetrain.py",
+        "polydrivetrain.py",
+    ],
+    legacy_create_init = False,
+    target_compatible_with = ["@platforms//cpu:x86_64"],
+    deps = [
+        ":python_init",
+        "//frc971/control_loops/python:polydrivetrain",
+        "@pip//glog",
+        "@pip//python_gflags",
+    ],
+)
+
+genrule(
+    name = "genrule_drivetrain",
+    outs = [
+        "drivetrain_dog_motor_plant.h",
+        "drivetrain_dog_motor_plant.cc",
+        "drivetrain_dog_motor_plant.json",
+        "kalman_drivetrain_motor_plant.h",
+        "kalman_drivetrain_motor_plant.cc",
+        "kalman_drivetrain_motor_plant.json",
+    ],
+    cmd = "$(location :drivetrain) $(OUTS)",
+    target_compatible_with = ["@platforms//os:linux"],
+    tools = [
+        ":drivetrain",
+    ],
+)
+
+genrule(
+    name = "genrule_polydrivetrain",
+    outs = [
+        "polydrivetrain_dog_motor_plant.h",
+        "polydrivetrain_dog_motor_plant.cc",
+        "polydrivetrain_dog_motor_plant.json",
+        "polydrivetrain_cim_plant.h",
+        "polydrivetrain_cim_plant.cc",
+        "polydrivetrain_cim_plant.json",
+        "hybrid_velocity_drivetrain.h",
+        "hybrid_velocity_drivetrain.cc",
+        "hybrid_velocity_drivetrain.json",
+    ],
+    cmd = "$(location :polydrivetrain) $(OUTS)",
+    target_compatible_with = ["@platforms//os:linux"],
+    tools = [
+        ":polydrivetrain",
+    ],
+)
+
+cc_library(
+    name = "polydrivetrain_plants",
+    srcs = [
+        "drivetrain_dog_motor_plant.cc",
+        "hybrid_velocity_drivetrain.cc",
+        "kalman_drivetrain_motor_plant.cc",
+        "polydrivetrain_dog_motor_plant.cc",
+    ],
+    hdrs = [
+        "drivetrain_dog_motor_plant.h",
+        "hybrid_velocity_drivetrain.h",
+        "kalman_drivetrain_motor_plant.h",
+        "polydrivetrain_dog_motor_plant.h",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//frc971/control_loops:hybrid_state_feedback_loop",
+        "//frc971/control_loops:state_feedback_loop",
+    ],
+)
+
+cc_test(
+    name = "drivetrain_json_test",
+    srcs = [
+        "drivetrain_json_test.cc",
+    ],
+    data = [
+        "drivetrain_dog_motor_plant.json",
+        "hybrid_velocity_drivetrain.json",
+        "kalman_drivetrain_motor_plant.json",
+        "polydrivetrain_cim_plant.json",
+        "polydrivetrain_dog_motor_plant.json",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = [
+        ":polydrivetrain_plants",
+        "//aos/testing:googletest",
+        "//frc971/control_loops:hybrid_state_feedback_loop_converters",
+    ],
+)
+
+py_library(
+    name = "python_init",
+    srcs = ["__init__.py"],
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//visibility:public"],
+    deps = ["//y2018/control_loops:python_init"],
+)
diff --git a/frc971/control_loops/python/test_drivetrain/__init__.py b/frc971/control_loops/python/test_drivetrain/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/frc971/control_loops/python/test_drivetrain/__init__.py
diff --git a/frc971/control_loops/python/test_drivetrain/drivetrain.py b/frc971/control_loops/python/test_drivetrain/drivetrain.py
new file mode 100644
index 0000000..0291487
--- /dev/null
+++ b/frc971/control_loops/python/test_drivetrain/drivetrain.py
@@ -0,0 +1,52 @@
+#!/usr/bin/python3
+
+from __future__ import print_function
+from frc971.control_loops.python import drivetrain
+import sys
+
+import gflags
+import glog
+
+FLAGS = gflags.FLAGS
+
+gflags.DEFINE_bool('plot', False, 'If true, plot the loop response.')
+
+# Values stolen from y2018 (most recent year with shifters).
+kDrivetrain = drivetrain.DrivetrainParams(
+    J=6.0,
+    mass=68.0,
+    robot_radius=0.616 / 2.0,
+    wheel_radius=0.127 / 2.0 * 120.0 / 118.0,
+    G_low=46.0 / 60.0 * 20.0 / 48.0 * 14.0 / 62.0,
+    G_high=62.0 / 44.0 * 20.0 / 48.0 * 14.0 / 62.0,
+    q_pos_low=0.12,
+    q_pos_high=0.14,
+    q_vel_low=1.0,
+    q_vel_high=0.95,
+    efficiency=0.70,
+    has_imu=True,
+    force=True,
+    kf_q_voltage=13.0,
+    controller_poles=[0.82, 0.82],
+    robot_cg_offset=0.0,
+)
+
+
+def main(argv):
+    argv = FLAGS(argv)
+    glog.init()
+
+    if FLAGS.plot:
+        drivetrain.PlotDrivetrainMotions(kDrivetrain)
+    elif len(argv) != 7:
+        print("Expected .h, .cc, and .json file names.")
+    else:
+        # Write the generated constants out to a file.
+        drivetrain.WriteDrivetrain(
+            argv[1:4], argv[4:7],
+            ['frc971', 'control_loops', 'python', 'test_drivetrain'],
+            kDrivetrain)
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv))
diff --git a/frc971/control_loops/python/test_drivetrain/drivetrain_json_test.cc b/frc971/control_loops/python/test_drivetrain/drivetrain_json_test.cc
new file mode 100644
index 0000000..b280e99
--- /dev/null
+++ b/frc971/control_loops/python/test_drivetrain/drivetrain_json_test.cc
@@ -0,0 +1,121 @@
+#include "gtest/gtest.h"
+
+#include "aos/util/file.h"
+#include "frc971/control_loops/hybrid_state_feedback_loop_converters.h"
+#include "frc971/control_loops/python/test_drivetrain/drivetrain_dog_motor_plant.h"
+#include "frc971/control_loops/python/test_drivetrain/hybrid_velocity_drivetrain.h"
+#include "frc971/control_loops/python/test_drivetrain/kalman_drivetrain_motor_plant.h"
+#include "frc971/control_loops/python/test_drivetrain/polydrivetrain_dog_motor_plant.h"
+
+namespace frc971::control_loops::drivetrain::testing {
+
+class DrivetrainJsonTest : public ::testing::Test {
+ protected:
+  DrivetrainJsonTest() {}
+
+  aos::FlatbufferDetachedBuffer<fbs::StateFeedbackLoopCoefficientsVector>
+  ReadCoefficients(std::string_view file) {
+    const std::string json =
+        "{\"loops\": " + aos::util::ReadFileToStringOrDie(file) + "}";
+    return aos::JsonToFlatbuffer<fbs::StateFeedbackLoopCoefficientsVector>(
+        json);
+  }
+  aos::FlatbufferDetachedBuffer<fbs::StateFeedbackHybridLoopCoefficientsVector>
+  ReadHybridCoefficients(std::string_view file) {
+    const std::string json =
+        "{\"loops\": " + aos::util::ReadFileToStringOrDie(file) + "}";
+    return aos::JsonToFlatbuffer<
+        fbs::StateFeedbackHybridLoopCoefficientsVector>(json);
+  }
+};
+
+TEST_F(DrivetrainJsonTest, DrivetrainLoop) {
+  StateFeedbackLoop<4, 2, 2> made_loop =
+      python::test_drivetrain::MakeDrivetrainLoop();
+  auto coefs = ReadCoefficients(
+      "frc971/control_loops/python/test_drivetrain/"
+      "drivetrain_dog_motor_plant.json");
+
+  StateFeedbackLoop<4, 2, 2> json_loop =
+      MakeStateFeedbackLoop<4, 2, 2>(*CHECK_NOTNULL(coefs.message().loops()));
+  for (size_t index = 0; index < 4; ++index) {
+    ASSERT_TRUE(coefs.span().size() > 0u);
+    made_loop.set_index(index);
+    json_loop.set_index(index);
+#define COMPARE(matrix)                              \
+  EXPECT_EQ(json_loop.plant().coefficients().matrix, \
+            made_loop.plant().coefficients().matrix);
+    COMPARE(A);
+    COMPARE(B);
+    COMPARE(C);
+    COMPARE(D);
+    COMPARE(U_min);
+    COMPARE(U_max);
+    COMPARE(U_limit_coefficient);
+    COMPARE(U_limit_constant);
+    COMPARE(dt);
+    COMPARE(delayed_u);
+#undef COMPARE
+#define COMPARE(matrix)                                   \
+  EXPECT_EQ(json_loop.controller().coefficients().matrix, \
+            made_loop.controller().coefficients().matrix);
+    COMPARE(K);
+    COMPARE(Kff);
+#undef COMPARE
+#define COMPARE(matrix)                                 \
+  EXPECT_EQ(json_loop.observer().coefficients().matrix, \
+            made_loop.observer().coefficients().matrix);
+    COMPARE(KalmanGain);
+    COMPARE(Q);
+    COMPARE(R);
+    COMPARE(delayed_u);
+#undef COMPARE
+  }
+}
+
+typedef StateFeedbackLoop<2, 2, 2, double, StateFeedbackHybridPlant<2, 2, 2>,
+                          HybridKalman<2, 2, 2>>
+    HybridLoop;
+TEST_F(DrivetrainJsonTest, HybridLoop) {
+  HybridLoop made_loop =
+      python::test_drivetrain::MakeHybridVelocityDrivetrainLoop();
+  auto coefs = ReadHybridCoefficients(
+      "frc971/control_loops/python/test_drivetrain/"
+      "hybrid_velocity_drivetrain.json");
+
+  HybridLoop json_loop = MakeHybridStateFeedbackLoop<2, 2, 2>(
+      *CHECK_NOTNULL(coefs.message().loops()));
+  for (size_t index = 0; index < 4; ++index) {
+    ASSERT_TRUE(coefs.span().size() > 0u);
+    made_loop.set_index(index);
+    json_loop.set_index(index);
+#define COMPARE(matrix)                              \
+  EXPECT_EQ(json_loop.plant().coefficients().matrix, \
+            made_loop.plant().coefficients().matrix);
+    COMPARE(A_continuous);
+    COMPARE(B_continuous);
+    COMPARE(C);
+    COMPARE(D);
+    COMPARE(U_min);
+    COMPARE(U_max);
+    COMPARE(U_limit_coefficient);
+    COMPARE(U_limit_constant);
+    COMPARE(delayed_u);
+#undef COMPARE
+#define COMPARE(matrix)                                   \
+  EXPECT_EQ(json_loop.controller().coefficients().matrix, \
+            made_loop.controller().coefficients().matrix);
+    COMPARE(K);
+    COMPARE(Kff);
+#undef COMPARE
+#define COMPARE(matrix)                                 \
+  EXPECT_EQ(json_loop.observer().coefficients().matrix, \
+            made_loop.observer().coefficients().matrix);
+    COMPARE(Q_continuous);
+    COMPARE(R_continuous);
+    COMPARE(P_steady_state);
+    COMPARE(delayed_u);
+#undef COMPARE
+  }
+}
+}  // namespace frc971::control_loops::drivetrain::testing
diff --git a/frc971/control_loops/python/test_drivetrain/polydrivetrain.py b/frc971/control_loops/python/test_drivetrain/polydrivetrain.py
new file mode 100644
index 0000000..70ccb34
--- /dev/null
+++ b/frc971/control_loops/python/test_drivetrain/polydrivetrain.py
@@ -0,0 +1,35 @@
+#!/usr/bin/python3
+
+import sys
+from frc971.control_loops.python.test_drivetrain import drivetrain
+from frc971.control_loops.python import polydrivetrain
+
+import gflags
+import glog
+
+__author__ = 'Austin Schuh (austin.linux@gmail.com)'
+
+FLAGS = gflags.FLAGS
+
+try:
+    gflags.DEFINE_bool('plot', False, 'If true, plot the loop response.')
+except gflags.DuplicateFlagError:
+    pass
+
+
+def main(argv):
+    if FLAGS.plot:
+        polydrivetrain.PlotPolyDrivetrainMotions(drivetrain.kDrivetrain)
+    elif len(argv) != 10:
+        glog.fatal('Expected .h, .cc, and .json filenames')
+    else:
+        polydrivetrain.WritePolyDrivetrain(
+            argv[1:4], argv[4:7], argv[7:10],
+            ['frc971', 'control_loops', 'python', 'test_drivetrain'],
+            drivetrain.kDrivetrain)
+
+
+if __name__ == '__main__':
+    argv = FLAGS(sys.argv)
+    glog.init()
+    sys.exit(main(argv))
diff --git a/frc971/control_loops/state_feedback_loop.fbs b/frc971/control_loops/state_feedback_loop.fbs
new file mode 100644
index 0000000..7c9bff8
--- /dev/null
+++ b/frc971/control_loops/state_feedback_loop.fbs
@@ -0,0 +1,76 @@
+include "frc971/math/matrix.fbs";
+
+namespace frc971.control_loops.fbs;
+
+// This file contains tables that represent the constants required
+// to initialize a control loop.
+// See frc971/control_loops/state_feedback_loop.h
+
+table StateFeedbackPlantCoefficients {
+  a:frc971.fbs.Matrix (id: 0);
+  b:frc971.fbs.Matrix (id: 1);
+  c:frc971.fbs.Matrix (id: 2);
+  d:frc971.fbs.Matrix (id: 3);
+  u_min:frc971.fbs.Matrix (id: 4);
+  u_max:frc971.fbs.Matrix (id: 5);
+  u_limit_coefficient:frc971.fbs.Matrix (id: 6);
+  u_limit_constant:frc971.fbs.Matrix (id: 7);
+  dt:uint64 (id: 8);
+  delayed_u:uint64 (id: 9);
+}
+
+table StateFeedbackHybridPlantCoefficients {
+  a_continuous:frc971.fbs.Matrix (id: 0);
+  b_continuous:frc971.fbs.Matrix (id: 1);
+  c:frc971.fbs.Matrix (id: 2);
+  d:frc971.fbs.Matrix (id: 3);
+  u_min:frc971.fbs.Matrix (id: 4);
+  u_max:frc971.fbs.Matrix (id: 5);
+  u_limit_coefficient:frc971.fbs.Matrix (id: 6);
+  u_limit_constant:frc971.fbs.Matrix (id: 7);
+  delayed_u:uint64 (id: 8);
+}
+
+table StateFeedbackControllerCoefficients {
+  k:frc971.fbs.Matrix (id: 0);
+  kff:frc971.fbs.Matrix (id: 1);
+}
+
+table StateFeedbackObserverCoefficients {
+  kalman_gain:frc971.fbs.Matrix (id: 0);
+  q:frc971.fbs.Matrix (id: 1);
+  r:frc971.fbs.Matrix (id: 2);
+  delayed_u:uint64 (id: 3);
+}
+
+table HybridKalmanCoefficients {
+  q_continuous:frc971.fbs.Matrix (id: 0);
+  r_continuous:frc971.fbs.Matrix (id: 1);
+  p_steady_state:frc971.fbs.Matrix (id: 2);
+  delayed_u:uint64 (id: 3);
+}
+
+// The tables below represent a single index within a
+// *Loop object. A vector of these will be necessary to initialize
+// the loop (the vector will be of length one except when using a
+// gain-scheduled controller).
+table StateFeedbackLoopCoefficients {
+  plant:StateFeedbackPlantCoefficients (id: 0);
+  controller:StateFeedbackControllerCoefficients (id: 1);
+  observer:StateFeedbackObserverCoefficients (id: 2);
+}
+
+table StateFeedbackHybridLoopCoefficients {
+  plant:StateFeedbackHybridPlantCoefficients (id: 0);
+  controller:StateFeedbackControllerCoefficients (id: 1);
+  observer:HybridKalmanCoefficients (id: 2);
+}
+
+// Helpers for when we need to be able to package just the loop coefficients.
+table StateFeedbackLoopCoefficientsVector {
+  loops:[StateFeedbackLoopCoefficients] (id: 0);
+}
+
+table StateFeedbackHybridLoopCoefficientsVector {
+  loops:[StateFeedbackHybridLoopCoefficients] (id: 0);
+}
diff --git a/frc971/control_loops/state_feedback_loop.h b/frc971/control_loops/state_feedback_loop.h
index c5d05c1..3c90434 100644
--- a/frc971/control_loops/state_feedback_loop.h
+++ b/frc971/control_loops/state_feedback_loop.h
@@ -463,7 +463,8 @@
 
  private:
   // Internal state estimate.
-  Eigen::Matrix<Scalar, number_of_states, 1> X_hat_;
+  Eigen::Matrix<Scalar, number_of_states, 1> X_hat_ =
+      Eigen::Matrix<Scalar, number_of_states, 1>::Zero();
   Eigen::Matrix<Scalar, number_of_inputs, Eigen::Dynamic> last_U_;
 
   int index_ = 0;
diff --git a/frc971/control_loops/state_feedback_loop_converters.h b/frc971/control_loops/state_feedback_loop_converters.h
new file mode 100644
index 0000000..062257d
--- /dev/null
+++ b/frc971/control_loops/state_feedback_loop_converters.h
@@ -0,0 +1,113 @@
+#ifndef FRC971_CONTROL_LOOPS_STATE_FEEDBACK_LOOP_CONVERTERS_H_
+#define FRC971_CONTROL_LOOPS_STATE_FEEDBACK_LOOP_CONVERTERS_H_
+
+#include "frc971/control_loops/state_feedback_loop.h"
+#include "frc971/control_loops/state_feedback_loop_static.h"
+#include "frc971/math/flatbuffers_matrix.h"
+
+namespace frc971::control_loops {
+// These converters are used for constructing a "default" StateFeedbackLoop
+// object from a flatbuffer.
+// The only method that should typically be called by users is
+// MakeStateFeedbackLoop().
+// Use hybrid_state_feedback_loop_converters.h for control loops that use the
+// hybrid kalman filters.
+template <int number_of_states, int number_of_inputs, int number_of_outputs>
+std::unique_ptr<StateFeedbackPlantCoefficients<
+    number_of_states, number_of_inputs, number_of_outputs>>
+MakeStateFeedbackPlantCoefficients(
+    const fbs::StateFeedbackPlantCoefficients &coefficients) {
+  return std::make_unique<StateFeedbackPlantCoefficients<
+      number_of_states, number_of_inputs, number_of_outputs>>(
+      ToEigenOrDie<number_of_states, number_of_states>(
+          *CHECK_NOTNULL(coefficients.a())),
+      ToEigenOrDie<number_of_states, number_of_inputs>(
+          *CHECK_NOTNULL(coefficients.b())),
+      ToEigenOrDie<number_of_outputs, number_of_states>(
+          *CHECK_NOTNULL(coefficients.c())),
+      ToEigenOrDie<number_of_outputs, number_of_inputs>(
+          *CHECK_NOTNULL(coefficients.d())),
+      ToEigenOrDie<number_of_inputs, 1>(*CHECK_NOTNULL(coefficients.u_max())),
+      ToEigenOrDie<number_of_inputs, 1>(*CHECK_NOTNULL(coefficients.u_min())),
+      ToEigenOrDie<number_of_inputs, number_of_states>(
+          *CHECK_NOTNULL(coefficients.u_limit_coefficient())),
+      ToEigenOrDie<number_of_inputs, 1>(
+          *CHECK_NOTNULL(coefficients.u_limit_constant())),
+      std::chrono::nanoseconds(coefficients.dt()), coefficients.delayed_u());
+}
+
+template <int number_of_states, int number_of_inputs, int number_of_outputs>
+std::unique_ptr<StateFeedbackControllerCoefficients<
+    number_of_states, number_of_inputs, number_of_outputs>>
+MakeStateFeedbackControllerCoefficients(
+    const fbs::StateFeedbackControllerCoefficients &coefficients) {
+  return std::make_unique<StateFeedbackControllerCoefficients<
+      number_of_states, number_of_inputs, number_of_outputs>>(
+      ToEigenOrDie<number_of_inputs, number_of_states>(
+          *CHECK_NOTNULL(coefficients.k())),
+      ToEigenOrDie<number_of_inputs, number_of_states>(
+          *CHECK_NOTNULL(coefficients.kff())));
+}
+
+template <int number_of_states, int number_of_inputs, int number_of_outputs>
+std::unique_ptr<StateFeedbackObserverCoefficients<
+    number_of_states, number_of_inputs, number_of_outputs>>
+MakeStateFeedbackObserverCoefficients(
+    const fbs::StateFeedbackObserverCoefficients &coefficients) {
+  return std::make_unique<StateFeedbackObserverCoefficients<
+      number_of_states, number_of_inputs, number_of_outputs>>(
+      ToEigenOrDie<number_of_states, number_of_outputs>(
+          *CHECK_NOTNULL(coefficients.kalman_gain())),
+      ToEigenOrDie<number_of_states, number_of_states>(
+          *CHECK_NOTNULL(coefficients.q())),
+      ToEigenOrDie<number_of_outputs, number_of_outputs>(
+          *CHECK_NOTNULL(coefficients.r())),
+      coefficients.delayed_u());
+}
+
+template <int number_of_states, int number_of_inputs, int number_of_outputs>
+StateFeedbackLoop<number_of_states, number_of_inputs, number_of_outputs>
+MakeStateFeedbackLoop(const flatbuffers::Vector<flatbuffers::Offset<
+                          fbs::StateFeedbackLoopCoefficients>> &coefficients) {
+  CHECK_LE(1u, coefficients.size());
+  std::vector<std::unique_ptr<StateFeedbackPlantCoefficients<
+      number_of_states, number_of_inputs, number_of_outputs>>>
+      plant_coefficients;
+  std::vector<std::unique_ptr<StateFeedbackControllerCoefficients<
+      number_of_states, number_of_inputs, number_of_outputs>>>
+      controller_coefficients;
+  std::vector<std::unique_ptr<StateFeedbackObserverCoefficients<
+      number_of_states, number_of_inputs, number_of_outputs>>>
+      observer_coefficients;
+  for (const fbs::StateFeedbackLoopCoefficients *loop : coefficients) {
+    CHECK(loop->has_plant());
+    CHECK(loop->has_controller());
+    CHECK(loop->has_observer());
+    plant_coefficients.emplace_back(
+        MakeStateFeedbackPlantCoefficients<number_of_states, number_of_inputs,
+                                           number_of_outputs>(*loop->plant()));
+    controller_coefficients.emplace_back(
+        MakeStateFeedbackControllerCoefficients<
+            number_of_states, number_of_inputs, number_of_outputs>(
+            *loop->controller()));
+    observer_coefficients.emplace_back(
+        MakeStateFeedbackObserverCoefficients<
+            number_of_states, number_of_inputs, number_of_outputs>(
+            *loop->observer()));
+  }
+
+  return StateFeedbackLoop<number_of_states, number_of_inputs,
+                           number_of_outputs>(
+      StateFeedbackPlant<number_of_states, number_of_inputs, number_of_outputs>(
+          std::move(plant_coefficients)),
+      StateFeedbackController<number_of_states, number_of_inputs,
+                              number_of_outputs>(
+          std::move(controller_coefficients)),
+      StateFeedbackObserver<number_of_states, number_of_inputs,
+                            number_of_outputs>(
+          std::move(observer_coefficients)));
+}
+
+}  // namespace frc971::control_loops
+
+#endif  // FRC971_CONTROL_LOOPS_STATE_FEEDBACK_LOOP_CONVERTERS_H_
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 5c94bd5..b26f69c 100644
--- a/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem.h
+++ b/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem.h
@@ -1,7 +1,9 @@
 #ifndef FRC971_CONTROL_LOOPS_STATIC_ZEROING_SINGLE_DOF_PROFILED_SUBSYSTEM_H_
 #define FRC971_CONTROL_LOOPS_STATIC_ZEROING_SINGLE_DOF_PROFILED_SUBSYSTEM_H_
 
+#include "aos/flatbuffer_merge.h"
 #include "frc971/control_loops/profiled_subsystem.h"
+#include "frc971/control_loops/state_feedback_loop_converters.h"
 
 namespace frc971 {
 namespace control_loops {
@@ -28,7 +30,52 @@
   typename ZeroingEstimator::ZeroingConstants zeroing_constants;
 
   // Function that makes the integral loop for the subsystem
-  ::std::function<StateFeedbackLoop<3, 1, 1>()> make_integral_loop;
+  std::function<StateFeedbackLoop<3, 1, 1>()> make_integral_loop;
+
+  // Used by make_integral_loop when constructed from a flatbuffer.
+  std::shared_ptr<aos::FlatbufferDetachedBuffer<
+      StaticZeroingSingleDOFProfiledSubsystemCommonParams>>
+      loop_params;
+
+  StaticZeroingSingleDOFProfiledSubsystemParams(
+      double zeroing_voltage_in, double operating_voltage_in,
+      const ProfileParametersT &zeroing_profile_params_in,
+      const ProfileParametersT &default_profile_params_in,
+      const ::frc971::constants::Range &range_in,
+      const typename ZeroingEstimator::ZeroingConstants &zeroing_constants_in,
+      std::function<StateFeedbackLoop<3, 1, 1>()> make_integral_loop_in)
+      : zeroing_voltage(zeroing_voltage_in),
+        operating_voltage(operating_voltage_in),
+        zeroing_profile_params(zeroing_profile_params_in),
+        default_profile_params(default_profile_params_in),
+        range(range_in),
+        zeroing_constants(zeroing_constants_in),
+        make_integral_loop(make_integral_loop_in) {}
+
+  // Constructs the parameters from flatbuffer types.
+  StaticZeroingSingleDOFProfiledSubsystemParams(
+      const StaticZeroingSingleDOFProfiledSubsystemCommonParams *common,
+      const ZeroingEstimator::ZeroingConstants::TableType *zeroing)
+      : zeroing_voltage(common->zeroing_voltage()),
+        operating_voltage(common->operating_voltage()),
+        zeroing_profile_params(
+            aos::UnpackFlatbuffer(common->zeroing_profile_params())),
+        default_profile_params(
+            aos::UnpackFlatbuffer(common->default_profile_params())),
+        range(frc971::constants::Range::FromFlatbuffer(common->range())),
+        zeroing_constants(aos::UnpackFlatbuffer(zeroing)),
+        make_integral_loop([this]() {
+          return MakeStateFeedbackLoop<3, 1, 1>(
+              *CHECK_NOTNULL(loop_params->message().loop()));
+        }),
+        loop_params(std::make_shared<aos::FlatbufferDetachedBuffer<
+                        StaticZeroingSingleDOFProfiledSubsystemCommonParams>>(
+            aos::RecursiveCopyFlatBuffer(common))) {}
+  StaticZeroingSingleDOFProfiledSubsystemParams() = default;
+  StaticZeroingSingleDOFProfiledSubsystemParams(
+      const StaticZeroingSingleDOFProfiledSubsystemParams &) = default;
+  StaticZeroingSingleDOFProfiledSubsystemParams &operator=(
+      const StaticZeroingSingleDOFProfiledSubsystemParams &) = default;
 };
 
 // Class for controlling and motion profiling a single degree of freedom
@@ -37,9 +84,20 @@
           typename TSubsystemParams = TZeroingEstimator>
 class StaticZeroingSingleDOFProfiledSubsystem {
  public:
+  // Constructs the subsystem from flatbuffer types (appropriate when using the
+  // constants.h for the subsystem; the constants.json should be preferred for
+  // new subsystems).
   StaticZeroingSingleDOFProfiledSubsystem(
       const StaticZeroingSingleDOFProfiledSubsystemParams<TSubsystemParams>
           &params);
+  // Constructs the subsystem from flatbuffer types (appropriate when using a
+  // constants.json for the subsystem).
+  StaticZeroingSingleDOFProfiledSubsystem(
+      const StaticZeroingSingleDOFProfiledSubsystemCommonParams *common,
+      const TZeroingEstimator::ZeroingConstants::TableType *zeroing)
+      : StaticZeroingSingleDOFProfiledSubsystem(
+            StaticZeroingSingleDOFProfiledSubsystemParams<TSubsystemParams>{
+                common, zeroing}) {}
 
   using ZeroingEstimator = TZeroingEstimator;
   using ProfiledJointStatus = TProfiledJointStatus;
@@ -58,8 +116,8 @@
 
   void set_max_position(double max_position) { max_position_ = max_position; }
 
-  // Sets a temporary acceleration limit.  No accelerations faster than this may
-  // be commanded.
+  // Sets a temporary acceleration limit.  No accelerations faster than this
+  // may be commanded.
   void set_max_acceleration(double max_acceleration) {
     max_acceleration_ = max_acceleration;
   }
@@ -111,9 +169,9 @@
       const typename ZeroingEstimator::Position *position, double *output,
       flatbuffers::FlatBufferBuilder *status_fbb);
 
-  // Sets the current profile state to solve from.  Useful for when an external
-  // controller gives back control and we want the trajectory generator to
-  // take over control again.
+  // Sets the current profile state to solve from.  Useful for when an
+  // external controller gives back control and we want the trajectory
+  // generator to take over control again.
   void ForceGoal(double goal, double goal_velocity);
 
   // Resets the profiled subsystem and returns to uninitialized
diff --git a/frc971/math/BUILD b/frc971/math/BUILD
index 54df5bd..f33b9f9 100644
--- a/frc971/math/BUILD
+++ b/frc971/math/BUILD
@@ -1,4 +1,5 @@
 load("//aos/flatbuffers:generate.bzl", "static_flatbuffer")
+load("@com_github_google_flatbuffers//:typescript.bzl", "flatbuffer_ts_library")
 
 static_flatbuffer(
     name = "matrix_fbs",
@@ -6,6 +7,14 @@
     visibility = ["//visibility:public"],
 )
 
+flatbuffer_ts_library(
+    name = "matrix_ts_fbs",
+    srcs = [
+        "matrix.fbs",
+    ],
+    visibility = ["//visibility:public"],
+)
+
 cc_library(
     name = "flatbuffers_matrix",
     hdrs = ["flatbuffers_matrix.h"],
diff --git a/motors/seems_reasonable/BUILD b/motors/seems_reasonable/BUILD
index b899ffb..63fea8a 100644
--- a/motors/seems_reasonable/BUILD
+++ b/motors/seems_reasonable/BUILD
@@ -34,8 +34,10 @@
     outs = [
         "drivetrain_dog_motor_plant.h",
         "drivetrain_dog_motor_plant.cc",
+        "drivetrain_dog_motor_plant.json",
         "kalman_drivetrain_motor_plant.h",
         "kalman_drivetrain_motor_plant.cc",
+        "kalman_drivetrain_motor_plant.json",
     ],
     cmd = "$(location :drivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:none"],
@@ -49,10 +51,13 @@
     outs = [
         "polydrivetrain_dog_motor_plant.h",
         "polydrivetrain_dog_motor_plant.cc",
+        "polydrivetrain_dog_motor_plant.json",
         "polydrivetrain_cim_plant.h",
         "polydrivetrain_cim_plant.cc",
+        "polydrivetrain_cim_plant.json",
         "hybrid_velocity_drivetrain.h",
         "hybrid_velocity_drivetrain.cc",
+        "hybrid_velocity_drivetrain.json",
     ],
     cmd = "$(location :polydrivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:none"],
diff --git a/motors/seems_reasonable/drivetrain.py b/motors/seems_reasonable/drivetrain.py
index ad3d92a..1eeeb42 100644
--- a/motors/seems_reasonable/drivetrain.py
+++ b/motors/seems_reasonable/drivetrain.py
@@ -26,12 +26,12 @@
     argv = FLAGS(argv)
     glog.init()
 
-    if len(argv) != 5:
-        glog.error("Expected .h file name and .cc file name")
+    if len(argv) != 7:
+        glog.error("Expected .h, .cc, and .json filenames")
     else:
         # Write the generated constants out to a file.
-        drivetrain.WriteDrivetrain(argv[1:3],
-                                   argv[3:5], ['motors', 'seems_reasonable'],
+        drivetrain.WriteDrivetrain(argv[1:4],
+                                   argv[4:7], ['motors', 'seems_reasonable'],
                                    kDrivetrain,
                                    scalar_type='float')
 
diff --git a/motors/seems_reasonable/polydrivetrain.py b/motors/seems_reasonable/polydrivetrain.py
index 665739f..505ed38 100644
--- a/motors/seems_reasonable/polydrivetrain.py
+++ b/motors/seems_reasonable/polydrivetrain.py
@@ -20,12 +20,12 @@
 def main(argv):
     if FLAGS.plot:
         polydrivetrain.PlotPolyDrivetrainMotions(drivetrain.kDrivetrain)
-    elif len(argv) != 7:
-        glog.fatal('Expected .h file name and .cc file name')
+    elif len(argv) != 10:
+        glog.fatal('Expected .h, .cc, and .json filenames')
     else:
-        polydrivetrain.WritePolyDrivetrain(argv[1:3],
-                                           argv[3:5],
-                                           argv[5:7],
+        polydrivetrain.WritePolyDrivetrain(argv[1:4],
+                                           argv[4:7],
+                                           argv[7:10],
                                            ['motors', 'seems_reasonable'],
                                            drivetrain.kDrivetrain,
                                            scalar_type='float')
diff --git a/y2014/control_loops/drivetrain/BUILD b/y2014/control_loops/drivetrain/BUILD
index f331b37..b185b4b 100644
--- a/y2014/control_loops/drivetrain/BUILD
+++ b/y2014/control_loops/drivetrain/BUILD
@@ -5,8 +5,10 @@
     outs = [
         "drivetrain_dog_motor_plant.h",
         "drivetrain_dog_motor_plant.cc",
+        "drivetrain_dog_motor_plant.json",
         "kalman_drivetrain_motor_plant.h",
         "kalman_drivetrain_motor_plant.cc",
+        "kalman_drivetrain_motor_plant.json",
     ],
     cmd = "$(location //y2014/control_loops/python:drivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
@@ -21,10 +23,13 @@
     outs = [
         "polydrivetrain_dog_motor_plant.h",
         "polydrivetrain_dog_motor_plant.cc",
+        "polydrivetrain_dog_motor_plant.json",
         "polydrivetrain_cim_plant.h",
         "polydrivetrain_cim_plant.cc",
+        "polydrivetrain_cim_plant.json",
         "hybrid_velocity_drivetrain.h",
         "hybrid_velocity_drivetrain.cc",
+        "hybrid_velocity_drivetrain.json",
     ],
     cmd = "$(location //y2014/control_loops/python:polydrivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
diff --git a/y2014/control_loops/python/drivetrain.py b/y2014/control_loops/python/drivetrain.py
index d72c222..41fbd27 100755
--- a/y2014/control_loops/python/drivetrain.py
+++ b/y2014/control_loops/python/drivetrain.py
@@ -31,11 +31,11 @@
 
     if FLAGS.plot:
         drivetrain.PlotDrivetrainMotions(kDrivetrain)
-    elif len(argv) != 5:
-        print("Expected .h file name and .cc file name")
+    elif len(argv) != 7:
+        print("Expected .h, .cc, and .json filenames")
     else:
         # Write the generated constants out to a file.
-        drivetrain.WriteDrivetrain(argv[1:3], argv[3:5], 'y2014', kDrivetrain)
+        drivetrain.WriteDrivetrain(argv[1:4], argv[4:7], 'y2014', kDrivetrain)
 
 
 if __name__ == '__main__':
diff --git a/y2014/control_loops/python/polydrivetrain.py b/y2014/control_loops/python/polydrivetrain.py
index 05b6658..978567a 100755
--- a/y2014/control_loops/python/polydrivetrain.py
+++ b/y2014/control_loops/python/polydrivetrain.py
@@ -20,10 +20,10 @@
 def main(argv):
     if FLAGS.plot:
         polydrivetrain.PlotPolyDrivetrainMotions(drivetrain.kDrivetrain)
-    elif len(argv) != 7:
-        glog.fatal('Expected .h file name and .cc file name')
+    elif len(argv) != 10:
+        glog.fatal('Expected .h, .cc, and .json filenames')
     else:
-        polydrivetrain.WritePolyDrivetrain(argv[1:3], argv[3:5], argv[5:7],
+        polydrivetrain.WritePolyDrivetrain(argv[1:4], argv[4:7], argv[7:10],
                                            'y2014', drivetrain.kDrivetrain)
 
 
diff --git a/y2014_bot3/control_loops/drivetrain/BUILD b/y2014_bot3/control_loops/drivetrain/BUILD
index 9a5bb53..7f303b3 100644
--- a/y2014_bot3/control_loops/drivetrain/BUILD
+++ b/y2014_bot3/control_loops/drivetrain/BUILD
@@ -5,8 +5,10 @@
     outs = [
         "drivetrain_dog_motor_plant.h",
         "drivetrain_dog_motor_plant.cc",
+        "drivetrain_dog_motor_plant.json",
         "kalman_drivetrain_motor_plant.h",
         "kalman_drivetrain_motor_plant.cc",
+        "kalman_drivetrain_motor_plant.json",
     ],
     cmd = "$(location //y2014_bot3/control_loops/python:drivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
@@ -21,10 +23,13 @@
     outs = [
         "polydrivetrain_dog_motor_plant.h",
         "polydrivetrain_dog_motor_plant.cc",
+        "polydrivetrain_dog_motor_plant.json",
         "polydrivetrain_cim_plant.h",
         "polydrivetrain_cim_plant.cc",
+        "polydrivetrain_cim_plant.json",
         "hybrid_velocity_drivetrain.h",
         "hybrid_velocity_drivetrain.cc",
+        "hybrid_velocity_drivetrain.json",
     ],
     cmd = "$(location //y2014_bot3/control_loops/python:polydrivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
diff --git a/y2014_bot3/control_loops/python/drivetrain.py b/y2014_bot3/control_loops/python/drivetrain.py
index 955cf4c..4d4a059 100755
--- a/y2014_bot3/control_loops/python/drivetrain.py
+++ b/y2014_bot3/control_loops/python/drivetrain.py
@@ -32,11 +32,11 @@
 
     if FLAGS.plot:
         drivetrain.PlotDrivetrainMotions(kDrivetrain)
-    elif len(argv) != 5:
-        print("Expected .h file name and .cc file name")
+    elif len(argv) != 7:
+        print("Expected .h, .cc, and .json filenames")
     else:
         # Write the generated constants out to a file.
-        drivetrain.WriteDrivetrain(argv[1:3], argv[3:5], 'y2014_bot3',
+        drivetrain.WriteDrivetrain(argv[1:4], argv[4:7], 'y2014_bot3',
                                    kDrivetrain)
 
 
diff --git a/y2014_bot3/control_loops/python/polydrivetrain.py b/y2014_bot3/control_loops/python/polydrivetrain.py
index 881b2b0..62552cc 100755
--- a/y2014_bot3/control_loops/python/polydrivetrain.py
+++ b/y2014_bot3/control_loops/python/polydrivetrain.py
@@ -20,10 +20,10 @@
 def main(argv):
     if FLAGS.plot:
         polydrivetrain.PlotPolyDrivetrainMotions(drivetrain.kDrivetrain)
-    elif len(argv) != 7:
-        glog.fatal('Expected .h file name and .cc file name')
+    elif len(argv) != 10:
+        glog.fatal('Expected .h, .cc, and .json filenames')
     else:
-        polydrivetrain.WritePolyDrivetrain(argv[1:3], argv[3:5], argv[5:7],
+        polydrivetrain.WritePolyDrivetrain(argv[1:4], argv[4:7], argv[7:10],
                                            'y2014_bot3',
                                            drivetrain.kDrivetrain)
 
diff --git a/y2016/control_loops/drivetrain/BUILD b/y2016/control_loops/drivetrain/BUILD
index 0988e03..c48fa14 100644
--- a/y2016/control_loops/drivetrain/BUILD
+++ b/y2016/control_loops/drivetrain/BUILD
@@ -3,8 +3,10 @@
     outs = [
         "drivetrain_dog_motor_plant.h",
         "drivetrain_dog_motor_plant.cc",
+        "drivetrain_dog_motor_plant.json",
         "kalman_drivetrain_motor_plant.h",
         "kalman_drivetrain_motor_plant.cc",
+        "kalman_drivetrain_motor_plant.json",
     ],
     cmd = "$(location //y2016/control_loops/python:drivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
@@ -18,10 +20,13 @@
     outs = [
         "polydrivetrain_dog_motor_plant.h",
         "polydrivetrain_dog_motor_plant.cc",
+        "polydrivetrain_dog_motor_plant.json",
         "polydrivetrain_cim_plant.h",
         "polydrivetrain_cim_plant.cc",
+        "polydrivetrain_cim_plant.json",
         "hybrid_velocity_drivetrain.h",
         "hybrid_velocity_drivetrain.cc",
+        "hybrid_velocity_drivetrain.json",
     ],
     cmd = "$(location //y2016/control_loops/python:polydrivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
diff --git a/y2016/control_loops/python/drivetrain.py b/y2016/control_loops/python/drivetrain.py
index a0205e7..14f8210 100755
--- a/y2016/control_loops/python/drivetrain.py
+++ b/y2016/control_loops/python/drivetrain.py
@@ -33,11 +33,11 @@
 
     if FLAGS.plot:
         drivetrain.PlotDrivetrainMotions(kDrivetrain)
-    elif len(argv) != 5:
-        print("Expected .h file name and .cc file name")
+    elif len(argv) != 7:
+        print("Expected .h, .cc, and .json filenames")
     else:
         # Write the generated constants out to a file.
-        drivetrain.WriteDrivetrain(argv[1:3], argv[3:5], 'y2016', kDrivetrain)
+        drivetrain.WriteDrivetrain(argv[1:4], argv[4:7], 'y2016', kDrivetrain)
 
 
 if __name__ == '__main__':
diff --git a/y2016/control_loops/python/polydrivetrain.py b/y2016/control_loops/python/polydrivetrain.py
index 69a7be9..e289cef 100755
--- a/y2016/control_loops/python/polydrivetrain.py
+++ b/y2016/control_loops/python/polydrivetrain.py
@@ -20,10 +20,10 @@
 def main(argv):
     if FLAGS.plot:
         polydrivetrain.PlotPolyDrivetrainMotions(drivetrain.kDrivetrain)
-    elif len(argv) != 7:
-        glog.fatal('Expected .h file name and .cc file name')
+    elif len(argv) != 10:
+        glog.fatal('Expected .h, .cc, and .json filenames')
     else:
-        polydrivetrain.WritePolyDrivetrain(argv[1:3], argv[3:5], argv[5:7],
+        polydrivetrain.WritePolyDrivetrain(argv[1:4], argv[4:7], argv[7:10],
                                            'y2016', drivetrain.kDrivetrain)
 
 
diff --git a/y2017/control_loops/drivetrain/BUILD b/y2017/control_loops/drivetrain/BUILD
index 86d923f..a4fcae3 100644
--- a/y2017/control_loops/drivetrain/BUILD
+++ b/y2017/control_loops/drivetrain/BUILD
@@ -5,8 +5,10 @@
     outs = [
         "drivetrain_dog_motor_plant.h",
         "drivetrain_dog_motor_plant.cc",
+        "drivetrain_dog_motor_plant.json",
         "kalman_drivetrain_motor_plant.h",
         "kalman_drivetrain_motor_plant.cc",
+        "kalman_drivetrain_motor_plant.json",
     ],
     cmd = "$(location //y2017/control_loops/python:drivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
@@ -21,10 +23,13 @@
     outs = [
         "polydrivetrain_dog_motor_plant.h",
         "polydrivetrain_dog_motor_plant.cc",
+        "polydrivetrain_dog_motor_plant.json",
         "polydrivetrain_cim_plant.h",
         "polydrivetrain_cim_plant.cc",
+        "polydrivetrain_cim_plant.json",
         "hybrid_velocity_drivetrain.h",
         "hybrid_velocity_drivetrain.cc",
+        "hybrid_velocity_drivetrain.json",
     ],
     cmd = "$(location //y2017/control_loops/python:polydrivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
diff --git a/y2017/control_loops/python/drivetrain.py b/y2017/control_loops/python/drivetrain.py
index b1ebcd8..b5d05e0 100755
--- a/y2017/control_loops/python/drivetrain.py
+++ b/y2017/control_loops/python/drivetrain.py
@@ -29,11 +29,11 @@
 
     if FLAGS.plot:
         drivetrain.PlotDrivetrainMotions(kDrivetrain)
-    elif len(argv) != 5:
-        print("Expected .h file name and .cc file name")
+    elif len(argv) != 7:
+        print("Expected .h, .cc, and .json filenames")
     else:
         # Write the generated constants out to a file.
-        drivetrain.WriteDrivetrain(argv[1:3], argv[3:5], 'y2017', kDrivetrain)
+        drivetrain.WriteDrivetrain(argv[1:4], argv[4:7], 'y2017', kDrivetrain)
 
 
 if __name__ == '__main__':
diff --git a/y2017/control_loops/python/polydrivetrain.py b/y2017/control_loops/python/polydrivetrain.py
index e181ef5..682ac49 100755
--- a/y2017/control_loops/python/polydrivetrain.py
+++ b/y2017/control_loops/python/polydrivetrain.py
@@ -20,10 +20,10 @@
 def main(argv):
     if FLAGS.plot:
         polydrivetrain.PlotPolyDrivetrainMotions(drivetrain.kDrivetrain)
-    elif len(argv) != 7:
-        glog.fatal('Expected .h file name and .cc file name')
+    elif len(argv) != 10:
+        glog.fatal('Expected .h, .cc, and .json filenames')
     else:
-        polydrivetrain.WritePolyDrivetrain(argv[1:3], argv[3:5], argv[5:7],
+        polydrivetrain.WritePolyDrivetrain(argv[1:4], argv[4:7], argv[7:10],
                                            'y2017', drivetrain.kDrivetrain)
 
 
diff --git a/y2018/control_loops/drivetrain/BUILD b/y2018/control_loops/drivetrain/BUILD
index e9a75b8..9ae4916 100644
--- a/y2018/control_loops/drivetrain/BUILD
+++ b/y2018/control_loops/drivetrain/BUILD
@@ -3,8 +3,10 @@
     outs = [
         "drivetrain_dog_motor_plant.h",
         "drivetrain_dog_motor_plant.cc",
+        "drivetrain_dog_motor_plant.json",
         "kalman_drivetrain_motor_plant.h",
         "kalman_drivetrain_motor_plant.cc",
+        "kalman_drivetrain_motor_plant.json",
     ],
     cmd = "$(location //y2018/control_loops/python:drivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
@@ -18,10 +20,13 @@
     outs = [
         "polydrivetrain_dog_motor_plant.h",
         "polydrivetrain_dog_motor_plant.cc",
+        "polydrivetrain_dog_motor_plant.json",
         "polydrivetrain_cim_plant.h",
         "polydrivetrain_cim_plant.cc",
+        "polydrivetrain_cim_plant.json",
         "hybrid_velocity_drivetrain.h",
         "hybrid_velocity_drivetrain.cc",
+        "hybrid_velocity_drivetrain.json",
     ],
     cmd = "$(location //y2018/control_loops/python:polydrivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
diff --git a/y2018/control_loops/python/drivetrain.py b/y2018/control_loops/python/drivetrain.py
index 0e5ed43..ccc8a20 100644
--- a/y2018/control_loops/python/drivetrain.py
+++ b/y2018/control_loops/python/drivetrain.py
@@ -41,11 +41,11 @@
 
     if FLAGS.plot:
         drivetrain.PlotDrivetrainMotions(kDrivetrain)
-    elif len(argv) != 5:
-        print("Expected .h file name and .cc file name")
+    elif len(argv) != 7:
+        print("Expected .h, .cc, and .json filenames")
     else:
         # Write the generated constants out to a file.
-        drivetrain.WriteDrivetrain(argv[1:3], argv[3:5], 'y2018', kDrivetrain)
+        drivetrain.WriteDrivetrain(argv[1:4], argv[4:7], 'y2018', kDrivetrain)
 
 
 if __name__ == '__main__':
diff --git a/y2018/control_loops/python/polydrivetrain.py b/y2018/control_loops/python/polydrivetrain.py
index f406aa2..45ed5dc 100644
--- a/y2018/control_loops/python/polydrivetrain.py
+++ b/y2018/control_loops/python/polydrivetrain.py
@@ -20,10 +20,10 @@
 def main(argv):
     if FLAGS.plot:
         polydrivetrain.PlotPolyDrivetrainMotions(drivetrain.kDrivetrain)
-    elif len(argv) != 7:
-        glog.fatal('Expected .h file name and .cc file name')
+    elif len(argv) != 10:
+        glog.fatal('Expected .h, .cc, and .json filenames')
     else:
-        polydrivetrain.WritePolyDrivetrain(argv[1:3], argv[3:5], argv[5:7],
+        polydrivetrain.WritePolyDrivetrain(argv[1:4], argv[4:7], argv[7:10],
                                            'y2018', drivetrain.kDrivetrain)
 
 
diff --git a/y2019/control_loops/drivetrain/BUILD b/y2019/control_loops/drivetrain/BUILD
index 9da671d..65857c6 100644
--- a/y2019/control_loops/drivetrain/BUILD
+++ b/y2019/control_loops/drivetrain/BUILD
@@ -7,8 +7,10 @@
     outs = [
         "drivetrain_dog_motor_plant.h",
         "drivetrain_dog_motor_plant.cc",
+        "drivetrain_dog_motor_plant.json",
         "kalman_drivetrain_motor_plant.h",
         "kalman_drivetrain_motor_plant.cc",
+        "kalman_drivetrain_motor_plant.json",
     ],
     cmd = "$(location //y2019/control_loops/python:drivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
@@ -22,10 +24,13 @@
     outs = [
         "polydrivetrain_dog_motor_plant.h",
         "polydrivetrain_dog_motor_plant.cc",
+        "polydrivetrain_dog_motor_plant.json",
         "polydrivetrain_cim_plant.h",
         "polydrivetrain_cim_plant.cc",
+        "polydrivetrain_cim_plant.json",
         "hybrid_velocity_drivetrain.h",
         "hybrid_velocity_drivetrain.cc",
+        "hybrid_velocity_drivetrain.json",
     ],
     cmd = "$(location //y2019/control_loops/python:polydrivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
diff --git a/y2019/control_loops/python/drivetrain.py b/y2019/control_loops/python/drivetrain.py
index 0ef54ef..efb080f 100644
--- a/y2019/control_loops/python/drivetrain.py
+++ b/y2019/control_loops/python/drivetrain.py
@@ -41,11 +41,11 @@
 
     if FLAGS.plot:
         drivetrain.PlotDrivetrainMotions(kDrivetrain)
-    elif len(argv) != 5:
-        print("Expected .h file name and .cc file name")
+    elif len(argv) != 7:
+        print("Expected .h, .cc, and .json filenames")
     else:
         # Write the generated constants out to a file.
-        drivetrain.WriteDrivetrain(argv[1:3], argv[3:5], 'y2019', kDrivetrain)
+        drivetrain.WriteDrivetrain(argv[1:4], argv[4:7], 'y2019', kDrivetrain)
 
 
 if __name__ == '__main__':
diff --git a/y2019/control_loops/python/polydrivetrain.py b/y2019/control_loops/python/polydrivetrain.py
index a045af1..b83f38f 100644
--- a/y2019/control_loops/python/polydrivetrain.py
+++ b/y2019/control_loops/python/polydrivetrain.py
@@ -20,10 +20,10 @@
 def main(argv):
     if FLAGS.plot:
         polydrivetrain.PlotPolyDrivetrainMotions(drivetrain.kDrivetrain)
-    elif len(argv) != 7:
-        glog.fatal('Expected .h file name and .cc file name')
+    elif len(argv) != 10:
+        glog.fatal('Expected .h, .cc, and .json filenames')
     else:
-        polydrivetrain.WritePolyDrivetrain(argv[1:3], argv[3:5], argv[5:7],
+        polydrivetrain.WritePolyDrivetrain(argv[1:4], argv[4:7], argv[7:10],
                                            'y2019', drivetrain.kDrivetrain)
 
 
diff --git a/y2020/control_loops/drivetrain/BUILD b/y2020/control_loops/drivetrain/BUILD
index 5e859ab..0070156 100644
--- a/y2020/control_loops/drivetrain/BUILD
+++ b/y2020/control_loops/drivetrain/BUILD
@@ -8,8 +8,10 @@
     outs = [
         "drivetrain_dog_motor_plant.h",
         "drivetrain_dog_motor_plant.cc",
+        "drivetrain_dog_motor_plant.json",
         "kalman_drivetrain_motor_plant.h",
         "kalman_drivetrain_motor_plant.cc",
+        "kalman_drivetrain_motor_plant.json",
     ],
     cmd = "$(location //y2020/control_loops/python:drivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
@@ -23,10 +25,13 @@
     outs = [
         "polydrivetrain_dog_motor_plant.h",
         "polydrivetrain_dog_motor_plant.cc",
+        "polydrivetrain_dog_motor_plant.json",
         "polydrivetrain_cim_plant.h",
         "polydrivetrain_cim_plant.cc",
+        "polydrivetrain_cim_plant.json",
         "hybrid_velocity_drivetrain.h",
         "hybrid_velocity_drivetrain.cc",
+        "hybrid_velocity_drivetrain.json",
     ],
     cmd = "$(location //y2020/control_loops/python:polydrivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
diff --git a/y2020/control_loops/python/drivetrain.py b/y2020/control_loops/python/drivetrain.py
index ba43450..28c46da 100644
--- a/y2020/control_loops/python/drivetrain.py
+++ b/y2020/control_loops/python/drivetrain.py
@@ -35,11 +35,11 @@
 
     if FLAGS.plot:
         drivetrain.PlotDrivetrainMotions(kDrivetrain)
-    elif len(argv) != 5:
-        print("Expected .h file name and .cc file name")
+    elif len(argv) != 7:
+        print("Expected .h, .cc, and .json filenames")
     else:
         # Write the generated constants out to a file.
-        drivetrain.WriteDrivetrain(argv[1:3], argv[3:5], 'y2020', kDrivetrain)
+        drivetrain.WriteDrivetrain(argv[1:4], argv[4:7], 'y2020', kDrivetrain)
 
 
 if __name__ == '__main__':
diff --git a/y2020/control_loops/python/polydrivetrain.py b/y2020/control_loops/python/polydrivetrain.py
index dbd5268..4dd4c0b 100644
--- a/y2020/control_loops/python/polydrivetrain.py
+++ b/y2020/control_loops/python/polydrivetrain.py
@@ -20,10 +20,10 @@
 def main(argv):
     if FLAGS.plot:
         polydrivetrain.PlotPolyDrivetrainMotions(drivetrain.kDrivetrain)
-    elif len(argv) != 7:
-        glog.fatal('Expected .h file name and .cc file name')
+    elif len(argv) != 10:
+        glog.fatal('Expected .h, .cc, and .json filenames')
     else:
-        polydrivetrain.WritePolyDrivetrain(argv[1:3], argv[3:5], argv[5:7],
+        polydrivetrain.WritePolyDrivetrain(argv[1:4], argv[4:7], argv[7:10],
                                            'y2020', drivetrain.kDrivetrain)
 
 
diff --git a/y2021_bot3/control_loops/drivetrain/BUILD b/y2021_bot3/control_loops/drivetrain/BUILD
index 3f9a3df..d35e46b 100644
--- a/y2021_bot3/control_loops/drivetrain/BUILD
+++ b/y2021_bot3/control_loops/drivetrain/BUILD
@@ -3,8 +3,10 @@
     outs = [
         "drivetrain_dog_motor_plant.h",
         "drivetrain_dog_motor_plant.cc",
+        "drivetrain_dog_motor_plant.json",
         "kalman_drivetrain_motor_plant.h",
         "kalman_drivetrain_motor_plant.cc",
+        "kalman_drivetrain_motor_plant.json",
     ],
     cmd = "$(location //y2021_bot3/control_loops/python:drivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
@@ -18,10 +20,13 @@
     outs = [
         "polydrivetrain_dog_motor_plant.h",
         "polydrivetrain_dog_motor_plant.cc",
+        "polydrivetrain_dog_motor_plant.json",
         "polydrivetrain_cim_plant.h",
         "polydrivetrain_cim_plant.cc",
+        "polydrivetrain_cim_plant.json",
         "hybrid_velocity_drivetrain.h",
         "hybrid_velocity_drivetrain.cc",
+        "hybrid_velocity_drivetrain.json",
     ],
     cmd = "$(location //y2021_bot3/control_loops/python:polydrivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
diff --git a/y2021_bot3/control_loops/python/drivetrain.py b/y2021_bot3/control_loops/python/drivetrain.py
index f3d4682..dfbdd91 100644
--- a/y2021_bot3/control_loops/python/drivetrain.py
+++ b/y2021_bot3/control_loops/python/drivetrain.py
@@ -36,11 +36,11 @@
 
     if FLAGS.plot:
         drivetrain.PlotDrivetrainMotions(kDrivetrain)
-    elif len(argv) != 5:
-        print("Expected .h file name and .cc file name")
+    elif len(argv) != 7:
+        print("Expected .h, .cc, and .json filenames")
     else:
         # Write the generated constants out to a file.
-        drivetrain.WriteDrivetrain(argv[1:3], argv[3:5], 'y2021_bot3',
+        drivetrain.WriteDrivetrain(argv[1:4], argv[4:7], 'y2021_bot3',
                                    kDrivetrain)
 
 
diff --git a/y2021_bot3/control_loops/python/polydrivetrain.py b/y2021_bot3/control_loops/python/polydrivetrain.py
index f8c3417..9268967 100644
--- a/y2021_bot3/control_loops/python/polydrivetrain.py
+++ b/y2021_bot3/control_loops/python/polydrivetrain.py
@@ -20,10 +20,10 @@
 def main(argv):
     if FLAGS.plot:
         polydrivetrain.PlotPolyDrivetrainMotions(drivetrain.kDrivetrain)
-    elif len(argv) != 7:
-        glog.fatal('Expected .h file name and .cc file name')
+    elif len(argv) != 10:
+        glog.fatal('Expected .h, .cc, and .json filenames')
     else:
-        polydrivetrain.WritePolyDrivetrain(argv[1:3], argv[3:5], argv[5:7],
+        polydrivetrain.WritePolyDrivetrain(argv[1:4], argv[4:7], argv[7:10],
                                            'y2021_bot3',
                                            drivetrain.kDrivetrain)
 
diff --git a/y2022/control_loops/drivetrain/BUILD b/y2022/control_loops/drivetrain/BUILD
index 148a998..97a7aba 100644
--- a/y2022/control_loops/drivetrain/BUILD
+++ b/y2022/control_loops/drivetrain/BUILD
@@ -5,8 +5,10 @@
     outs = [
         "drivetrain_dog_motor_plant.h",
         "drivetrain_dog_motor_plant.cc",
+        "drivetrain_dog_motor_plant.json",
         "kalman_drivetrain_motor_plant.h",
         "kalman_drivetrain_motor_plant.cc",
+        "kalman_drivetrain_motor_plant.json",
     ],
     cmd = "$(location //y2022/control_loops/python:drivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
@@ -20,10 +22,13 @@
     outs = [
         "polydrivetrain_dog_motor_plant.h",
         "polydrivetrain_dog_motor_plant.cc",
+        "polydrivetrain_dog_motor_plant.json",
         "polydrivetrain_cim_plant.h",
         "polydrivetrain_cim_plant.cc",
+        "polydrivetrain_cim_plant.json",
         "hybrid_velocity_drivetrain.h",
         "hybrid_velocity_drivetrain.cc",
+        "hybrid_velocity_drivetrain.json",
     ],
     cmd = "$(location //y2022/control_loops/python:polydrivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
diff --git a/y2022/control_loops/python/drivetrain.py b/y2022/control_loops/python/drivetrain.py
index 59b9651..095ae98 100644
--- a/y2022/control_loops/python/drivetrain.py
+++ b/y2022/control_loops/python/drivetrain.py
@@ -36,11 +36,11 @@
 
     if FLAGS.plot:
         drivetrain.PlotDrivetrainMotions(kDrivetrain)
-    elif len(argv) != 5:
-        print("Expected .h file name and .cc file name")
+    elif len(argv) != 7:
+        print("Expected .h, .cc, and .json filenames")
     else:
         # Write the generated constants out to a file.
-        drivetrain.WriteDrivetrain(argv[1:3], argv[3:5], 'y2022', kDrivetrain)
+        drivetrain.WriteDrivetrain(argv[1:4], argv[4:7], 'y2022', kDrivetrain)
 
 
 if __name__ == '__main__':
diff --git a/y2022/control_loops/python/polydrivetrain.py b/y2022/control_loops/python/polydrivetrain.py
index cf54470..64ec11b 100644
--- a/y2022/control_loops/python/polydrivetrain.py
+++ b/y2022/control_loops/python/polydrivetrain.py
@@ -20,10 +20,10 @@
 def main(argv):
     if FLAGS.plot:
         polydrivetrain.PlotPolyDrivetrainMotions(drivetrain.kDrivetrain)
-    elif len(argv) != 7:
-        glog.fatal('Expected .h file name and .cc file name')
+    elif len(argv) != 10:
+        glog.fatal('Expected .h, .cc, and .json filenames')
     else:
-        polydrivetrain.WritePolyDrivetrain(argv[1:3], argv[3:5], argv[5:7],
+        polydrivetrain.WritePolyDrivetrain(argv[1:4], argv[4:7], argv[7:10],
                                            'y2022', drivetrain.kDrivetrain)
 
 
diff --git a/y2022_bot3/control_loops/drivetrain/BUILD b/y2022_bot3/control_loops/drivetrain/BUILD
index ecdcf6b..3bd4db7 100644
--- a/y2022_bot3/control_loops/drivetrain/BUILD
+++ b/y2022_bot3/control_loops/drivetrain/BUILD
@@ -5,8 +5,10 @@
     outs = [
         "drivetrain_dog_motor_plant.h",
         "drivetrain_dog_motor_plant.cc",
+        "drivetrain_dog_motor_plant.json",
         "kalman_drivetrain_motor_plant.h",
         "kalman_drivetrain_motor_plant.cc",
+        "kalman_drivetrain_motor_plant.json",
     ],
     cmd = "$(location //y2022_bot3/control_loops/python:drivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
@@ -20,10 +22,13 @@
     outs = [
         "polydrivetrain_dog_motor_plant.h",
         "polydrivetrain_dog_motor_plant.cc",
+        "polydrivetrain_dog_motor_plant.json",
         "polydrivetrain_cim_plant.h",
         "polydrivetrain_cim_plant.cc",
+        "polydrivetrain_cim_plant.json",
         "hybrid_velocity_drivetrain.h",
         "hybrid_velocity_drivetrain.cc",
+        "hybrid_velocity_drivetrain.json",
     ],
     cmd = "$(location //y2022_bot3/control_loops/python:polydrivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
diff --git a/y2022_bot3/control_loops/python/drivetrain.py b/y2022_bot3/control_loops/python/drivetrain.py
index 7db0a05..a49b8f2 100644
--- a/y2022_bot3/control_loops/python/drivetrain.py
+++ b/y2022_bot3/control_loops/python/drivetrain.py
@@ -36,11 +36,11 @@
 
     if FLAGS.plot:
         drivetrain.PlotDrivetrainMotions(kDrivetrain)
-    elif len(argv) != 5:
-        print("Expected .h file name and .cc file name")
+    elif len(argv) != 7:
+        print("Expected .h, .cc, and .json filenames")
     else:
         # Write the generated constants out to a file.
-        drivetrain.WriteDrivetrain(argv[1:3], argv[3:5], 'y2022_bot3',
+        drivetrain.WriteDrivetrain(argv[1:4], argv[4:7], 'y2022_bot3',
                                    kDrivetrain)
 
 
diff --git a/y2022_bot3/control_loops/python/polydrivetrain.py b/y2022_bot3/control_loops/python/polydrivetrain.py
index 14b9285..13884ee 100644
--- a/y2022_bot3/control_loops/python/polydrivetrain.py
+++ b/y2022_bot3/control_loops/python/polydrivetrain.py
@@ -20,10 +20,10 @@
 def main(argv):
     if FLAGS.plot:
         polydrivetrain.PlotPolyDrivetrainMotions(drivetrain.kDrivetrain)
-    elif len(argv) != 7:
-        glog.fatal('Expected .h file name and .cc file name')
+    elif len(argv) != 10:
+        glog.fatal('Expected .h, .cc, and .json filenames')
     else:
-        polydrivetrain.WritePolyDrivetrain(argv[1:3], argv[3:5], argv[5:7],
+        polydrivetrain.WritePolyDrivetrain(argv[1:4], argv[4:7], argv[7:10],
                                            'y2022_bot3',
                                            drivetrain.kDrivetrain)
 
diff --git a/y2023/control_loops/drivetrain/BUILD b/y2023/control_loops/drivetrain/BUILD
index 01c360b..ea2110e 100644
--- a/y2023/control_loops/drivetrain/BUILD
+++ b/y2023/control_loops/drivetrain/BUILD
@@ -6,8 +6,10 @@
     outs = [
         "drivetrain_dog_motor_plant.h",
         "drivetrain_dog_motor_plant.cc",
+        "drivetrain_dog_motor_plant.json",
         "kalman_drivetrain_motor_plant.h",
         "kalman_drivetrain_motor_plant.cc",
+        "kalman_drivetrain_motor_plant.json",
     ],
     cmd = "$(location //y2023/control_loops/python:drivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
@@ -21,10 +23,13 @@
     outs = [
         "polydrivetrain_dog_motor_plant.h",
         "polydrivetrain_dog_motor_plant.cc",
+        "polydrivetrain_dog_motor_plant.json",
         "polydrivetrain_cim_plant.h",
         "polydrivetrain_cim_plant.cc",
+        "polydrivetrain_cim_plant.json",
         "hybrid_velocity_drivetrain.h",
         "hybrid_velocity_drivetrain.cc",
+        "hybrid_velocity_drivetrain.json",
     ],
     cmd = "$(location //y2023/control_loops/python:polydrivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
diff --git a/y2023/control_loops/python/drivetrain.py b/y2023/control_loops/python/drivetrain.py
index 9d2b006..69d4f3e 100644
--- a/y2023/control_loops/python/drivetrain.py
+++ b/y2023/control_loops/python/drivetrain.py
@@ -36,11 +36,11 @@
 
     if FLAGS.plot:
         drivetrain.PlotDrivetrainMotions(kDrivetrain)
-    elif len(argv) != 5:
-        print("Expected .h file name and .cc file name")
+    elif len(argv) != 7:
+        print("Expected .h, .cc, and .json filenames")
     else:
         # Write the generated constants out to a file.
-        drivetrain.WriteDrivetrain(argv[1:3], argv[3:5], 'y2023', kDrivetrain)
+        drivetrain.WriteDrivetrain(argv[1:4], argv[4:7], 'y2023', kDrivetrain)
 
 
 if __name__ == '__main__':
diff --git a/y2023/control_loops/python/polydrivetrain.py b/y2023/control_loops/python/polydrivetrain.py
index 2f2346d..8ce6b8c 100644
--- a/y2023/control_loops/python/polydrivetrain.py
+++ b/y2023/control_loops/python/polydrivetrain.py
@@ -20,10 +20,10 @@
 def main(argv):
     if FLAGS.plot:
         polydrivetrain.PlotPolyDrivetrainMotions(drivetrain.kDrivetrain)
-    elif len(argv) != 7:
-        glog.fatal('Expected .h file name and .cc file name')
+    elif len(argv) != 10:
+        glog.fatal('Expected .h, .cc, and .json filenames')
     else:
-        polydrivetrain.WritePolyDrivetrain(argv[1:3], argv[3:5], argv[5:7],
+        polydrivetrain.WritePolyDrivetrain(argv[1:4], argv[4:7], argv[7:10],
                                            'y2023', drivetrain.kDrivetrain)
 
 
diff --git a/y2023_bot3/control_loops/drivetrain/BUILD b/y2023_bot3/control_loops/drivetrain/BUILD
index 46654ec..62c213d 100644
--- a/y2023_bot3/control_loops/drivetrain/BUILD
+++ b/y2023_bot3/control_loops/drivetrain/BUILD
@@ -3,8 +3,10 @@
     outs = [
         "drivetrain_dog_motor_plant.h",
         "drivetrain_dog_motor_plant.cc",
+        "drivetrain_dog_motor_plant.json",
         "kalman_drivetrain_motor_plant.h",
         "kalman_drivetrain_motor_plant.cc",
+        "kalman_drivetrain_motor_plant.json",
     ],
     cmd = "$(location //y2023_bot3/control_loops/python:drivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
@@ -18,10 +20,13 @@
     outs = [
         "polydrivetrain_dog_motor_plant.h",
         "polydrivetrain_dog_motor_plant.cc",
+        "polydrivetrain_dog_motor_plant.json",
         "polydrivetrain_cim_plant.h",
         "polydrivetrain_cim_plant.cc",
+        "polydrivetrain_cim_plant.json",
         "hybrid_velocity_drivetrain.h",
         "hybrid_velocity_drivetrain.cc",
+        "hybrid_velocity_drivetrain.json",
     ],
     cmd = "$(location //y2023_bot3/control_loops/python:polydrivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
diff --git a/y2023_bot3/control_loops/python/drivetrain.py b/y2023_bot3/control_loops/python/drivetrain.py
index 26cb043..ac0da93 100644
--- a/y2023_bot3/control_loops/python/drivetrain.py
+++ b/y2023_bot3/control_loops/python/drivetrain.py
@@ -37,11 +37,11 @@
 
     if FLAGS.plot:
         drivetrain.PlotDrivetrainMotions(kDrivetrain)
-    elif len(argv) != 5:
-        print("Expected .h file name and .cc file name")
+    elif len(argv) != 7:
+        print("Expected .h, .cc, and .json filenames")
     else:
         # Write the generated constants out to a file.
-        drivetrain.WriteDrivetrain(argv[1:3], argv[3:5], 'y2023_bot3',
+        drivetrain.WriteDrivetrain(argv[1:4], argv[4:7], 'y2023_bot3',
                                    kDrivetrain)
 
 
diff --git a/y2023_bot3/control_loops/python/polydrivetrain.py b/y2023_bot3/control_loops/python/polydrivetrain.py
index 80e4f13..bd9dc89 100644
--- a/y2023_bot3/control_loops/python/polydrivetrain.py
+++ b/y2023_bot3/control_loops/python/polydrivetrain.py
@@ -20,10 +20,10 @@
 def main(argv):
     if FLAGS.plot:
         polydrivetrain.PlotPolyDrivetrainMotions(drivetrain.kDrivetrain)
-    elif len(argv) != 7:
-        glog.fatal('Expected .h file name and .cc file name')
+    elif len(argv) != 10:
+        glog.fatal('Expected .h, .cc, and .json filenames')
     else:
-        polydrivetrain.WritePolyDrivetrain(argv[1:3], argv[3:5], argv[5:7],
+        polydrivetrain.WritePolyDrivetrain(argv[1:4], argv[4:7], argv[7:10],
                                            'y2023_bot3',
                                            drivetrain.kDrivetrain)
 
diff --git a/y2024/control_loops/drivetrain/BUILD b/y2024/control_loops/drivetrain/BUILD
index e3c627a..0cca123 100644
--- a/y2024/control_loops/drivetrain/BUILD
+++ b/y2024/control_loops/drivetrain/BUILD
@@ -5,8 +5,10 @@
     outs = [
         "drivetrain_dog_motor_plant.h",
         "drivetrain_dog_motor_plant.cc",
+        "drivetrain_dog_motor_plant.json",
         "kalman_drivetrain_motor_plant.h",
         "kalman_drivetrain_motor_plant.cc",
+        "kalman_drivetrain_motor_plant.json",
     ],
     cmd = "$(location //y2024/control_loops/python:drivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
@@ -20,10 +22,13 @@
     outs = [
         "polydrivetrain_dog_motor_plant.h",
         "polydrivetrain_dog_motor_plant.cc",
+        "polydrivetrain_dog_motor_plant.json",
         "polydrivetrain_cim_plant.h",
         "polydrivetrain_cim_plant.cc",
+        "polydrivetrain_cim_plant.json",
         "hybrid_velocity_drivetrain.h",
         "hybrid_velocity_drivetrain.cc",
+        "hybrid_velocity_drivetrain.json",
     ],
     cmd = "$(location //y2024/control_loops/python:polydrivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
diff --git a/y2024/control_loops/python/drivetrain.py b/y2024/control_loops/python/drivetrain.py
index 64dcc47..caedada 100644
--- a/y2024/control_loops/python/drivetrain.py
+++ b/y2024/control_loops/python/drivetrain.py
@@ -36,11 +36,11 @@
 
     if FLAGS.plot:
         drivetrain.PlotDrivetrainMotions(kDrivetrain)
-    elif len(argv) != 5:
-        print("Expected .h file name and .cc file name")
+    elif len(argv) != 7:
+        print("Expected .h, .cc, and .json filenames")
     else:
         # Write the generated constants out to a file.
-        drivetrain.WriteDrivetrain(argv[1:3], argv[3:5], 'y2024', kDrivetrain)
+        drivetrain.WriteDrivetrain(argv[1:4], argv[4:7], 'y2024', kDrivetrain)
 
 
 if __name__ == '__main__':
diff --git a/y2024/control_loops/python/polydrivetrain.py b/y2024/control_loops/python/polydrivetrain.py
index 99a9b53..9a9d2a7 100644
--- a/y2024/control_loops/python/polydrivetrain.py
+++ b/y2024/control_loops/python/polydrivetrain.py
@@ -20,10 +20,10 @@
 def main(argv):
     if FLAGS.plot:
         polydrivetrain.PlotPolyDrivetrainMotions(drivetrain.kDrivetrain)
-    elif len(argv) != 7:
-        glog.fatal('Expected .h file name and .cc file name')
+    elif len(argv) != 10:
+        glog.fatal('Expected .h, .cc, and .json filenames')
     else:
-        polydrivetrain.WritePolyDrivetrain(argv[1:3], argv[3:5], argv[5:7],
+        polydrivetrain.WritePolyDrivetrain(argv[1:4], argv[4:7], argv[7:10],
                                            'y2024', drivetrain.kDrivetrain)
 
 
diff --git a/y2024_defense/control_loops/drivetrain/BUILD b/y2024_defense/control_loops/drivetrain/BUILD
index 664f9a1..9f2349e 100644
--- a/y2024_defense/control_loops/drivetrain/BUILD
+++ b/y2024_defense/control_loops/drivetrain/BUILD
@@ -5,8 +5,10 @@
     outs = [
         "drivetrain_dog_motor_plant.h",
         "drivetrain_dog_motor_plant.cc",
+        "drivetrain_dog_motor_plant.json",
         "kalman_drivetrain_motor_plant.h",
         "kalman_drivetrain_motor_plant.cc",
+        "kalman_drivetrain_motor_plant.json",
     ],
     cmd = "$(location //y2024_defense/control_loops/python:drivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
@@ -20,10 +22,13 @@
     outs = [
         "polydrivetrain_dog_motor_plant.h",
         "polydrivetrain_dog_motor_plant.cc",
+        "polydrivetrain_dog_motor_plant.json",
         "polydrivetrain_cim_plant.h",
         "polydrivetrain_cim_plant.cc",
+        "polydrivetrain_cim_plant.json",
         "hybrid_velocity_drivetrain.h",
         "hybrid_velocity_drivetrain.cc",
+        "hybrid_velocity_drivetrain.json",
     ],
     cmd = "$(location //y2024_defense/control_loops/python:polydrivetrain) $(OUTS)",
     target_compatible_with = ["@platforms//os:linux"],
diff --git a/y2024_defense/control_loops/python/drivetrain.py b/y2024_defense/control_loops/python/drivetrain.py
index 438eb24..a2f46e3 100644
--- a/y2024_defense/control_loops/python/drivetrain.py
+++ b/y2024_defense/control_loops/python/drivetrain.py
@@ -36,11 +36,11 @@
 
     if FLAGS.plot:
         drivetrain.PlotDrivetrainMotions(kDrivetrain)
-    elif len(argv) != 5:
-        print("Expected .h file name and .cc file name")
+    elif len(argv) != 7:
+        print("Expected .h, .cc, and .json filenames")
     else:
         # Write the generated constants out to a file.
-        drivetrain.WriteDrivetrain(argv[1:3], argv[3:5], 'y2024_defense',
+        drivetrain.WriteDrivetrain(argv[1:4], argv[4:7], 'y2024_defense',
                                    kDrivetrain)
 
 
diff --git a/y2024_defense/control_loops/python/polydrivetrain.py b/y2024_defense/control_loops/python/polydrivetrain.py
index 301c7b5..a455f08 100644
--- a/y2024_defense/control_loops/python/polydrivetrain.py
+++ b/y2024_defense/control_loops/python/polydrivetrain.py
@@ -20,10 +20,10 @@
 def main(argv):
     if FLAGS.plot:
         polydrivetrain.PlotPolyDrivetrainMotions(drivetrain.kDrivetrain)
-    elif len(argv) != 7:
-        glog.fatal('Expected .h file name and .cc file name')
+    elif len(argv) != 10:
+        glog.fatal('Expected .h, .cc, and .json filenames')
     else:
-        polydrivetrain.WritePolyDrivetrain(argv[1:3], argv[3:5], argv[5:7],
+        polydrivetrain.WritePolyDrivetrain(argv[1:4], argv[4:7], argv[7:10],
                                            'y2024_defense',
                                            drivetrain.kDrivetrain)