Merge "Scouting: Update dead screen and dropped"
diff --git a/.bazelrc b/.bazelrc
index 9ff6c55..a69ccad 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -96,11 +96,25 @@
 
 build --spawn_strategy=sandboxed
 build --sandbox_default_allow_network=false
+build --incompatible_exclusive_test_sandboxed
 
 build --strategy=TsProject=sandboxed
 build --strategy=CopyFile=standalone
 build --strategy=CopyDirectory=standalone
 
+# Honor the setting of `skipLibCheck` in the tsconfig.json file.
+# https://www.typescriptlang.org/tsconfig#skipLibCheck
+build --@aspect_rules_ts//ts:skipLibCheck=honor_tsconfig
+fetch --@aspect_rules_ts//ts:skipLibCheck=honor_tsconfig
+query --@aspect_rules_ts//ts:skipLibCheck=honor_tsconfig
+
+# Use "tsc" as the transpiler when ts_project has no `transpiler` set.
+# For now this is an acceptable default, but it would be nice to switch to swc in the future.
+# https://docs.aspect.build/rulesets/aspect_rules_ts/docs/transpiler/
+build --@aspect_rules_ts//ts:default_to_tsc_transpiler
+fetch --@aspect_rules_ts//ts:default_to_tsc_transpiler
+query --@aspect_rules_ts//ts:default_to_tsc_transpiler
+
 # Use our hermetic JDK.
 # Note that this doesn't quite work fully, but it should. See
 # https://github.com/bazelbuild/bazel/issues/6341 for ongoing discussion with
diff --git a/Cargo.Bazel.lock b/Cargo.Bazel.lock
index e70a4ca..6bf8948 100644
--- a/Cargo.Bazel.lock
+++ b/Cargo.Bazel.lock
@@ -1,5 +1,5 @@
 {
-  "checksum": "7305dfa1e83bfd0222912e22f8294905db9c35e036a34182aaaf7bafa13c28ac",
+  "checksum": "6c02572fd1f0170376a09b2b84af516c934a7918e2fa5f5842e6a3cbccb0cb31",
   "crates": {
     "addr2line 0.20.0": {
       "name": "addr2line",
@@ -233,6 +233,76 @@
       },
       "license": "MIT"
     },
+    "anstream 0.6.11": {
+      "name": "anstream",
+      "version": "0.6.11",
+      "repository": {
+        "Http": {
+          "url": "https://crates.io/api/v1/crates/anstream/0.6.11/download",
+          "sha256": "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5"
+        }
+      },
+      "targets": [
+        {
+          "Library": {
+            "crate_name": "anstream",
+            "crate_root": "src/lib.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        }
+      ],
+      "library_target_name": "anstream",
+      "common_attrs": {
+        "compile_data_glob": [
+          "**"
+        ],
+        "crate_features": {
+          "common": [
+            "auto",
+            "default",
+            "wincon"
+          ],
+          "selects": {}
+        },
+        "deps": {
+          "common": [
+            {
+              "id": "anstyle 1.0.1",
+              "target": "anstyle"
+            },
+            {
+              "id": "anstyle-parse 0.2.3",
+              "target": "anstyle_parse"
+            },
+            {
+              "id": "anstyle-query 1.0.2",
+              "target": "anstyle_query"
+            },
+            {
+              "id": "colorchoice 1.0.0",
+              "target": "colorchoice"
+            },
+            {
+              "id": "utf8parse 0.2.1",
+              "target": "utf8parse"
+            }
+          ],
+          "selects": {
+            "cfg(windows)": [
+              {
+                "id": "anstyle-wincon 3.0.2",
+                "target": "anstyle_wincon"
+              }
+            ]
+          }
+        },
+        "edition": "2021",
+        "version": "0.6.11"
+      },
+      "license": "MIT OR Apache-2.0"
+    },
     "anstyle 1.0.1": {
       "name": "anstyle",
       "version": "1.0.1",
@@ -270,6 +340,139 @@
       },
       "license": "MIT OR Apache-2.0"
     },
+    "anstyle-parse 0.2.3": {
+      "name": "anstyle-parse",
+      "version": "0.2.3",
+      "repository": {
+        "Http": {
+          "url": "https://crates.io/api/v1/crates/anstyle-parse/0.2.3/download",
+          "sha256": "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
+        }
+      },
+      "targets": [
+        {
+          "Library": {
+            "crate_name": "anstyle_parse",
+            "crate_root": "src/lib.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        }
+      ],
+      "library_target_name": "anstyle_parse",
+      "common_attrs": {
+        "compile_data_glob": [
+          "**"
+        ],
+        "crate_features": {
+          "common": [
+            "default",
+            "utf8"
+          ],
+          "selects": {}
+        },
+        "deps": {
+          "common": [
+            {
+              "id": "utf8parse 0.2.1",
+              "target": "utf8parse"
+            }
+          ],
+          "selects": {}
+        },
+        "edition": "2021",
+        "version": "0.2.3"
+      },
+      "license": "MIT OR Apache-2.0"
+    },
+    "anstyle-query 1.0.2": {
+      "name": "anstyle-query",
+      "version": "1.0.2",
+      "repository": {
+        "Http": {
+          "url": "https://crates.io/api/v1/crates/anstyle-query/1.0.2/download",
+          "sha256": "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
+        }
+      },
+      "targets": [
+        {
+          "Library": {
+            "crate_name": "anstyle_query",
+            "crate_root": "src/lib.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        }
+      ],
+      "library_target_name": "anstyle_query",
+      "common_attrs": {
+        "compile_data_glob": [
+          "**"
+        ],
+        "deps": {
+          "common": [],
+          "selects": {
+            "cfg(windows)": [
+              {
+                "id": "windows-sys 0.52.0",
+                "target": "windows_sys"
+              }
+            ]
+          }
+        },
+        "edition": "2021",
+        "version": "1.0.2"
+      },
+      "license": "MIT OR Apache-2.0"
+    },
+    "anstyle-wincon 3.0.2": {
+      "name": "anstyle-wincon",
+      "version": "3.0.2",
+      "repository": {
+        "Http": {
+          "url": "https://crates.io/api/v1/crates/anstyle-wincon/3.0.2/download",
+          "sha256": "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
+        }
+      },
+      "targets": [
+        {
+          "Library": {
+            "crate_name": "anstyle_wincon",
+            "crate_root": "src/lib.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        }
+      ],
+      "library_target_name": "anstyle_wincon",
+      "common_attrs": {
+        "compile_data_glob": [
+          "**"
+        ],
+        "deps": {
+          "common": [
+            {
+              "id": "anstyle 1.0.1",
+              "target": "anstyle"
+            }
+          ],
+          "selects": {
+            "cfg(windows)": [
+              {
+                "id": "windows-sys 0.52.0",
+                "target": "windows_sys"
+              }
+            ]
+          }
+        },
+        "edition": "2021",
+        "version": "3.0.2"
+      },
+      "license": "MIT OR Apache-2.0"
+    },
     "anyhow 1.0.71": {
       "name": "anyhow",
       "version": "1.0.71",
@@ -385,6 +588,43 @@
       },
       "license": "MIT"
     },
+    "arrayvec 0.7.4": {
+      "name": "arrayvec",
+      "version": "0.7.4",
+      "repository": {
+        "Http": {
+          "url": "https://crates.io/api/v1/crates/arrayvec/0.7.4/download",
+          "sha256": "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
+        }
+      },
+      "targets": [
+        {
+          "Library": {
+            "crate_name": "arrayvec",
+            "crate_root": "src/lib.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        }
+      ],
+      "library_target_name": "arrayvec",
+      "common_attrs": {
+        "compile_data_glob": [
+          "**"
+        ],
+        "crate_features": {
+          "common": [
+            "default",
+            "std"
+          ],
+          "selects": {}
+        },
+        "edition": "2018",
+        "version": "0.7.4"
+      },
+      "license": "MIT OR Apache-2.0"
+    },
     "assert_cmd 2.0.12": {
       "name": "assert_cmd",
       "version": "2.0.12",
@@ -863,7 +1103,8 @@
           "common": [
             {
               "id": "clap 3.2.25",
-              "target": "clap"
+              "target": "clap",
+              "alias": "clap3"
             },
             {
               "id": "env_logger 0.9.3",
@@ -1981,6 +2222,188 @@
       },
       "license": "MIT OR Apache-2.0"
     },
+    "clap 4.4.18": {
+      "name": "clap",
+      "version": "4.4.18",
+      "repository": {
+        "Http": {
+          "url": "https://crates.io/api/v1/crates/clap/4.4.18/download",
+          "sha256": "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c"
+        }
+      },
+      "targets": [
+        {
+          "Library": {
+            "crate_name": "clap",
+            "crate_root": "src/lib.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        }
+      ],
+      "library_target_name": "clap",
+      "common_attrs": {
+        "compile_data_glob": [
+          "**"
+        ],
+        "crate_features": {
+          "common": [
+            "color",
+            "default",
+            "derive",
+            "error-context",
+            "help",
+            "std",
+            "string",
+            "suggestions",
+            "usage"
+          ],
+          "selects": {}
+        },
+        "deps": {
+          "common": [
+            {
+              "id": "clap_builder 4.4.18",
+              "target": "clap_builder"
+            }
+          ],
+          "selects": {}
+        },
+        "edition": "2021",
+        "proc_macro_deps": {
+          "common": [
+            {
+              "id": "clap_derive 4.4.7",
+              "target": "clap_derive"
+            }
+          ],
+          "selects": {}
+        },
+        "version": "4.4.18"
+      },
+      "license": "MIT OR Apache-2.0"
+    },
+    "clap_builder 4.4.18": {
+      "name": "clap_builder",
+      "version": "4.4.18",
+      "repository": {
+        "Http": {
+          "url": "https://crates.io/api/v1/crates/clap_builder/4.4.18/download",
+          "sha256": "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7"
+        }
+      },
+      "targets": [
+        {
+          "Library": {
+            "crate_name": "clap_builder",
+            "crate_root": "src/lib.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        }
+      ],
+      "library_target_name": "clap_builder",
+      "common_attrs": {
+        "compile_data_glob": [
+          "**"
+        ],
+        "crate_features": {
+          "common": [
+            "color",
+            "error-context",
+            "help",
+            "std",
+            "string",
+            "suggestions",
+            "usage"
+          ],
+          "selects": {}
+        },
+        "deps": {
+          "common": [
+            {
+              "id": "anstream 0.6.11",
+              "target": "anstream"
+            },
+            {
+              "id": "anstyle 1.0.1",
+              "target": "anstyle"
+            },
+            {
+              "id": "clap_lex 0.6.0",
+              "target": "clap_lex"
+            },
+            {
+              "id": "strsim 0.10.0",
+              "target": "strsim"
+            }
+          ],
+          "selects": {}
+        },
+        "edition": "2021",
+        "version": "4.4.18"
+      },
+      "license": "MIT OR Apache-2.0"
+    },
+    "clap_derive 4.4.7": {
+      "name": "clap_derive",
+      "version": "4.4.7",
+      "repository": {
+        "Http": {
+          "url": "https://crates.io/api/v1/crates/clap_derive/4.4.7/download",
+          "sha256": "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
+        }
+      },
+      "targets": [
+        {
+          "ProcMacro": {
+            "crate_name": "clap_derive",
+            "crate_root": "src/lib.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        }
+      ],
+      "library_target_name": "clap_derive",
+      "common_attrs": {
+        "compile_data_glob": [
+          "**"
+        ],
+        "crate_features": {
+          "common": [
+            "default"
+          ],
+          "selects": {}
+        },
+        "deps": {
+          "common": [
+            {
+              "id": "heck 0.4.1",
+              "target": "heck"
+            },
+            {
+              "id": "proc-macro2 1.0.63",
+              "target": "proc_macro2"
+            },
+            {
+              "id": "quote 1.0.29",
+              "target": "quote"
+            },
+            {
+              "id": "syn 2.0.28",
+              "target": "syn"
+            }
+          ],
+          "selects": {}
+        },
+        "edition": "2021",
+        "version": "4.4.7"
+      },
+      "license": "MIT OR Apache-2.0"
+    },
     "clap_lex 0.2.4": {
       "name": "clap_lex",
       "version": "0.2.4",
@@ -2009,7 +2432,7 @@
         "deps": {
           "common": [
             {
-              "id": "os_str_bytes 6.5.1",
+              "id": "os_str_bytes 6.6.1",
               "target": "os_str_bytes"
             }
           ],
@@ -2020,6 +2443,36 @@
       },
       "license": "MIT OR Apache-2.0"
     },
+    "clap_lex 0.6.0": {
+      "name": "clap_lex",
+      "version": "0.6.0",
+      "repository": {
+        "Http": {
+          "url": "https://crates.io/api/v1/crates/clap_lex/0.6.0/download",
+          "sha256": "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
+        }
+      },
+      "targets": [
+        {
+          "Library": {
+            "crate_name": "clap_lex",
+            "crate_root": "src/lib.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        }
+      ],
+      "library_target_name": "clap_lex",
+      "common_attrs": {
+        "compile_data_glob": [
+          "**"
+        ],
+        "edition": "2021",
+        "version": "0.6.0"
+      },
+      "license": "MIT OR Apache-2.0"
+    },
     "codespan-reporting 0.11.1": {
       "name": "codespan-reporting",
       "version": "0.11.1",
@@ -2063,6 +2516,36 @@
       },
       "license": "Apache-2.0"
     },
+    "colorchoice 1.0.0": {
+      "name": "colorchoice",
+      "version": "1.0.0",
+      "repository": {
+        "Http": {
+          "url": "https://crates.io/api/v1/crates/colorchoice/1.0.0/download",
+          "sha256": "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+        }
+      },
+      "targets": [
+        {
+          "Library": {
+            "crate_name": "colorchoice",
+            "crate_root": "src/lib.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        }
+      ],
+      "library_target_name": "colorchoice",
+      "common_attrs": {
+        "compile_data_glob": [
+          "**"
+        ],
+        "edition": "2021",
+        "version": "1.0.0"
+      },
+      "license": "MIT OR Apache-2.0"
+    },
     "compile_with_bazel 0.0.0": {
       "name": "compile_with_bazel",
       "version": "0.0.0",
@@ -2098,6 +2581,10 @@
               "target": "bitflags"
             },
             {
+              "id": "clap 4.4.18",
+              "target": "clap"
+            },
+            {
               "id": "cxx 1.0.97",
               "target": "cxx"
             },
@@ -2805,6 +3292,10 @@
         "deps": {
           "common": [
             {
+              "id": "arrayvec 0.7.4",
+              "target": "arrayvec"
+            },
+            {
               "id": "bitflags 1.3.2",
               "target": "bitflags"
             },
@@ -3554,6 +4045,42 @@
       },
       "license": "MIT OR Apache-2.0"
     },
+    "heck 0.4.1": {
+      "name": "heck",
+      "version": "0.4.1",
+      "repository": {
+        "Http": {
+          "url": "https://crates.io/api/v1/crates/heck/0.4.1/download",
+          "sha256": "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+        }
+      },
+      "targets": [
+        {
+          "Library": {
+            "crate_name": "heck",
+            "crate_root": "src/lib.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        }
+      ],
+      "library_target_name": "heck",
+      "common_attrs": {
+        "compile_data_glob": [
+          "**"
+        ],
+        "crate_features": {
+          "common": [
+            "default"
+          ],
+          "selects": {}
+        },
+        "edition": "2018",
+        "version": "0.4.1"
+      },
+      "license": "MIT OR Apache-2.0"
+    },
     "hermit-abi 0.1.19": {
       "name": "hermit-abi",
       "version": "0.1.19",
@@ -5059,13 +5586,13 @@
       },
       "license": "MIT OR Apache-2.0"
     },
-    "os_str_bytes 6.5.1": {
+    "os_str_bytes 6.6.1": {
       "name": "os_str_bytes",
-      "version": "6.5.1",
+      "version": "6.6.1",
       "repository": {
         "Http": {
-          "url": "https://crates.io/api/v1/crates/os_str_bytes/6.5.1/download",
-          "sha256": "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac"
+          "url": "https://crates.io/api/v1/crates/os_str_bytes/6.6.1/download",
+          "sha256": "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
         }
       },
       "targets": [
@@ -5091,7 +5618,7 @@
           "selects": {}
         },
         "edition": "2021",
-        "version": "6.5.1"
+        "version": "6.6.1"
       },
       "license": "MIT OR Apache-2.0"
     },
@@ -7928,6 +8455,42 @@
       },
       "license": "MIT/Apache-2.0"
     },
+    "utf8parse 0.2.1": {
+      "name": "utf8parse",
+      "version": "0.2.1",
+      "repository": {
+        "Http": {
+          "url": "https://crates.io/api/v1/crates/utf8parse/0.2.1/download",
+          "sha256": "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+        }
+      },
+      "targets": [
+        {
+          "Library": {
+            "crate_name": "utf8parse",
+            "crate_root": "src/lib.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        }
+      ],
+      "library_target_name": "utf8parse",
+      "common_attrs": {
+        "compile_data_glob": [
+          "**"
+        ],
+        "crate_features": {
+          "common": [
+            "default"
+          ],
+          "selects": {}
+        },
+        "edition": "2018",
+        "version": "0.2.1"
+      },
+      "license": "Apache-2.0 OR MIT"
+    },
     "uuid 1.4.0": {
       "name": "uuid",
       "version": "1.4.0",
@@ -8437,6 +9000,45 @@
       },
       "license": "MIT OR Apache-2.0"
     },
+    "windows-sys 0.52.0": {
+      "name": "windows-sys",
+      "version": "0.52.0",
+      "repository": {
+        "Http": {
+          "url": "https://crates.io/api/v1/crates/windows-sys/0.52.0/download",
+          "sha256": "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+        }
+      },
+      "targets": [
+        {
+          "Library": {
+            "crate_name": "windows_sys",
+            "crate_root": "src/lib.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        }
+      ],
+      "library_target_name": "windows_sys",
+      "common_attrs": {
+        "compile_data_glob": [
+          "**"
+        ],
+        "deps": {
+          "common": [
+            {
+              "id": "windows-targets 0.52.0",
+              "target": "windows_targets"
+            }
+          ],
+          "selects": {}
+        },
+        "edition": "2021",
+        "version": "0.52.0"
+      },
+      "license": "MIT OR Apache-2.0"
+    },
     "windows-targets 0.48.1": {
       "name": "windows-targets",
       "version": "0.48.1",
@@ -8514,6 +9116,83 @@
       },
       "license": "MIT OR Apache-2.0"
     },
+    "windows-targets 0.52.0": {
+      "name": "windows-targets",
+      "version": "0.52.0",
+      "repository": {
+        "Http": {
+          "url": "https://crates.io/api/v1/crates/windows-targets/0.52.0/download",
+          "sha256": "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
+        }
+      },
+      "targets": [
+        {
+          "Library": {
+            "crate_name": "windows_targets",
+            "crate_root": "src/lib.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        }
+      ],
+      "library_target_name": "windows_targets",
+      "common_attrs": {
+        "compile_data_glob": [
+          "**"
+        ],
+        "deps": {
+          "common": [],
+          "selects": {
+            "aarch64-pc-windows-gnullvm": [
+              {
+                "id": "windows_aarch64_gnullvm 0.52.0",
+                "target": "windows_aarch64_gnullvm"
+              }
+            ],
+            "cfg(all(target_arch = \"aarch64\", target_env = \"msvc\", not(windows_raw_dylib)))": [
+              {
+                "id": "windows_aarch64_msvc 0.52.0",
+                "target": "windows_aarch64_msvc"
+              }
+            ],
+            "cfg(all(target_arch = \"x86\", target_env = \"gnu\", not(windows_raw_dylib)))": [
+              {
+                "id": "windows_i686_gnu 0.52.0",
+                "target": "windows_i686_gnu"
+              }
+            ],
+            "cfg(all(target_arch = \"x86\", target_env = \"msvc\", not(windows_raw_dylib)))": [
+              {
+                "id": "windows_i686_msvc 0.52.0",
+                "target": "windows_i686_msvc"
+              }
+            ],
+            "cfg(all(target_arch = \"x86_64\", target_env = \"gnu\", not(target_abi = \"llvm\"), not(windows_raw_dylib)))": [
+              {
+                "id": "windows_x86_64_gnu 0.52.0",
+                "target": "windows_x86_64_gnu"
+              }
+            ],
+            "cfg(all(target_arch = \"x86_64\", target_env = \"msvc\", not(windows_raw_dylib)))": [
+              {
+                "id": "windows_x86_64_msvc 0.52.0",
+                "target": "windows_x86_64_msvc"
+              }
+            ],
+            "x86_64-pc-windows-gnullvm": [
+              {
+                "id": "windows_x86_64_gnullvm 0.52.0",
+                "target": "windows_x86_64_gnullvm"
+              }
+            ]
+          }
+        },
+        "edition": "2021",
+        "version": "0.52.0"
+      },
+      "license": "MIT OR Apache-2.0"
+    },
     "windows_aarch64_gnullvm 0.48.0": {
       "name": "windows_aarch64_gnullvm",
       "version": "0.48.0",
@@ -8567,6 +9246,59 @@
       },
       "license": "MIT OR Apache-2.0"
     },
+    "windows_aarch64_gnullvm 0.52.0": {
+      "name": "windows_aarch64_gnullvm",
+      "version": "0.52.0",
+      "repository": {
+        "Http": {
+          "url": "https://crates.io/api/v1/crates/windows_aarch64_gnullvm/0.52.0/download",
+          "sha256": "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
+        }
+      },
+      "targets": [
+        {
+          "Library": {
+            "crate_name": "windows_aarch64_gnullvm",
+            "crate_root": "src/lib.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        },
+        {
+          "BuildScript": {
+            "crate_name": "build_script_build",
+            "crate_root": "build.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        }
+      ],
+      "library_target_name": "windows_aarch64_gnullvm",
+      "common_attrs": {
+        "compile_data_glob": [
+          "**"
+        ],
+        "deps": {
+          "common": [
+            {
+              "id": "windows_aarch64_gnullvm 0.52.0",
+              "target": "build_script_build"
+            }
+          ],
+          "selects": {}
+        },
+        "edition": "2021",
+        "version": "0.52.0"
+      },
+      "build_script_attrs": {
+        "data_glob": [
+          "**"
+        ]
+      },
+      "license": "MIT OR Apache-2.0"
+    },
     "windows_aarch64_msvc 0.48.0": {
       "name": "windows_aarch64_msvc",
       "version": "0.48.0",
@@ -8620,6 +9352,59 @@
       },
       "license": "MIT OR Apache-2.0"
     },
+    "windows_aarch64_msvc 0.52.0": {
+      "name": "windows_aarch64_msvc",
+      "version": "0.52.0",
+      "repository": {
+        "Http": {
+          "url": "https://crates.io/api/v1/crates/windows_aarch64_msvc/0.52.0/download",
+          "sha256": "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
+        }
+      },
+      "targets": [
+        {
+          "Library": {
+            "crate_name": "windows_aarch64_msvc",
+            "crate_root": "src/lib.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        },
+        {
+          "BuildScript": {
+            "crate_name": "build_script_build",
+            "crate_root": "build.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        }
+      ],
+      "library_target_name": "windows_aarch64_msvc",
+      "common_attrs": {
+        "compile_data_glob": [
+          "**"
+        ],
+        "deps": {
+          "common": [
+            {
+              "id": "windows_aarch64_msvc 0.52.0",
+              "target": "build_script_build"
+            }
+          ],
+          "selects": {}
+        },
+        "edition": "2021",
+        "version": "0.52.0"
+      },
+      "build_script_attrs": {
+        "data_glob": [
+          "**"
+        ]
+      },
+      "license": "MIT OR Apache-2.0"
+    },
     "windows_i686_gnu 0.48.0": {
       "name": "windows_i686_gnu",
       "version": "0.48.0",
@@ -8673,6 +9458,59 @@
       },
       "license": "MIT OR Apache-2.0"
     },
+    "windows_i686_gnu 0.52.0": {
+      "name": "windows_i686_gnu",
+      "version": "0.52.0",
+      "repository": {
+        "Http": {
+          "url": "https://crates.io/api/v1/crates/windows_i686_gnu/0.52.0/download",
+          "sha256": "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
+        }
+      },
+      "targets": [
+        {
+          "Library": {
+            "crate_name": "windows_i686_gnu",
+            "crate_root": "src/lib.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        },
+        {
+          "BuildScript": {
+            "crate_name": "build_script_build",
+            "crate_root": "build.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        }
+      ],
+      "library_target_name": "windows_i686_gnu",
+      "common_attrs": {
+        "compile_data_glob": [
+          "**"
+        ],
+        "deps": {
+          "common": [
+            {
+              "id": "windows_i686_gnu 0.52.0",
+              "target": "build_script_build"
+            }
+          ],
+          "selects": {}
+        },
+        "edition": "2021",
+        "version": "0.52.0"
+      },
+      "build_script_attrs": {
+        "data_glob": [
+          "**"
+        ]
+      },
+      "license": "MIT OR Apache-2.0"
+    },
     "windows_i686_msvc 0.48.0": {
       "name": "windows_i686_msvc",
       "version": "0.48.0",
@@ -8726,6 +9564,59 @@
       },
       "license": "MIT OR Apache-2.0"
     },
+    "windows_i686_msvc 0.52.0": {
+      "name": "windows_i686_msvc",
+      "version": "0.52.0",
+      "repository": {
+        "Http": {
+          "url": "https://crates.io/api/v1/crates/windows_i686_msvc/0.52.0/download",
+          "sha256": "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
+        }
+      },
+      "targets": [
+        {
+          "Library": {
+            "crate_name": "windows_i686_msvc",
+            "crate_root": "src/lib.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        },
+        {
+          "BuildScript": {
+            "crate_name": "build_script_build",
+            "crate_root": "build.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        }
+      ],
+      "library_target_name": "windows_i686_msvc",
+      "common_attrs": {
+        "compile_data_glob": [
+          "**"
+        ],
+        "deps": {
+          "common": [
+            {
+              "id": "windows_i686_msvc 0.52.0",
+              "target": "build_script_build"
+            }
+          ],
+          "selects": {}
+        },
+        "edition": "2021",
+        "version": "0.52.0"
+      },
+      "build_script_attrs": {
+        "data_glob": [
+          "**"
+        ]
+      },
+      "license": "MIT OR Apache-2.0"
+    },
     "windows_x86_64_gnu 0.48.0": {
       "name": "windows_x86_64_gnu",
       "version": "0.48.0",
@@ -8779,6 +9670,59 @@
       },
       "license": "MIT OR Apache-2.0"
     },
+    "windows_x86_64_gnu 0.52.0": {
+      "name": "windows_x86_64_gnu",
+      "version": "0.52.0",
+      "repository": {
+        "Http": {
+          "url": "https://crates.io/api/v1/crates/windows_x86_64_gnu/0.52.0/download",
+          "sha256": "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
+        }
+      },
+      "targets": [
+        {
+          "Library": {
+            "crate_name": "windows_x86_64_gnu",
+            "crate_root": "src/lib.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        },
+        {
+          "BuildScript": {
+            "crate_name": "build_script_build",
+            "crate_root": "build.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        }
+      ],
+      "library_target_name": "windows_x86_64_gnu",
+      "common_attrs": {
+        "compile_data_glob": [
+          "**"
+        ],
+        "deps": {
+          "common": [
+            {
+              "id": "windows_x86_64_gnu 0.52.0",
+              "target": "build_script_build"
+            }
+          ],
+          "selects": {}
+        },
+        "edition": "2021",
+        "version": "0.52.0"
+      },
+      "build_script_attrs": {
+        "data_glob": [
+          "**"
+        ]
+      },
+      "license": "MIT OR Apache-2.0"
+    },
     "windows_x86_64_gnullvm 0.48.0": {
       "name": "windows_x86_64_gnullvm",
       "version": "0.48.0",
@@ -8832,6 +9776,59 @@
       },
       "license": "MIT OR Apache-2.0"
     },
+    "windows_x86_64_gnullvm 0.52.0": {
+      "name": "windows_x86_64_gnullvm",
+      "version": "0.52.0",
+      "repository": {
+        "Http": {
+          "url": "https://crates.io/api/v1/crates/windows_x86_64_gnullvm/0.52.0/download",
+          "sha256": "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
+        }
+      },
+      "targets": [
+        {
+          "Library": {
+            "crate_name": "windows_x86_64_gnullvm",
+            "crate_root": "src/lib.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        },
+        {
+          "BuildScript": {
+            "crate_name": "build_script_build",
+            "crate_root": "build.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        }
+      ],
+      "library_target_name": "windows_x86_64_gnullvm",
+      "common_attrs": {
+        "compile_data_glob": [
+          "**"
+        ],
+        "deps": {
+          "common": [
+            {
+              "id": "windows_x86_64_gnullvm 0.52.0",
+              "target": "build_script_build"
+            }
+          ],
+          "selects": {}
+        },
+        "edition": "2021",
+        "version": "0.52.0"
+      },
+      "build_script_attrs": {
+        "data_glob": [
+          "**"
+        ]
+      },
+      "license": "MIT OR Apache-2.0"
+    },
     "windows_x86_64_msvc 0.48.0": {
       "name": "windows_x86_64_msvc",
       "version": "0.48.0",
@@ -8884,6 +9881,59 @@
         ]
       },
       "license": "MIT OR Apache-2.0"
+    },
+    "windows_x86_64_msvc 0.52.0": {
+      "name": "windows_x86_64_msvc",
+      "version": "0.52.0",
+      "repository": {
+        "Http": {
+          "url": "https://crates.io/api/v1/crates/windows_x86_64_msvc/0.52.0/download",
+          "sha256": "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
+        }
+      },
+      "targets": [
+        {
+          "Library": {
+            "crate_name": "windows_x86_64_msvc",
+            "crate_root": "src/lib.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        },
+        {
+          "BuildScript": {
+            "crate_name": "build_script_build",
+            "crate_root": "build.rs",
+            "srcs": [
+              "**/*.rs"
+            ]
+          }
+        }
+      ],
+      "library_target_name": "windows_x86_64_msvc",
+      "common_attrs": {
+        "compile_data_glob": [
+          "**"
+        ],
+        "deps": {
+          "common": [
+            {
+              "id": "windows_x86_64_msvc 0.52.0",
+              "target": "build_script_build"
+            }
+          ],
+          "selects": {}
+        },
+        "edition": "2021",
+        "version": "0.52.0"
+      },
+      "build_script_attrs": {
+        "data_glob": [
+          "**"
+        ]
+      },
+      "license": "MIT OR Apache-2.0"
     }
   },
   "binary_crates": [],
diff --git a/Cargo.lock b/Cargo.lock
index 5fcc5eb..3829492 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -47,12 +47,54 @@
 ]
 
 [[package]]
+name = "anstream"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "utf8parse",
+]
+
+[[package]]
 name = "anstyle"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd"
 
 [[package]]
+name = "anstyle-parse"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
 name = "anyhow"
 version = "1.0.71"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -72,6 +114,12 @@
 ]
 
 [[package]]
+name = "arrayvec"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
+
+[[package]]
 name = "assert_cmd"
 version = "2.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -385,7 +433,7 @@
 dependencies = [
  "atty",
  "bitflags 1.3.2",
- "clap_lex",
+ "clap_lex 0.2.4",
  "indexmap",
  "once_cell",
  "strsim 0.10.0",
@@ -394,6 +442,40 @@
 ]
 
 [[package]]
+name = "clap"
+version = "4.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex 0.6.0",
+ "strsim 0.10.0",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.28",
+]
+
+[[package]]
 name = "clap_lex"
 version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -403,6 +485,12 @@
 ]
 
 [[package]]
+name = "clap_lex"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
+
+[[package]]
 name = "codespan-reporting"
 version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -413,12 +501,19 @@
 ]
 
 [[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
+[[package]]
 name = "compile_with_bazel"
 version = "0.0.0"
 dependencies = [
  "anyhow",
  "bindgen",
  "bitflags 1.3.2",
+ "clap 4.4.18",
  "cxx",
  "cxxbridge-macro",
  "futures",
@@ -524,7 +619,7 @@
 dependencies = [
  "errno-dragonfly",
  "libc",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -550,6 +645,7 @@
 name = "flatbuffers"
 version = "22.10.26"
 dependencies = [
+ "arrayvec",
  "bitflags 1.3.2",
  "rustc_version",
  "serde",
@@ -677,6 +773,12 @@
 ]
 
 [[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
 name = "hermit-abi"
 version = "0.1.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -731,7 +833,7 @@
 dependencies = [
  "hermit-abi 0.3.2",
  "libc",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -742,7 +844,7 @@
 dependencies = [
  "hermit-abi 0.3.2",
  "rustix 0.38.7",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -939,9 +1041,9 @@
 
 [[package]]
 name = "os_str_bytes"
-version = "6.5.1"
+version = "6.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac"
+checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
 
 [[package]]
 name = "owo-colors"
@@ -1129,7 +1231,7 @@
  "io-lifetimes",
  "libc",
  "linux-raw-sys 0.3.8",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -1142,7 +1244,7 @@
  "errno",
  "libc",
  "linux-raw-sys 0.4.5",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -1300,7 +1402,7 @@
  "fastrand",
  "redox_syscall",
  "rustix 0.37.23",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -1432,6 +1534,12 @@
 checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
 
 [[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[package]]
 name = "uuid"
 version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1521,7 +1629,16 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
 dependencies = [
- "windows-targets",
+ "windows-targets 0.48.1",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.0",
 ]
 
 [[package]]
@@ -1530,13 +1647,28 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f"
 dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "windows_aarch64_gnullvm 0.48.0",
+ "windows_aarch64_msvc 0.48.0",
+ "windows_i686_gnu 0.48.0",
+ "windows_i686_msvc 0.48.0",
+ "windows_x86_64_gnu 0.48.0",
+ "windows_x86_64_gnullvm 0.48.0",
+ "windows_x86_64_msvc 0.48.0",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.0",
+ "windows_aarch64_msvc 0.52.0",
+ "windows_i686_gnu 0.52.0",
+ "windows_i686_msvc 0.52.0",
+ "windows_x86_64_gnu 0.52.0",
+ "windows_x86_64_gnullvm 0.52.0",
+ "windows_x86_64_msvc 0.52.0",
 ]
 
 [[package]]
@@ -1546,37 +1678,79 @@
 checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
 
 [[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
+
+[[package]]
 name = "windows_aarch64_msvc"
 version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
 
 [[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
+
+[[package]]
 name = "windows_i686_gnu"
 version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
 
 [[package]]
+name = "windows_i686_gnu"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
+
+[[package]]
 name = "windows_i686_msvc"
 version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
 
 [[package]]
+name = "windows_i686_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
+
+[[package]]
 name = "windows_x86_64_gnu"
 version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
 
 [[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
+
+[[package]]
 name = "windows_x86_64_gnullvm"
 version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
 
 [[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
+
+[[package]]
 name = "windows_x86_64_msvc"
 version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
diff --git a/Cargo.toml b/Cargo.toml
index 8d07945..993491a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,6 +18,7 @@
 ]
 
 [dependencies]
+clap = { version = "4.4", features = ["derive", "string"] }
 cxx = "1.0"
 cxxbridge-macro = "1.0"
 uuid = "1.0"
diff --git a/WORKSPACE b/WORKSPACE
index 7809594..82a0dd4 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -31,15 +31,17 @@
 
 http_archive(
     name = "aspect_bazel_lib",
-    sha256 = "80897b673c2b506d21f861ae316689aa8abcc3e56947580a41bf9e68ff13af58",
-    strip_prefix = "bazel-lib-1.27.1",
-    url = "https://github.com/aspect-build/bazel-lib/releases/download/v1.27.1/bazel-lib-v1.27.1.tar.gz",
+    sha256 = "979667bb7276ee8fcf2c114c9be9932b9a3052a64a647e0dcaacfb9c0016f0a3",
+    strip_prefix = "bazel-lib-2.4.1",
+    url = "https://github.com/aspect-build/bazel-lib/releases/download/v2.4.1/bazel-lib-v2.4.1.tar.gz",
 )
 
-load("@aspect_bazel_lib//lib:repositories.bzl", "aspect_bazel_lib_dependencies", "register_jq_toolchains")
+load("@aspect_bazel_lib//lib:repositories.bzl", "aspect_bazel_lib_dependencies", "aspect_bazel_lib_register_toolchains", "register_jq_toolchains")
 
 aspect_bazel_lib_dependencies()
 
+aspect_bazel_lib_register_toolchains()
+
 register_jq_toolchains()
 
 http_archive(
@@ -575,6 +577,23 @@
 )
 
 http_archive(
+    name = "ffmpeg",
+    build_file_content = """
+load("@bazel_skylib//rules:native_binary.bzl", "native_binary")
+
+native_binary(
+  name = "ffmpeg",
+  src = "ffmpeg-6.0.1-amd64-static/ffmpeg",
+  out = "ffmpeg",
+  visibility = ["//visibility:public"],
+  target_compatible_with = ["@platforms//cpu:x86_64", "@platforms//os:linux"],
+)
+    """,
+    sha256 = "28268bf402f1083833ea269331587f60a242848880073be8016501d864bd07a5",
+    url = "https://www.johnvansickle.com/ffmpeg/old-releases/ffmpeg-6.0.1-amd64-static.tar.xz",
+)
+
+http_archive(
     name = "apache2",
     build_file = "@//debian:apache2.BUILD",
     sha256 = "98b0ad6d911751ba0aa486429e6278f995e7bbabd928c7d3d44c888fa2bf371b",
@@ -853,25 +872,28 @@
 
 http_archive(
     name = "aspect_rules_js",
-    sha256 = "9fadde0ae6e0101755b8aedabf7d80b166491a8de297c60f6a5179cd0d0fea58",
-    strip_prefix = "rules_js-1.20.0",
-    url = "https://github.com/aspect-build/rules_js/releases/download/v1.20.0/rules_js-v1.20.0.tar.gz",
+    sha256 = "630a71aba66c4023a5b16ab3efafaeed8b1a2865ccd168a34611eb73876b3fc4",
+    strip_prefix = "rules_js-1.37.1",
+    url = "https://github.com/aspect-build/rules_js/releases/download/v1.37.1/rules_js-v1.37.1.tar.gz",
 )
 
-load("@aspect_rules_js//npm:npm_import.bzl", "npm_translate_lock", "pnpm_repository")
-
-pnpm_repository(name = "pnpm")
-
 load("@aspect_rules_js//js:repositories.bzl", "rules_js_dependencies")
 
 rules_js_dependencies()
 
+load("@bazel_features//:deps.bzl", "bazel_features_deps")
+
+bazel_features_deps()
+
+load("@aspect_rules_js//npm:npm_import.bzl", "npm_translate_lock", "pnpm_repository")
+
+pnpm_repository(name = "pnpm")
+
 http_archive(
     name = "aspect_rules_esbuild",
-    sha256 = "b98cde83e9e6a006d8300e88e2f09da56b5a6c18166465a224cfe36bdcbc03e0",
-    strip_prefix = "aspect-build-rules_esbuild-110b94c",
-    type = "tar.gz",
-    url = "https://github.com/aspect-build/rules_esbuild/tarball/110b94c7f16f328a0eab8aa0b862030055b86564",
+    sha256 = "999349afef62875301f45ec8515189ceaf2e85b1e67a17e2d28b95b30e1d6c0b",
+    strip_prefix = "rules_esbuild-0.18.0",
+    url = "https://github.com/aspect-build/rules_esbuild/releases/download/v0.18.0/rules_esbuild-v0.18.0.tar.gz",
 )
 
 load("@aspect_rules_esbuild//esbuild:dependencies.bzl", "rules_esbuild_dependencies")
@@ -916,7 +938,7 @@
     verify_node_modules_ignored = "//:.bazelignore",
 )
 
-load("@aspect_rules_esbuild//esbuild:repositories.bzl", "esbuild_register_toolchains", LATEST_ESBUILD_VERSION = "LATEST_VERSION")
+load("@aspect_rules_esbuild//esbuild:repositories.bzl", "LATEST_ESBUILD_VERSION", "esbuild_register_toolchains")
 
 esbuild_register_toolchains(
     name = "esbuild",
@@ -931,65 +953,34 @@
     patches = [
         "//third_party:rules_rollup/0001-Fix-resolving-files.patch",
     ],
-    sha256 = "4c43d20ce377b93cd43a3553e6159a17b85ce80c36a564b55051c2320d32b777",
-    strip_prefix = "rules_rollup-0.13.1",
-    url = "https://github.com/aspect-build/rules_rollup/releases/download/v0.13.1/rules_rollup-v0.13.1.tar.gz",
+    sha256 = "a0433a0b0206a45d362749d71bc1e4e0dacf5ca2a572b059328f9753392bca80",
+    strip_prefix = "rules_rollup-1.0.0",
+    url = "https://github.com/aspect-build/rules_rollup/releases/download/v1.0.0/rules_rollup-v1.0.0.tar.gz",
 )
 
-load("@aspect_rules_rollup//rollup:dependencies.bzl", "rules_rollup_dependencies")
-
-# Fetches the rules_rollup dependencies.
-# If you want to have a different version of some dependency,
-# you should fetch it *before* calling this.
-# Alternatively, you can skip calling this function, so long as you've
-# already fetched all the dependencies.
-rules_rollup_dependencies()
-
-load("@aspect_rules_rollup//rollup:repositories.bzl", "rollup_repositories")
-
-rollup_repositories(name = "rollup")
-
-load("@rollup//:npm_repositories.bzl", rollup_npm_repositories = "npm_repositories")
-
-rollup_npm_repositories()
-
 http_archive(
     name = "aspect_rules_terser",
-    sha256 = "918e7ac036eca1402cae4d4ddba75ecdcdd886ac35bc0624d9f1ebc7527e369b",
-    strip_prefix = "rules_terser-0.13.0",
-    url = "https://github.com/aspect-build/rules_terser/archive/refs/tags/v0.13.0.tar.gz",
+    sha256 = "8424b4c064d0e490e5b6f215b993712ef641b77e03b68fdc64221edf48d14add",
+    strip_prefix = "rules_terser-1.0.0",
+    url = "https://github.com/aspect-build/rules_terser/releases/download/v1.0.0/rules_terser-v1.0.0.tar.gz",
 )
 
 load("@aspect_rules_terser//terser:dependencies.bzl", "rules_terser_dependencies")
 
 rules_terser_dependencies()
 
-# Fetch and register a nodejs interpreter, if you haven't already
-
-nodejs_register_toolchains(
-    name = "node",
-    node_version = DEFAULT_NODE_VERSION,
-)
-
-# Fetch and register the terser tool
-load("@aspect_rules_terser//terser:repositories.bzl", "terser_repositories")
-
-terser_repositories(name = "terser")
-
-load("@terser//:npm_repositories.bzl", terser_npm_repositories = "npm_repositories")
-
-terser_npm_repositories()
-
 http_archive(
     name = "aspect_rules_ts",
-    sha256 = "db77d904284d21121ae63dbaaadfd8c75ff6d21ad229f92038b415c1ad5019cc",
-    strip_prefix = "rules_ts-1.3.0",
-    url = "https://github.com/aspect-build/rules_ts/releases/download/v1.3.0/rules_ts-v1.3.0.tar.gz",
+    sha256 = "6ad28b5bac2bb5a74e737925fbc3f62ce1edabe5a48d61a9980c491ef4cedfb7",
+    strip_prefix = "rules_ts-2.1.1",
+    url = "https://github.com/aspect-build/rules_ts/releases/download/v2.1.1/rules_ts-v2.1.1.tar.gz",
 )
 
 load("@aspect_rules_ts//ts:repositories.bzl", "rules_ts_dependencies")
 
-rules_ts_dependencies(ts_version_from = "//:package.json")
+rules_ts_dependencies(
+    ts_version_from = "//:package.json",
+)
 
 load("@npm//:repositories.bzl", "npm_repositories")
 
@@ -997,11 +988,10 @@
 
 http_archive(
     name = "aspect_rules_cypress",
-    sha256 = "06d70a2960108607d2e70f9bc6863af6b82317fdfcf7a5a30fd226a5abc46782",
-    strip_prefix = "aspect-build-rules_cypress-3db1b74",
-    type = "tar.gz",
+    sha256 = "76947778d8e855eee3c15931e1fcdc1c2a25d56d6c0edd110b2227c05b794d08",
+    strip_prefix = "rules_cypress-0.3.2",
     urls = [
-        "https://github.com/aspect-build/rules_cypress/tarball/3db1b74818ac4ce1b9d489a6e0065b36c1076761",
+        "https://github.com/aspect-build/rules_cypress/archive/refs/tags/v0.3.2.tar.gz",
     ],
 )
 
@@ -1433,6 +1423,14 @@
 )
 
 http_archive(
+    name = "com_github_nghttp2_nghttp2",
+    build_file = "//debian:BUILD.nghttp2.bazel",
+    sha256 = "7da19947b33a07ddcf97b9791331bfee8a8545e6b394275a9971f43cae9d636b",
+    strip_prefix = "nghttp2-1.58.0",
+    url = "https://github.com/nghttp2/nghttp2/archive/refs/tags/v1.58.0.tar.gz",
+)
+
+http_archive(
     # No official name exists.  Names used in our external dependencies include
     # zlib, madler_zlib, com_github_madler_zlib.
     name = "zlib",
@@ -1521,8 +1519,8 @@
 
 # This contains the *compiled* foxglove studio. This can be reproduced by:
 # 1. Cloning https://github.com/foxglove/studio
-# 2. Building the code (yarn web:build:prod)
-# 3. tar'ing the web/.webpack folder
+# 2. Building the code (yarn install; yarn web:build:prod)
+# 3. tar'ing the web/.webpack folder (e.g., tar czvf foxglove-1456f4a4cb6f4c6c7e50e020ba9918dba9e04b96.tar.gz --directory=web/.webpack/ .)
 # These files can be hosted locally to provide an offline foxglove server.
 # Foxglove may be served on any port and may be nested at a subpath
 # (e.g., at hostname:8000/foxglove behind a proxy).
@@ -1534,9 +1532,9 @@
     srcs = glob(["**"]),
     visibility = ["//visibility:public"],
 )""",
-    sha256 = "68513024efb60dcdfc9b130d3e0466d194a8599fbd85f8e99d01c148d03e5887",
+    sha256 = "d02f4ca629e6dcf2b65557a0353871ce0025e70715214de4e6ec7e9f862de420",
     url =
-        "https://software.frc971.org/Build-Dependencies/foxglove-9857d637e90dfeecd63ad47fa760046791f8d43c.tar.gz",
+        "https://software.frc971.org/Build-Dependencies/foxglove-1456f4a4cb6f4c6c7e50e020ba9918dba9e04b96.tar.gz",
 )
 
 #
@@ -1577,6 +1575,20 @@
     url = "https://software.frc971.org/Build-Dependencies/orin_large_gs_apriltag.bfbs",
 )
 
+http_file(
+    name = "orin_capture_24_04",
+    downloaded_file_path = "orin_capture_24_04.bfbs",
+    sha256 = "719edb1d1394c13c1b55d02cf35c277e1d4c2111f4eb4220b28addc08634488a",
+    url = "https://software.frc971.org/Build-Dependencies/orin-capture-24-04-2024.02.14.bfbs",
+)
+
+http_file(
+    name = "orin_capture_24_04_side",
+    downloaded_file_path = "orin_capture_24_04_side.bfbs",
+    sha256 = "4747cc98f8794d6570cb12a3171d7984e358581914a28b43fb6bb8b9bd7a10ac",
+    url = "https://software.frc971.org/Build-Dependencies/orin-capture-24-04-side-2024.02.17.bfbs",
+)
+
 http_archive(
     name = "libedgetpu",
     build_file = "//third_party:libedgetpu/libedgetpu.BUILD",
@@ -1633,10 +1645,11 @@
 # https://github.com/hedronvision/bazel-compile-commands-extractor
 http_archive(
     name = "hedron_compile_commands",
-    strip_prefix = "bazel-compile-commands-extractor-daae6f40adfa5fdb7c89684cbe4d88b691c63b2d",
 
     # Replace the commit hash (daae6f40adfa5fdb7c89684cbe4d88b691c63b2d) in both places (below) with the latest (https://github.com/hedronvision/bazel-compile-commands-extractor/commits/main), rather than using the stale one here.
     # Even better, set up Renovate and let it do the work for you (see "Suggestion: Updates" in the README).
+    sha256 = "43451a32bf271e7ba4635a07f7996d535501f066c0fe8feab04fb0c91dd5986e",
+    strip_prefix = "bazel-compile-commands-extractor-daae6f40adfa5fdb7c89684cbe4d88b691c63b2d",
     url = "https://github.com/hedronvision/bazel-compile-commands-extractor/archive/daae6f40adfa5fdb7c89684cbe4d88b691c63b2d.tar.gz",
     # When you first run this tool, it'll recommend a sha256 hash to put here with a message like: "DEBUG: Rule 'hedron_compile_commands' indicated that a canonical reproducible form can be obtained by modifying arguments sha256 = ..."
 )
diff --git a/aos/BUILD b/aos/BUILD
index 4d041ca..67e5b29 100644
--- a/aos/BUILD
+++ b/aos/BUILD
@@ -1,9 +1,9 @@
-load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library", "flatbuffer_py_library", "flatbuffer_rust_library")
+load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library", "flatbuffer_py_library")
 load("//aos/flatbuffers:generate.bzl", "static_flatbuffer")
 load("@com_github_google_flatbuffers//:typescript.bzl", "flatbuffer_ts_library")
-load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
 load("//aos:flatbuffers.bzl", "cc_static_flatbuffer")
 load("//tools/build_rules:autocxx.bzl", "autocxx_library")
+load("//tools/rust:defs.bzl", "flatbuffer_rust_library", "rust_library")
 
 exports_files(["aos_dump_autocomplete.sh"])
 
@@ -184,19 +184,36 @@
     ],
 )
 
+cc_library(
+    name = "init_for_rust",
+    srcs = [
+        "init_for_rust.cc",
+    ],
+    hdrs = [
+        "init_for_rust.h",
+    ],
+    deps = [
+        ":for_rust",
+        ":init",
+        "//aos/logging",
+        "@com_github_gflags_gflags//:gflags",
+        "@crate_index//:cxx_cc",
+    ],
+)
+
 autocxx_library(
     name = "init_rs",
     srcs = ["init.rs"],
     crate_name = "aos_init",
     libs = [
-        ":init",
+        ":init_for_rust",
     ],
     override_cc_toolchain = "@llvm_toolchain//:cc-clang-x86_64-linux",
-    target_compatible_with = select({
-        "//conditions:default": ["//tools/platforms/rust:has_support"],
-        "//tools:has_msan": ["@platforms//:incompatible"],
-    }),
     visibility = ["//visibility:public"],
+    deps = [
+        "@crate_index//:clap",
+        "@crate_index//:env_logger",
+    ],
 )
 
 autocxx_library(
@@ -204,17 +221,15 @@
     testonly = True,
     srcs = ["test_init.rs"],
     crate_name = "aos_test_init",
+    gen_docs = False,
     libs = [
         "//aos/testing:tmpdir",
     ],
     override_cc_toolchain = "@llvm_toolchain//:cc-clang-x86_64-linux",
-    target_compatible_with = select({
-        "//conditions:default": ["//tools/platforms/rust:has_support"],
-        "//tools:has_msan": ["@platforms//:incompatible"],
-    }),
     visibility = ["//visibility:public"],
     deps = [
         ":init_rs",
+        "@crate_index//:env_logger",
     ],
 )
 
@@ -239,6 +254,7 @@
     srcs = ["configuration.fbs"],
     target_compatible_with = ["@platforms//os:linux"],
     visibility = ["//visibility:public"],
+    deps = ["//aos/flatbuffers/reflection:reflection_fbs"],
 )
 
 cc_static_flatbuffer(
@@ -274,10 +290,6 @@
     name = "configuration_rust_fbs",
     srcs = ["configuration.fbs"],
     crate_name = "aos_configuration_fbs",
-    target_compatible_with = select({
-        "//conditions:default": ["//tools/platforms/rust:has_support"],
-        "//tools:has_msan": ["@platforms//:incompatible"],
-    }),
     visibility = ["//visibility:public"],
 )
 
@@ -331,10 +343,9 @@
         ":configuration_fbs",
     ],
     override_cc_toolchain = "@llvm_toolchain//:cc-clang-x86_64-linux",
-    target_compatible_with = select({
-        "//conditions:default": ["//tools/platforms/rust:has_support"],
-        "//tools:has_msan": ["@platforms//:incompatible"],
-    }),
+    test_data = [
+        "//aos/testdata:test_configs",
+    ],
     visibility = ["//visibility:public"],
     deps = [
         ":configuration_rust_fbs",
@@ -343,17 +354,6 @@
     ],
 )
 
-rust_test(
-    name = "configuration_rs_test",
-    crate = ":configuration_rs",
-    data = [
-        "//aos/testdata:test_configs",
-    ],
-    # TODO: Make Rust play happy with pic vs nopic. Details at:
-    # https://github.com/bazelbuild/rules_rust/issues/118
-    rustc_flags = ["-Crelocation-model=static"],
-)
-
 flatbuffer_ts_library(
     name = "json_to_flatbuffer_fbs_ts",
     srcs = ["json_to_flatbuffer.fbs"],
@@ -373,10 +373,6 @@
     name = "json_to_flatbuffer_rust_fbs",
     srcs = ["json_to_flatbuffer.fbs"],
     crate_name = "aos_json_to_flatbuffer_fbs",
-    target_compatible_with = select({
-        "//conditions:default": ["//tools/platforms/rust:has_support"],
-        "//tools:has_msan": ["@platforms//:incompatible"],
-    }),
     visibility = ["//aos:__subpackages__"],
 )
 
@@ -517,24 +513,15 @@
     name = "flatbuffers_rs",
     srcs = ["flatbuffers.rs"],
     crate_name = "aos_flatbuffers",
-    target_compatible_with = select({
-        "//conditions:default": ["//tools/platforms/rust:has_support"],
-        "//tools:has_msan": ["@platforms//:incompatible"],
-    }),
+    test_deps = [
+        ":json_to_flatbuffer_rust_fbs",
+    ],
     visibility = ["//visibility:public"],
     deps = [
         "@com_github_google_flatbuffers//rust",
     ],
 )
 
-rust_test(
-    name = "flatbuffers_rs_test",
-    crate = ":flatbuffers_rs",
-    deps = [
-        ":json_to_flatbuffer_rust_fbs",
-    ],
-)
-
 cc_test(
     name = "configuration_test",
     srcs = [
@@ -765,10 +752,6 @@
     rs_deps = [
         "@crate_index//:uuid",
     ],
-    target_compatible_with = select({
-        "//conditions:default": ["//tools/platforms/rust:has_support"],
-        "//tools:has_msan": ["@platforms//:incompatible"],
-    }),
     visibility = ["//visibility:public"],
 )
 
@@ -825,3 +808,19 @@
         "//aos/testing:tmpdir",
     ],
 )
+
+rust_library(
+    name = "aos_rs",
+    srcs = ["aos.rs"],
+    crate_name = "aos",
+    visibility = ["//visibility:public"],
+    deps = [
+        ":configuration_rs",
+        ":flatbuffers_rs",
+        ":init_rs",
+        ":uuid_rs",
+        "//aos/events:event_loop_runtime",
+        "//aos/events:shm_event_loop_rs",
+        "//aos/events:simulated_event_loop_rs",
+    ],
+)
diff --git a/aos/aos.rs b/aos/aos.rs
new file mode 100644
index 0000000..3a13c4d
--- /dev/null
+++ b/aos/aos.rs
@@ -0,0 +1,14 @@
+//! All of AOS into a single, easy to use library.
+
+pub use aos_configuration as configuration;
+pub use aos_init as init;
+pub use aos_uuid as uuid;
+
+pub use aos_flatbuffers as flatbuffers;
+
+/// The essentials for working with the AOS event loop.
+pub mod events {
+    pub use aos_events_event_loop_runtime as event_loop_runtime;
+    pub use aos_events_shm_event_loop as shm_event_loop;
+    pub use aos_events_simulated_event_loop as simulated_event_loop;
+}
diff --git a/aos/configuration.fbs b/aos/configuration.fbs
index 0b42f5b..b2b34c1 100644
--- a/aos/configuration.fbs
+++ b/aos/configuration.fbs
@@ -169,6 +169,10 @@
   // If set, this is the memory limit to enforce in bytes for the application
   // (and it's children)
   memory_limit:uint64 = 0 (id: 8);
+
+  // If set, this is the number of nanoseconds the application has to stop. If the application
+  // doesn't stop within the specified time, then it is killed.
+  stop_time:int64 = 1000000000 (id: 9);
 }
 
 // Per node data and connection information.
diff --git a/aos/events/BUILD b/aos/events/BUILD
index 9910fc6..e82fdde 100644
--- a/aos/events/BUILD
+++ b/aos/events/BUILD
@@ -1,10 +1,9 @@
-load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_rust_library")
 load("@com_github_google_flatbuffers//:typescript.bzl", "flatbuffer_ts_library")
 load("//aos/flatbuffers:generate.bzl", "static_flatbuffer")
 load("//aos:flatbuffers.bzl", "cc_static_flatbuffer")
 load("//aos:config.bzl", "aos_config")
 load("//tools/build_rules:autocxx.bzl", "autocxx_library")
-load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_doc", "rust_doc_test", "rust_test")
+load("//tools/rust:defs.bzl", "flatbuffer_rust_library", "rust_binary", "rust_library", "rust_test")
 
 package(default_visibility = ["//visibility:public"])
 
@@ -46,19 +45,11 @@
 flatbuffer_rust_library(
     name = "ping_rust_fbs",
     srcs = ["ping.fbs"],
-    target_compatible_with = select({
-        "//conditions:default": ["//tools/platforms/rust:has_support"],
-        "//tools:has_msan": ["@platforms//:incompatible"],
-    }),
 )
 
 flatbuffer_rust_library(
     name = "pong_rust_fbs",
     srcs = ["pong.fbs"],
-    target_compatible_with = select({
-        "//conditions:default": ["//tools/platforms/rust:has_support"],
-        "//tools:has_msan": ["@platforms//:incompatible"],
-    }),
 )
 
 static_flatbuffer(
@@ -155,6 +146,10 @@
     name = "event_loop_runtime",
     srcs = ["event_loop_runtime.rs"],
     crate_name = "aos_events_event_loop_runtime",
+    doctest_deps = [
+        ":pong_rust_fbs",
+    ],
+    gen_tests = False,
     libs = [
         ":event_loop_runtime_cc",
     ],
@@ -165,10 +160,6 @@
         "@crate_index//:futures",
         "@crate_index//:thiserror",
     ],
-    target_compatible_with = select({
-        "//conditions:default": ["//tools/platforms/rust:has_support"],
-        "//tools:has_msan": ["@platforms//:incompatible"],
-    }),
     visibility = ["//visibility:public"],
     deps = [
         "//aos:configuration_rs",
@@ -176,37 +167,19 @@
     ],
 )
 
-rust_doc(
-    name = "event_loop_runtime_doc",
-    crate = ":event_loop_runtime",
-)
-
-rust_doc_test(
-    name = "event_loop_runtime_doc_test",
-    crate = ":event_loop_runtime",
-    target_compatible_with = ["@platforms//cpu:x86_64"],
-    deps = [
-        ":pong_rust_fbs",
-    ],
-)
-
 autocxx_library(
     name = "event_loop_runtime_test_lib_rs",
     testonly = True,
     srcs = ["event_loop_runtime_test_lib.rs"],
+    gen_docs = False,
     libs = [
         ":event_loop",
     ],
-    override_cc_toolchain = "@llvm_toolchain//:cc-clang-x86_64-linux",
     rs_deps = [
         ":event_loop_runtime",
         ":ping_rust_fbs",
         ":pong_rust_fbs",
     ],
-    target_compatible_with = select({
-        "//conditions:default": ["//tools/platforms/rust:has_support"],
-        "//tools:has_msan": ["@platforms//:incompatible"],
-    }),
 )
 
 cc_test(
@@ -259,6 +232,28 @@
     ],
 )
 
+rust_test(
+    name = "pingpong_test_rs",
+    srcs = [
+        "pingpong_test.rs",
+    ],
+    data = [":pingpong_config"],
+    # TODO(adam.snaider): Remove later. For now we need this because when
+    # a rust test crashes inside of C++, the output gets captured which makes
+    # it pretty much impossible to figure out what happened.
+    env = {"RUST_TEST_NOCAPTURE": "1"},
+    deps = [
+        ":ping_lib_rs",
+        ":ping_rust_fbs",
+        ":pong_lib_rs",
+        ":pong_rust_fbs",
+        "//aos/testing:aos_rs",
+        "@com_github_google_flatbuffers//rust",
+        "@crate_index//:futures",
+        "@rules_rust//tools/runfiles",
+    ],
+)
+
 rust_binary(
     name = "ping_rs",
     srcs = [
@@ -267,22 +262,27 @@
     data = [
         ":pingpong_config",
     ],
-    rustc_flags = ["-Crelocation-model=static"],
-    target_compatible_with = select({
-        "//conditions:default": ["//tools/platforms/rust:has_support"],
-        "//tools:has_msan": ["@platforms//:incompatible"],
-    }),
     deps = [
-        ":event_loop_runtime",
+        ":ping_lib_rs",
+        ":shm_event_loop_rs",
+        "//aos:aos_rs",
+        "@crate_index//:clap",
+    ],
+)
+
+rust_library(
+    name = "ping_lib_rs",
+    srcs = [
+        "ping_lib.rs",
+    ],
+    crate_name = "ping_lib",
+    deps = [
         ":ping_rust_fbs",
         ":pong_rust_fbs",
-        ":shm_event_loop_rs",
-        "//aos:configuration_rs",
-        "//aos:configuration_rust_fbs",
-        "//aos:flatbuffers_rs",
-        "//aos:init_rs",
+        "//aos:aos_rs",
         "@com_github_google_flatbuffers//rust",
         "@crate_index//:futures",
+        "@crate_index//:log",
     ],
 )
 
@@ -294,22 +294,27 @@
     data = [
         ":pingpong_config",
     ],
-    rustc_flags = ["-Crelocation-model=static"],
-    target_compatible_with = select({
-        "//conditions:default": ["//tools/platforms/rust:has_support"],
-        "//tools:has_msan": ["@platforms//:incompatible"],
-    }),
     deps = [
-        ":event_loop_runtime",
+        ":pong_lib_rs",
+        ":shm_event_loop_rs",
+        "//aos:aos_rs",
+        "@crate_index//:clap",
+    ],
+)
+
+rust_library(
+    name = "pong_lib_rs",
+    srcs = [
+        "pong_lib.rs",
+    ],
+    crate_name = "pong_lib",
+    deps = [
         ":ping_rust_fbs",
         ":pong_rust_fbs",
-        ":shm_event_loop_rs",
-        "//aos:configuration_rs",
-        "//aos:configuration_rust_fbs",
-        "//aos:flatbuffers_rs",
-        "//aos:init_rs",
+        "//aos:aos_rs",
         "@com_github_google_flatbuffers//rust",
         "@crate_index//:futures",
+        "@crate_index//:log",
     ],
 )
 
@@ -599,6 +604,15 @@
         "//aos:flatbuffers_rs",
         "@crate_index//:futures",
     ],
+    test_data = [
+        ":multinode_pingpong_test_combined_config",
+    ],
+    test_deps = [
+        ":ping_rust_fbs",
+        "//aos:test_init_rs",
+        "@crate_index//:futures",
+        "@rules_rust//tools/runfiles",
+    ],
     visibility = ["//visibility:public"],
     deps = [
         ":event_loop_runtime",
@@ -607,27 +621,14 @@
     ],
 )
 
-rust_test(
-    name = "simulated_event_loop_rs_test",
-    crate = ":simulated_event_loop_rs",
-    data = [
-        ":multinode_pingpong_test_combined_config",
-    ],
-    # TODO: Make Rust play happy with pic vs nopic. Details at:
-    # https://github.com/bazelbuild/rules_rust/issues/118
-    rustc_flags = ["-Crelocation-model=static"],
-    deps = [
-        ":ping_rust_fbs",
-        "//aos:test_init_rs",
-        "@crate_index//:futures",
-        "@rules_rust//tools/runfiles",
-    ],
-)
-
 autocxx_library(
     name = "shm_event_loop_rs",
     srcs = ["shm_event_loop.rs"],
     crate_name = "aos_events_shm_event_loop",
+    doctest_deps = [
+        ":ping_rust_fbs",
+        ":pong_rust_fbs",
+    ],
     libs = [
         ":shm_event_loop",
         ":shm_event_loop_for_rust",
@@ -638,49 +639,17 @@
         "//aos:configuration_rust_fbs",
         "//aos:flatbuffers_rs",
     ],
-    target_compatible_with = select({
-        "//conditions:default": ["//tools/platforms/rust:has_support"],
-        "//tools:has_msan": ["@platforms//:incompatible"],
-    }),
-    visibility = ["//visibility:public"],
-    deps = [
-        ":event_loop_runtime",
-        "//aos:configuration_rs",
-    ],
-)
-
-rust_doc(
-    name = "shm_event_loop_rs_doc",
-    crate = ":shm_event_loop_rs",
-    target_compatible_with = ["@platforms//cpu:x86_64"],
-)
-
-rust_test(
-    name = "shm_event_loop_rs_test",
-    crate = ":shm_event_loop_rs",
-    data = [":pingpong_config"],
-    # TODO: Make Rust play happy with pic vs nopic. Details at:
-    # https://github.com/bazelbuild/rules_rust/issues/118
-    rustc_flags = ["-Crelocation-model=static"],
-    target_compatible_with = select({
-        "//conditions:default": ["//tools/platforms/rust:has_support"],
-        "//tools:has_msan": ["@platforms//:incompatible"],
-    }),
-    deps = [
+    test_data = [":pingpong_config"],
+    test_deps = [
         ":ping_rust_fbs",
         "//aos:test_init_rs",
         "@crate_index//:futures",
         "@rules_rust//tools/runfiles",
     ],
-)
-
-rust_doc_test(
-    name = "shm_event_loop_rs_doc_test",
-    crate = ":shm_event_loop_rs",
-    target_compatible_with = ["@platforms//cpu:x86_64"],
+    visibility = ["//visibility:public"],
     deps = [
-        ":ping_rust_fbs",
-        ":pong_rust_fbs",
+        ":event_loop_runtime",
+        "//aos:configuration_rs",
     ],
 )
 
diff --git a/aos/events/event_loop_runtime.cc b/aos/events/event_loop_runtime.cc
index e3d73c1..790fe9e 100644
--- a/aos/events/event_loop_runtime.cc
+++ b/aos/events/event_loop_runtime.cc
@@ -2,13 +2,15 @@
 
 namespace aos {
 
-OnRunForRust::OnRunForRust(EventLoopRuntime *runtime) : runtime_(runtime) {
+OnRunForRust::OnRunForRust(const EventLoopRuntime *runtime)
+    : runtime_(runtime) {
   ++runtime->child_count_;
 }
 OnRunForRust::~OnRunForRust() { --runtime_->child_count_; }
 bool OnRunForRust::is_running() const { return runtime_->is_running(); }
 
-std::unique_ptr<TimerForRust> TimerForRust::Make(EventLoopRuntime *runtime) {
+std::unique_ptr<TimerForRust> TimerForRust::Make(
+    const EventLoopRuntime *runtime) {
   auto handler = std::unique_ptr<TimerForRust>(new TimerForRust());
   TimerForRust *inner = handler.get();
   handler->timer_ = runtime->event_loop()->AddTimer([inner, runtime] {
diff --git a/aos/events/event_loop_runtime.h b/aos/events/event_loop_runtime.h
index 7cc551f..0505852 100644
--- a/aos/events/event_loop_runtime.h
+++ b/aos/events/event_loop_runtime.h
@@ -131,18 +131,18 @@
 
 class OnRunForRust {
  public:
-  OnRunForRust(EventLoopRuntime *runtime);
+  OnRunForRust(const EventLoopRuntime *runtime);
   ~OnRunForRust();
 
   bool is_running() const;
 
  private:
-  EventLoopRuntime *const runtime_;
+  const EventLoopRuntime *const runtime_;
 };
 
 class TimerForRust {
  public:
-  static std::unique_ptr<TimerForRust> Make(EventLoopRuntime *runtime);
+  static std::unique_ptr<TimerForRust> Make(const EventLoopRuntime *runtime);
 
   TimerForRust(const TimerForRust &) = delete;
   TimerForRust(TimerForRust &&) = delete;
@@ -177,7 +177,16 @@
 
 class EventLoopRuntime {
  public:
-  EventLoopRuntime(EventLoop *event_loop) : event_loop_(event_loop) {}
+  EventLoopRuntime(const EventLoop *event_loop)
+      // SAFETY: A &EventLoop in Rust becomes a const EventLoop*. While
+      // that's generally a reasonable convention, they are semantically
+      // different. In Rust, a &mut EventLoop is very restrictive as it enforces
+      // uniqueness of the reference. Additionally, a &EventLoop doesn't convey
+      // const-ness in the C++ sense. So to make the FFI boundary more
+      // ergonomic, we allow a &EventLoop passed from rust to be translated into
+      // an EventLoop* in C++. This is safe so long as EventLoop is !Sync and no
+      // &mut EventLoop references are constructed in Rust.
+      : event_loop_(const_cast<EventLoop *>(event_loop)) {}
   ~EventLoopRuntime() {
     // Do this first, because it may hold child objects.
     task_.reset();
@@ -185,9 +194,9 @@
         << ": Some child objects were not destroyed first";
   }
 
-  EventLoop *event_loop() { return event_loop_; }
+  EventLoop *event_loop() const { return event_loop_; }
 
-  void Spawn(std::unique_ptr<ApplicationFuture> task) {
+  void Spawn(std::unique_ptr<ApplicationFuture> task) const {
     CHECK(!task_) << ": May only call Spawn once";
     task_ = std::move(task);
     DoPoll();
@@ -219,30 +228,40 @@
 
   rust::Str name() const { return StringViewToRustStr(event_loop_->name()); }
 
-  WatcherForRust MakeWatcher(const Channel *channel) {
+  WatcherForRust MakeWatcher(const Channel *channel) const {
     event_loop_->MakeRawNoArgWatcher(channel,
                                      [this](const Context &) { DoPoll(); });
     return WatcherForRust(event_loop_->MakeRawFetcher(channel));
   }
 
-  SenderForRust MakeSender(const Channel *channel) {
+  SenderForRust MakeSender(const Channel *channel) const {
     return SenderForRust(event_loop_->MakeRawSender(channel));
   }
 
-  FetcherForRust MakeFetcher(const Channel *channel) {
+  FetcherForRust MakeFetcher(const Channel *channel) const {
     return FetcherForRust(event_loop_->MakeRawFetcher(channel));
   }
 
-  OnRunForRust MakeOnRun() { return OnRunForRust(this); }
+  OnRunForRust MakeOnRun() const { return OnRunForRust(this); }
 
-  std::unique_ptr<TimerForRust> AddTimer() { return TimerForRust::Make(this); }
+  std::unique_ptr<TimerForRust> AddTimer() const {
+    return TimerForRust::Make(this);
+  }
+
+  void SetRuntimeRealtimePriority(int priority) const {
+    event_loop_->SetRuntimeRealtimePriority(priority);
+  }
+
+  void SetRuntimeAffinity(const cpu_set_t &cpuset) const {
+    event_loop_->SetRuntimeAffinity(cpuset);
+  }
 
  private:
   friend class OnRunForRust;
   friend class TimerForRust;
 
   // Polls the top-level future once. This is what all the callbacks should do.
-  void DoPoll() {
+  void DoPoll() const {
     if (task_) {
       CHECK(task_->Poll()) << ": Rust panic, aborting";
     }
@@ -250,9 +269,20 @@
 
   EventLoop *const event_loop_;
 
-  std::unique_ptr<ApplicationFuture> task_;
+  // For Rust's EventLoopRuntime to be semantically equivelant to C++'s event
+  // loop, we need the ability to have shared references (&EventLoopRuntime) on
+  // the Rust side. Without that, the API would be overly restrictive to be
+  // usable. In order for the generated code to use &self references on methods,
+  // they need to be marked `const` on the C++ side. We use the `mutable`
+  // keyword to allow mutation through `const` methods.
+  //
+  // SAFETY:
+  //   * The event loop runtime must be `!Sync` in the Rust side (default).
+  //   * We can't expose exclusive references (&mut) to either of the mutable
+  //     fields on the Rust side from a shared reference (&).
+  mutable std::unique_ptr<ApplicationFuture> task_;
 
-  int child_count_ = 0;
+  mutable int child_count_ = 0;
 };
 
 }  // namespace aos
diff --git a/aos/events/event_loop_runtime.rs b/aos/events/event_loop_runtime.rs
index 35a4225..18d743e 100644
--- a/aos/events/event_loop_runtime.rs
+++ b/aos/events/event_loop_runtime.rs
@@ -22,7 +22,7 @@
 //!    future".
 //! 3. Multiple applications are better suited to multiple `EventLoopRuntime`s, on separate
 //!    `aos::EventLoop`s. Otherwise they can't send messages to each other, among other
-//!    restrictions. https://github.com/frc971/971-Robot-Code/issues/12 covers creating an adapter
+//!    restrictions. <https://github.com/frc971/971-Robot-Code/issues/12> covers creating an adapter
 //!    that provides multiple `EventLoop`s on top of a single underlying implementation.
 //!
 //! ## Design
@@ -31,7 +31,7 @@
 //! considerations in arriving at this design include:
 //!   * `EventLoop` implementations alias the objects they're returning from C++, which means
 //!     creating Rust unique references to them is unsound. See
-//!     https://github.com/google/autocxx/issues/1146 for details.
+//!     <https://github.com/google/autocxx/issues/1146> for details.
 //!   * For various reasons autocxx can't directly wrap APIs using types ergonomic for C++. This and
 //!     the previous point mean we wrap all of the C++ objects specifically for this class.
 //!   * Rust's lifetimes are only flexible enough to track everything with a single big lifetime.
@@ -40,14 +40,13 @@
 //!   * We can't use [`futures::stream::Stream`] and all of its nice [`futures::stream::StreamExt`]
 //!     helpers for watchers because we need lifetime-generic `Item` types. Effectively we're making
 //!     a lending stream. This is very close to lending iterators, which is one of the motivating
-//!     examples for generic associated types (https://github.com/rust-lang/rust/issues/44265).
+//!     examples for generic associated types (<https://github.com/rust-lang/rust/issues/44265>).
 
 use std::{
     fmt,
     future::Future,
     marker::PhantomData,
-    mem::ManuallyDrop,
-    ops::Add,
+    ops::{Add, Deref, DerefMut},
     panic::{catch_unwind, AssertUnwindSafe},
     pin::Pin,
     slice,
@@ -60,7 +59,9 @@
     WithinBox,
 };
 use cxx::UniquePtr;
-use flatbuffers::{root_unchecked, Follow, FollowWith, FullyQualifiedName};
+use flatbuffers::{
+    root_unchecked, Allocator, FlatBufferBuilder, Follow, FollowWith, FullyQualifiedName,
+};
 use futures::{future::pending, future::FusedFuture, never::Never};
 use thiserror::Error;
 use uuid::Uuid;
@@ -69,6 +70,7 @@
 use aos_configuration::{ChannelLookupError, ConfigurationExt};
 
 pub use aos_uuid::UUID;
+pub use ffi::aos::EventLoop as CppEventLoop;
 pub use ffi::aos::EventLoopRuntime as CppEventLoopRuntime;
 pub use ffi::aos::ExitHandle as CppExitHandle;
 
@@ -95,8 +97,6 @@
 extern_cpp_type!("aos::UUID", crate::UUID)
 );
 
-pub type EventLoop = ffi::aos::EventLoop;
-
 /// A marker type which is invariant with respect to the given lifetime.
 ///
 /// When interacting with functions that take and return things with a given lifetime, the lifetime
@@ -161,35 +161,29 @@
 ///
 /// # Safety
 ///
-/// Objects implementing this trait *must* have mostly-exclusive (except for running it) ownership
-/// of the `aos::EventLoop` *for its entire lifetime*, which *must* be dropped when this object is.
-/// See [`EventLoopRuntime.new`]'s safety requirements for why this can be important and details of
-/// mostly-exclusive. In other words, nothing else may mutate it in any way except processing events
-/// (including dropping, because this object has to be the one to drop it).
+/// Objects implementing this trait must guarantee that the underlying event loop (as returned
+/// from [`EventLoopHolder::as_raw`]), must be valid for as long as this object is. One way to do
+/// this may be by managing ownership of the event loop with Rust's ownership semantics. However,
+/// this is not strictly necessary.
 ///
-/// This also implies semantics similar to `Pin<&mut ffi::aos::EventLoop>` for the underlying object.
-/// Implementations of this trait must have exclusive ownership of it, and the underlying object
-/// must not be moved.
+/// This also implies semantics similar to `Pin<&mut CppEventLoop>` for the underlying object.
+/// Implementations of this trait must guarantee that the underlying object must not be moved while
+/// this object exists.
 pub unsafe trait EventLoopHolder {
-    /// Converts this holder into a raw C++ pointer. This may be fed through other Rust and C++
-    /// code, and eventually passed back to [`from_raw`].
-    fn into_raw(self) -> *mut ffi::aos::EventLoop;
-
-    /// Converts a raw C++ pointer back to a holder object.
+    /// Returns the raw C++ pointer of the underlying event loop.
     ///
-    /// # Safety
-    ///
-    /// `raw` must be the result of [`into_raw`] on an instance of this same type. These raw
-    /// pointers *are not* interchangeable between implementations of this trait.
-    unsafe fn from_raw(raw: *mut ffi::aos::EventLoop) -> Self;
+    /// Caller can only assume this pointer is valid while `self` is still alive.
+    fn as_raw(&self) -> *const CppEventLoop;
 }
 
 /// Owns an [`EventLoopRuntime`] and its underlying `aos::EventLoop`, with safe management of the
 /// associated Rust lifetimes.
-pub struct EventLoopRuntimeHolder<T: EventLoopHolder>(
-    ManuallyDrop<Pin<Box<CppEventLoopRuntime>>>,
-    PhantomData<T>,
-);
+pub struct EventLoopRuntimeHolder<T: EventLoopHolder> {
+    // NOTE: `runtime` must get dropped first, so we declare it before the event_loop:
+    // https://doc.rust-lang.org/reference/destructors.html
+    _runtime: Pin<Box<CppEventLoopRuntime>>,
+    _event_loop: T,
+}
 
 impl<T: EventLoopHolder> EventLoopRuntimeHolder<T> {
     /// Creates a new [`EventLoopRuntime`] and runs an initialization function on it. This is a
@@ -235,59 +229,41 @@
     /// ```
     pub fn new<F>(event_loop: T, fun: F) -> Self
     where
-        F: for<'event_loop> FnOnce(&mut EventLoopRuntime<'event_loop>),
+        F: for<'event_loop> FnOnce(EventLoopRuntime<'event_loop>),
     {
-        // SAFETY: The EventLoopRuntime never escapes this function, which means the only code that
-        // observes its lifetime is `fun`. `fun` must be generic across any value of its
-        // `'event_loop` lifetime parameter, which means we can choose any lifetime here, which
-        // satisfies the safety requirements.
-        //
-        // This is a similar pattern as `std::thread::scope`, `ghost-cell`, etc. Note that unlike
-        // `std::thread::scope`, our inner functions (the async ones) are definitely not allowed to
-        // capture things from the calling scope of this function, so there's no `'env` equivalent.
-        // `ghost-cell` ends up looking very similar despite doing different things with the
-        // pattern, while `std::thread::scope` has a lot of additional complexity to achieve a
-        // similar result.
-        //
-        // `EventLoopHolder`s safety requirements prevent anybody else from touching the underlying
-        // `aos::EventLoop`.
-        let mut runtime = unsafe { EventLoopRuntime::new(event_loop.into_raw()) };
-        fun(&mut runtime);
-        Self(ManuallyDrop::new(runtime.into_cpp()), PhantomData)
+        // SAFETY: The event loop pointer produced by as_raw must be valid and it will get dropped
+        // first (see https://doc.rust-lang.org/reference/destructors.html)
+        let runtime = unsafe { CppEventLoopRuntime::new(event_loop.as_raw()).within_box() };
+        EventLoopRuntime::with(&runtime, fun);
+        Self {
+            _runtime: runtime,
+            _event_loop: event_loop,
+        }
     }
 }
 
-impl<T: EventLoopHolder> Drop for EventLoopRuntimeHolder<T> {
-    fn drop(&mut self) {
-        let event_loop = self.0.as_mut().event_loop();
-        // SAFETY: We're not going to touch this field again. The underlying EventLoop will not be
-        // run again because we're going to drop it next.
-        unsafe { ManuallyDrop::drop(&mut self.0) };
-        // SAFETY: We took this from `into_raw`, and we just dropped the runtime which may contain
-        // Rust references to it.
-        unsafe { drop(T::from_raw(event_loop)) };
-    }
-}
-
+/// Manages the Rust interface to a *single* `aos::EventLoop`.
+///
+/// This is intended to be used by a single application.
+#[derive(Copy, Clone)]
 pub struct EventLoopRuntime<'event_loop>(
-    Pin<Box<ffi::aos::EventLoopRuntime>>,
+    &'event_loop CppEventLoopRuntime,
     // See documentation of [`new`] for details.
     InvariantLifetime<'event_loop>,
 );
 
-/// Manages the Rust interface to a *single* `aos::EventLoop`. This is intended to be used by a
-/// single application.
 impl<'event_loop> EventLoopRuntime<'event_loop> {
-    /// Creates a new runtime. This must be the only user of the underlying `aos::EventLoop`.
+    /// Creates a new runtime for the underlying event loop.
     ///
     /// Consider using [`EventLoopRuntimeHolder.new`] instead, if you're working with an
-    /// `aos::EventLoop` owned (indirectly) by Rust code.
+    /// `aos::EventLoop` owned (indirectly) by Rust code or using [`EventLoopRuntime::with`] as a safe
+    /// alternative.
     ///
-    /// One common pattern is calling this in the constructor of an object whose lifetime is managed
-    /// by C++; C++ doesn't inherit the Rust lifetime but we do have a lot of C++ code that obeys
-    /// these rules implicitly.
+    /// One common pattern is wrapping the lifetime behind a higher-rank trait bound (such as
+    /// [`FnOnce`]). This would constraint the lifetime to `'static` and objects with `'event_loop`
+    /// returned by this runtime.
     ///
-    /// Call [`spawn`] to respond to events. The non-event-driven APIs may be used without calling
+    /// Call [`EventLoopRuntime::spawn`] to respond to events. The non-event-driven APIs may be used without calling
     /// this.
     ///
     /// This is an async runtime, but it's a somewhat unusual one. See the module-level
@@ -299,14 +275,11 @@
     /// together. It all boils down to choosing `'event_loop` correctly, which is very complicated.
     /// Here are the rules:
     ///
-    /// 1. The `aos::EventLoop` APIs, and any other consumer-facing APIs, of the underlying
-    ///    `aos::EventLoop` *must* be exclusively used by this object, and things it calls, for
-    ///    `'event_loop`.
-    /// 2. `'event_loop` extends until after the last time the underlying `aos::EventLoop` is run.
-    ///    This is often beyond the lifetime of this Rust `EventLoopRuntime` object.
-    /// 3. `'event_loop` must outlive this object, because this object stores references to the
+    /// 1. `'event_loop` extends until after the last time the underlying `aos::EventLoop` is run.
+    ///    **This is often beyond the lifetime of this Rust `EventLoopRuntime` object**.
+    /// 2. `'event_loop` must outlive this object, because this object stores references to the
     ///    underlying `aos::EventLoop`.
-    /// 4. Any other references stored in the underlying `aos::EventLoop` must be valid for
+    /// 3. Any other references stored in the underlying `aos::EventLoop` must be valid for
     ///    `'event_loop`. The easiest way to ensure this is by not using the `aos::EventLoop` before
     ///    passing it to this object.
     ///
@@ -314,13 +287,15 @@
     ///
     /// 1. The underlying `aos::EventLoop` must be dropped after this object.
     /// 2. This object will store various references valid for `'event_loop` with a duration of
-    ///   `'event_loop`, which is safe as long as they're both the same `'event_loop`. Note that
-    ///   this requires this type to be invariant with respect to `'event_loop`.
-    /// 3. `event_loop` (the pointer being passed in) is effectively `Pin`, which is also implied
-    ///    by the underlying `aos::EventLoop` C++ type.
-    /// 4. You cannot create multiple `EventLoopRuntime`s from the same underlying `aos::EventLoop`
-    ///    or otherwise use it from a different application. The first one may create
-    ///    mutable Rust references while the second one expects exclusive ownership, for example.
+    ///   `'event_loop`, which is safe as long as
+    ///
+    /// * `'event_loop` outlives the underlying event loop, and
+    /// * `'event_loop` references are not used once the event loop is destroyed
+    ///
+    /// Note that this requires this type to be invariant with respect to `'event_loop`. This can
+    /// be achieved by using [`EventLoopRuntime::with`] since `'event_loop` referenes can't leave
+    /// `fun` and the runtime holding `'event_loop` references will be destroyed before the event
+    /// loop.
     ///
     /// `aos::EventLoop`'s public API is exclusively for consumers of the event loop. Some
     /// subclasses extend this API. Additionally, all useful implementations of `aos::EventLoop`
@@ -330,18 +305,6 @@
     /// loop functions independently of the consuming functions in every way except lifetime of the
     /// `aos::EventLoop`, and may be used independently of `'event_loop`.
     ///
-    /// ## Discussion of the rules
-    ///
-    /// Rule 1 is similar to rule 3 (they're both similar to mutable borrowing), but rule 1 extends
-    /// for the entire lifetime of the object instead of being limited to the lifetime of an
-    /// individual borrow by an instance of this type. This is similar to the way [`Pin`]'s
-    /// estrictions extend for the entire lifetime of the object, until it is dropped.
-    ///
-    /// Rule 2 and corollaries 2 and 3 go together, and are essential for making [`spawn`]ed tasks
-    /// useful. The `aos::EventLoop` is full of indirect circular references, both within itself
-    /// and via all of the callbacks. This is sound if all of these references have the *exact
-    /// same* Rust lifetime, which is `'event_loop`.
-    ///
     /// ## Alternatives and why they don't work
     ///
     /// Making the argument `Pin<&'event_loop mut EventLoop>` would express some (but not all) of
@@ -350,7 +313,7 @@
     /// same object from C++, which is a common operation. See the module-level documentation for
     /// details.
     ///
-    /// [`spawn`]ed tasks need to hold `&'event_loop` references to things like channels. Using a
+    /// spawned tasks need to hold `&'event_loop` references to things like channels. Using a
     /// separate `'config` lifetime wouldn't change much; the tasks still need to do things which
     /// require them to not outlive something they don't control. This is fundamental to
     /// self-referential objects, which `aos::EventLoop` is based around, but Rust requires unsafe
@@ -358,49 +321,39 @@
     ///
     /// ## Final cautions
     ///
-    /// Following these rules is very tricky. Be very cautious calling this function. It exposes an
-    /// unbound lifetime, which means you should wrap it directly in a function that attaches a
-    /// correct lifetime.
-    pub unsafe fn new(event_loop: *mut ffi::aos::EventLoop) -> Self {
-        Self(
-            // SAFETY: We push all the validity requirements for this up to our caller.
-            unsafe { ffi::aos::EventLoopRuntime::new(event_loop) }.within_box(),
-            InvariantLifetime::default(),
-        )
+    /// Following these rules is very tricky. Be very cautious calling this function. The
+    /// exposed lifetime doesn't actually convey all the rules to the compiler. To the compiler,
+    /// `'event_loop` ends when this object is dropped which is not the case!
+    pub unsafe fn new(event_loop: &'event_loop CppEventLoopRuntime) -> Self {
+        Self(event_loop, InvariantLifetime::default())
     }
 
-    /// Creates a Rust wrapper from the underlying C++ object, with an unbound lifetime.
+    /// Safely builds a "constrained" EventLoopRuntime with `fun`.
     ///
-    /// This may never be useful, but it's here for this big scary comment to explain why it's not
-    /// useful.
-    ///
-    /// # Safety
-    ///
-    /// See [`new`] for safety restrictions on `'event_loop` when calling this. In particular, see
-    /// the note about how tricky doing this correctly is, and remember that for this function the
-    /// event loop in question isn't even an argument to this function so it's even trickier. Also
-    /// note that you cannot call this on the result of [`into_cpp`] without violating those
-    /// restrictions.
-    pub unsafe fn from_cpp(cpp: Pin<Box<ffi::aos::EventLoopRuntime>>) -> Self {
-        Self(cpp, InvariantLifetime::default())
-    }
-
-    /// Extracts the underlying C++ object, without the corresponding Rust lifetime. This is useful
-    /// to stop the propagation of Rust lifetimes without destroying the underlying object which
-    /// contains all the state.
-    ///
-    /// Note that you *cannot* call [`from_cpp`] on the result of this, because that will violate
-    /// [`from_cpp`]'s safety requirements.
-    pub fn into_cpp(self) -> Pin<Box<ffi::aos::EventLoopRuntime>> {
-        self.0
+    /// We constrain the scope of the `[EventLoopRuntime]` by tying it to **any** `'a` lifetime. The
+    /// idea is that the only things that satisfy this lifetime are either ``static` or produced by
+    /// the event loop itself with a '`event_loop` runtime.
+    pub fn with<F>(event_loop: &'event_loop CppEventLoopRuntime, fun: F)
+    where
+        F: for<'a> FnOnce(EventLoopRuntime<'a>),
+    {
+        // SAFETY: We satisfy the event loop lifetime constraint by scoping it inside of a higher-
+        // rank lifetime in FnOnce. This is similar to what is done in std::thread::scope, and the
+        // point is that `fun` can only assume that `'static` and types produced by this type with a
+        // 'event_loop lifetime are the only lifetimes that will satisfy `'a`. This is possible due
+        // to this type's invariance over its lifetime, otherwise, one could easily make a Subtype
+        // that, due to its shorter lifetime, would include things from its outer scope.
+        unsafe {
+            fun(Self::new(event_loop));
+        }
     }
 
     /// Returns the pointer passed into the constructor.
     ///
     /// The returned value should only be used for destroying it (_after_ `self` is dropped) or
     /// calling other C++ APIs.
-    pub fn raw_event_loop(&mut self) -> *mut ffi::aos::EventLoop {
-        self.0.as_mut().event_loop()
+    pub fn raw_event_loop(&self) -> *mut CppEventLoop {
+        self.0.event_loop()
     }
 
     /// Returns a reference to the name of this EventLoop.
@@ -443,7 +396,7 @@
     /// will never complete. `task` will not be polled after the underlying `aos::EventLoop` exits.
     ///
     /// Note that task will be polled immediately, to give it a chance to initialize. If you want to
-    /// defer work until the event loop starts running, await [`on_run`] in the task.
+    /// defer work until the event loop starts running, await [`EventLoopRuntime::on_run`] in the task.
     ///
     /// # Panics
     ///
@@ -563,8 +516,8 @@
     /// }});
     /// # }
     /// ```
-    pub fn spawn(&mut self, task: impl Future<Output = Never> + 'event_loop) {
-        self.0.as_mut().Spawn(RustApplicationFuture::new(task));
+    pub fn spawn(&self, task: impl Future<Output = Never> + 'event_loop) {
+        self.0.Spawn(RustApplicationFuture::new(task));
     }
 
     pub fn configuration(&self) -> &'event_loop Configuration {
@@ -590,10 +543,10 @@
     /// # Panics
     ///
     /// Dropping `self` before the returned object is dropped will panic.
-    pub fn make_raw_watcher(&mut self, channel: &'event_loop Channel) -> RawWatcher {
+    pub fn make_raw_watcher(&self, channel: &'event_loop Channel) -> RawWatcher {
         // SAFETY: `channel` is valid for the necessary lifetime, all other requirements fall under
         // the usual autocxx heuristics.
-        RawWatcher(unsafe { self.0.as_mut().MakeWatcher(channel) }.within_box())
+        RawWatcher(unsafe { self.0.MakeWatcher(channel) }.within_box())
     }
 
     /// Provides type-safe async blocking access to messages on a channel. `T` should be a
@@ -603,7 +556,7 @@
     /// # Panics
     ///
     /// Dropping `self` before the returned object is dropped will panic.
-    pub fn make_watcher<T>(&mut self, channel_name: &str) -> Result<Watcher<T>, ChannelLookupError>
+    pub fn make_watcher<T>(&self, channel_name: &str) -> Result<Watcher<T>, ChannelLookupError>
     where
         for<'a> T: FollowWith<'a>,
         for<'a> <T as FollowWith<'a>>::Inner: Follow<'a>,
@@ -619,10 +572,10 @@
     /// # Panics
     ///
     /// Dropping `self` before the returned object is dropped will panic.
-    pub fn make_raw_sender(&mut self, channel: &'event_loop Channel) -> RawSender {
+    pub fn make_raw_sender(&self, channel: &'event_loop Channel) -> RawSender {
         // SAFETY: `channel` is valid for the necessary lifetime, all other requirements fall under
         // the usual autocxx heuristics.
-        RawSender(unsafe { self.0.as_mut().MakeSender(channel) }.within_box())
+        RawSender(unsafe { self.0.MakeSender(channel) }.within_box())
     }
 
     /// Allows sending messages on a channel with a type-safe API.
@@ -630,7 +583,7 @@
     /// # Panics
     ///
     /// Dropping `self` before the returned object is dropped will panic.
-    pub fn make_sender<T>(&mut self, channel_name: &str) -> Result<Sender<T>, ChannelLookupError>
+    pub fn make_sender<T>(&self, channel_name: &str) -> Result<Sender<T>, ChannelLookupError>
     where
         for<'a> T: FollowWith<'a>,
         for<'a> <T as FollowWith<'a>>::Inner: Follow<'a>,
@@ -646,10 +599,10 @@
     /// # Panics
     ///
     /// Dropping `self` before the returned object is dropped will panic.
-    pub fn make_raw_fetcher(&mut self, channel: &'event_loop Channel) -> RawFetcher {
+    pub fn make_raw_fetcher(&self, channel: &'event_loop Channel) -> RawFetcher {
         // SAFETY: `channel` is valid for the necessary lifetime, all other requirements fall under
         // the usual autocxx heuristics.
-        RawFetcher(unsafe { self.0.as_mut().MakeFetcher(channel) }.within_box())
+        RawFetcher(unsafe { self.0.MakeFetcher(channel) }.within_box())
     }
 
     /// Provides type-safe access to messages on a channel, without the ability to wait for a new
@@ -659,7 +612,7 @@
     /// # Panics
     ///
     /// Dropping `self` before the returned object is dropped will panic.
-    pub fn make_fetcher<T>(&mut self, channel_name: &str) -> Result<Fetcher<T>, ChannelLookupError>
+    pub fn make_fetcher<T>(&self, channel_name: &str) -> Result<Fetcher<T>, ChannelLookupError>
     where
         for<'a> T: FollowWith<'a>,
         for<'a> <T as FollowWith<'a>>::Inner: Follow<'a>,
@@ -676,8 +629,8 @@
     /// subsequent code will have any realtime scheduling applied. This means it can rely on
     /// consistent timing, but it can no longer create any EventLoop child objects or do anything
     /// else non-realtime.
-    pub fn on_run(&mut self) -> OnRun {
-        OnRun(self.0.as_mut().MakeOnRun().within_box())
+    pub fn on_run(&self) -> OnRun {
+        OnRun(self.0.MakeOnRun().within_box())
     }
 
     pub fn is_running(&self) -> bool {
@@ -685,16 +638,21 @@
     }
 
     /// Returns an unarmed timer.
-    pub fn add_timer(&mut self) -> Timer {
-        Timer(self.0.as_mut().AddTimer())
+    pub fn add_timer(&self) -> Timer {
+        Timer(self.0.AddTimer())
     }
 
     /// Returns a timer that goes off every `duration`-long ticks.
-    pub fn add_interval(&mut self, duration: Duration) -> Timer {
+    pub fn add_interval(&self, duration: Duration) -> Timer {
         let mut timer = self.add_timer();
         timer.setup(self.monotonic_now(), Some(duration));
         timer
     }
+
+    /// Sets the scheduler priority to run the event loop at.
+    pub fn set_realtime_priority(&self, priority: i32) {
+        self.0.SetRuntimeRealtimePriority(priority.into());
+    }
 }
 
 /// An event loop primitive that allows sleeping asynchronously.
@@ -817,7 +775,7 @@
 ///
 /// We also run into some limitations in the borrow checker trying to implement `poll`, I think it's
 /// the same one mentioned here:
-/// https://blog.rust-lang.org/2022/08/05/nll-by-default.html#looking-forward-what-can-we-expect-for-the-borrow-checker-of-the-future
+/// <https://blog.rust-lang.org/2022/08/05/nll-by-default.html#looking-forward-what-can-we-expect-for-the-borrow-checker-of-the-future>
 /// We get around that one by moving the unbounded lifetime from the pointer dereference into the
 /// function with the if statement.
 // SAFETY: If this outlives the parent EventLoop, the C++ code will LOG(FATAL).
@@ -1160,11 +1118,6 @@
 pub struct RawSender(Pin<Box<ffi::aos::SenderForRust>>);
 
 impl RawSender {
-    fn buffer(&mut self) -> &mut [u8] {
-        // SAFETY: This is a valid slice, and `u8` doesn't have any alignment requirements.
-        unsafe { slice::from_raw_parts_mut(self.0.as_mut().data(), self.0.as_mut().size()) }
-    }
-
     /// Returns an object which can be used to build a message.
     ///
     /// # Examples
@@ -1187,6 +1140,7 @@
     /// # unsafe {
     /// let mut builder1 = sender.make_builder();
     /// builder1.fbb();
+    /// drop(builder1);
     /// let mut builder2 = sender.make_builder();
     /// let pong = PongBuilder::new(builder2.fbb()).finish();
     /// builder2.send(pong);
@@ -1206,11 +1160,14 @@
     /// # }
     /// ```
     pub fn make_builder(&mut self) -> RawBuilder {
-        // TODO(Brian): Actually use the provided buffer instead of just using its
-        // size to allocate a separate one.
-        //
-        // See https://github.com/google/flatbuffers/issues/7385.
-        let fbb = flatbuffers::FlatBufferBuilder::with_capacity(self.buffer().len());
+        // SAFETY: This is a valid slice, and `u8` doesn't have any alignment
+        // requirements. Additionally, the lifetime of the builder is tied to
+        // the lifetime of self so the buffer won't be accessible again until
+        // the builder is destroyed.
+        let allocator = ChannelPreallocatedAllocator::new(unsafe {
+            slice::from_raw_parts_mut(self.0.as_mut().data(), self.0.as_mut().size())
+        });
+        let fbb = FlatBufferBuilder::new_in(allocator);
         RawBuilder {
             raw_sender: self,
             fbb,
@@ -1221,11 +1178,13 @@
 /// Used for building a message. See [`RawSender::make_builder`] for details.
 pub struct RawBuilder<'sender> {
     raw_sender: &'sender mut RawSender,
-    fbb: flatbuffers::FlatBufferBuilder<'sender>,
+    fbb: FlatBufferBuilder<'sender, ChannelPreallocatedAllocator<'sender>>,
 }
 
 impl<'sender> RawBuilder<'sender> {
-    pub fn fbb(&mut self) -> &mut flatbuffers::FlatBufferBuilder<'sender> {
+    pub fn fbb(
+        &mut self,
+    ) -> &mut FlatBufferBuilder<'sender, ChannelPreallocatedAllocator<'sender>> {
         &mut self.fbb
     }
 
@@ -1238,12 +1197,7 @@
 
         use ffi::aos::RawSender_Error as FfiError;
         // SAFETY: This is a valid buffer we're passing.
-        match unsafe {
-            self.raw_sender
-                .0
-                .as_mut()
-                .CopyAndSend(data.as_ptr(), data.len())
-        } {
+        match self.raw_sender.0.as_mut().SendBuffer(data.len()) {
             FfiError::kOk => Ok(()),
             FfiError::kMessagesSentTooFast => Err(SendError::MessagesSentTooFast),
             FfiError::kInvalidRedzone => Err(SendError::InvalidRedzone),
@@ -1287,6 +1241,7 @@
     /// # fn compile_check(mut sender: aos_events_event_loop_runtime::Sender<Pong<'static>>) {
     /// let mut builder1 = sender.make_builder();
     /// builder1.fbb();
+    /// drop(builder1);
     /// let mut builder2 = sender.make_builder();
     /// let pong = PongBuilder::new(builder2.fbb()).finish();
     /// builder2.send(pong);
@@ -1322,7 +1277,9 @@
     for<'a> T: FollowWith<'a>,
     for<'a> <T as FollowWith<'a>>::Inner: Follow<'a>,
 {
-    pub fn fbb(&mut self) -> &mut flatbuffers::FlatBufferBuilder<'sender> {
+    pub fn fbb(
+        &mut self,
+    ) -> &mut FlatBufferBuilder<'sender, ChannelPreallocatedAllocator<'sender>> {
         self.0.fbb()
     }
 
@@ -1415,7 +1372,7 @@
 #[repr(transparent)]
 pub struct OnRun(Pin<Box<ffi::aos::OnRunForRust>>);
 
-impl Future for OnRun {
+impl Future for &'_ OnRun {
     type Output = ();
 
     fn poll(self: Pin<&mut Self>, _: &mut std::task::Context) -> Poll<()> {
@@ -1461,6 +1418,12 @@
     }
 }
 
+impl From<MonotonicInstant> for i64 {
+    fn from(value: MonotonicInstant) -> Self {
+        value.0
+    }
+}
+
 impl fmt::Debug for MonotonicInstant {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         self.duration_since_epoch().fmt(f)
@@ -1489,6 +1452,12 @@
     }
 }
 
+impl From<RealtimeInstant> for i64 {
+    fn from(value: RealtimeInstant) -> Self {
+        value.0
+    }
+}
+
 impl fmt::Debug for RealtimeInstant {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         self.duration_since_epoch().fmt(f)
@@ -1527,8 +1496,8 @@
 
 impl ExitHandle {
     /// Exits the EventLoops represented by this handle. You probably want to immediately return
-    /// from the context this is called in. Awaiting [`exit`] instead of using this function is an
-    /// easy way to do that.
+    /// from the context this is called in. Awaiting [`ExitHandle::exit`] instead of using this
+    /// function is an easy way to do that.
     pub fn exit_sync(mut self) {
         self.0.as_mut().unwrap().Exit();
     }
@@ -1547,3 +1516,44 @@
         Self(inner)
     }
 }
+
+pub struct ChannelPreallocatedAllocator<'a> {
+    buffer: &'a mut [u8],
+}
+
+impl<'a> ChannelPreallocatedAllocator<'a> {
+    pub fn new(buffer: &'a mut [u8]) -> Self {
+        Self { buffer }
+    }
+}
+
+#[derive(Debug, Error)]
+#[error("Can't allocate more memory with a fixed size allocator")]
+pub struct OutOfMemory;
+
+// SAFETY: Allocator follows the required behavior.
+unsafe impl Allocator for ChannelPreallocatedAllocator<'_> {
+    type Error = OutOfMemory;
+    fn grow_downwards(&mut self) -> Result<(), Self::Error> {
+        // Fixed size allocator can't grow.
+        Err(OutOfMemory)
+    }
+
+    fn len(&self) -> usize {
+        self.buffer.len()
+    }
+}
+
+impl Deref for ChannelPreallocatedAllocator<'_> {
+    type Target = [u8];
+
+    fn deref(&self) -> &Self::Target {
+        self.buffer
+    }
+}
+
+impl DerefMut for ChannelPreallocatedAllocator<'_> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        self.buffer
+    }
+}
diff --git a/aos/events/event_loop_runtime_test_lib.rs b/aos/events/event_loop_runtime_test_lib.rs
index 56dc9ef..acc162d 100644
--- a/aos/events/event_loop_runtime_test_lib.rs
+++ b/aos/events/event_loop_runtime_test_lib.rs
@@ -1,14 +1,26 @@
 //! These test helpers have to live in a separate file because autocxx only generates one set of
 //! outputs per file, and that needs to be the non-`#[cfg(test)]` stuff.
 
-use aos_events_event_loop_runtime::{EventLoop, EventLoopRuntime, Fetcher, RawFetcher};
+use aos_events_event_loop_runtime::{CppEventLoop as EventLoop, Fetcher, RawFetcher};
 use ping_rust_fbs::aos::examples::{root_as_ping, Ping};
 use pong_rust_fbs::aos::examples::{Pong, PongBuilder};
 
 mod tests {
+    use aos_events_event_loop_runtime::{EventLoopHolder, EventLoopRuntimeHolder};
+
     use super::*;
 
-    use std::cell::RefCell;
+    use std::{borrow::Borrow, cell::RefCell};
+
+    /// Represents a holder of the event loop that is managed in C++.
+    struct CppEventLoopHolder(*const EventLoop);
+
+    // SAFETY: We defer the requirement that the event loop is valid and won't move to C++.
+    unsafe impl EventLoopHolder for CppEventLoopHolder {
+        fn as_raw(&self) -> *const EventLoop {
+            self.0
+        }
+    }
 
     #[derive(Debug, Default)]
     struct GlobalState {
@@ -39,62 +51,65 @@
         GLOBAL_STATE.with(|g| g.borrow().on_run_count)
     }
 
-    pub struct TestApplication<'event_loop> {
-        _runtime: EventLoopRuntime<'event_loop>,
+    pub struct TestApplication {
+        _runtime: EventLoopRuntimeHolder<CppEventLoopHolder>,
         raw_ping_fetcher: RawFetcher,
     }
 
-    impl<'event_loop> TestApplication<'event_loop> {
-        fn new(mut runtime: EventLoopRuntime<'event_loop>) -> Self {
-            let ping_channel = runtime
-                .get_raw_channel("/test", "aos.examples.Ping")
-                .expect("Should have Ping channel");
-            let mut raw_ping_watcher = runtime.make_raw_watcher(ping_channel);
-            let mut raw_pong_sender = runtime.make_raw_sender(
-                runtime
-                    .get_raw_channel("/test", "aos.examples.Pong")
-                    .expect("Should have Pong channel"),
-            );
-            let on_run = runtime.on_run();
-            runtime.spawn(async move {
-                on_run.await;
-                GLOBAL_STATE.with(|g| {
-                    let g = &mut *g.borrow_mut();
-                    assert_eq!(g.creation_count, g.drop_count + 1);
-                    assert_eq!(g.drop_count, g.on_run_count);
-                    assert_eq!(g.drop_count, g.before_count);
-                    assert_eq!(g.drop_count, g.watcher_count);
-                    assert_eq!(g.drop_count, g.after_count);
-                    g.on_run_count += 1;
-                });
-                loop {
-                    let context = raw_ping_watcher.next().await;
-                    assert!(!context.monotonic_event_time().is_min_time());
-                    assert!(!context.data().is_none());
+    impl TestApplication {
+        fn new(event_loop: CppEventLoopHolder) -> Self {
+            let mut raw_ping_fetcher = None;
+            let runtime = EventLoopRuntimeHolder::new(event_loop, |runtime| {
+                let ping_channel = runtime
+                    .get_raw_channel("/test", "aos.examples.Ping")
+                    .expect("Should have Ping channel");
+                let mut raw_ping_watcher = runtime.make_raw_watcher(ping_channel);
+                let mut raw_pong_sender = runtime.make_raw_sender(
+                    runtime
+                        .get_raw_channel("/test", "aos.examples.Pong")
+                        .expect("Should have Pong channel"),
+                );
+                let on_run = runtime.on_run();
+                runtime.spawn(async move {
+                    on_run.borrow().await;
                     GLOBAL_STATE.with(|g| {
                         let g = &mut *g.borrow_mut();
                         assert_eq!(g.creation_count, g.drop_count + 1);
-                        assert_eq!(g.creation_count, g.on_run_count);
-                        assert_eq!(g.creation_count, g.before_count);
+                        assert_eq!(g.drop_count, g.on_run_count);
+                        assert_eq!(g.drop_count, g.before_count);
                         assert_eq!(g.drop_count, g.watcher_count);
                         assert_eq!(g.drop_count, g.after_count);
-                        g.watcher_count += 1;
+                        g.on_run_count += 1;
                     });
-                    let ping = root_as_ping(context.data().expect("should have the data"))
-                        .expect("Ping should be valid");
+                    loop {
+                        let context = raw_ping_watcher.next().await;
+                        assert!(!context.monotonic_event_time().is_min_time());
+                        assert!(!context.data().is_none());
+                        GLOBAL_STATE.with(|g| {
+                            let g = &mut *g.borrow_mut();
+                            assert_eq!(g.creation_count, g.drop_count + 1);
+                            assert_eq!(g.creation_count, g.on_run_count);
+                            assert_eq!(g.creation_count, g.before_count);
+                            assert_eq!(g.drop_count, g.watcher_count);
+                            assert_eq!(g.drop_count, g.after_count);
+                            g.watcher_count += 1;
+                        });
+                        let ping = root_as_ping(context.data().expect("should have the data"))
+                            .expect("Ping should be valid");
 
-                    let mut builder = raw_pong_sender.make_builder();
-                    let mut pong = PongBuilder::new(builder.fbb());
-                    pong.add_value(ping.value());
-                    let pong = pong.finish();
-                    // SAFETY: We're sending the correct type here.
-                    unsafe { builder.send(pong) }.expect("send should succeed");
-                }
+                        let mut builder = raw_pong_sender.make_builder();
+                        let mut pong = PongBuilder::new(builder.fbb());
+                        pong.add_value(ping.value());
+                        let pong = pong.finish();
+                        // SAFETY: We're sending the correct type here.
+                        unsafe { builder.send(pong) }.expect("send should succeed");
+                    }
+                });
+                raw_ping_fetcher = Some(runtime.make_raw_fetcher(ping_channel));
             });
-            let raw_ping_fetcher = runtime.make_raw_fetcher(ping_channel);
             Self {
                 _runtime: runtime,
-                raw_ping_fetcher,
+                raw_ping_fetcher: raw_ping_fetcher.unwrap(),
             }
         }
 
@@ -137,7 +152,7 @@
         }
     }
 
-    impl Drop for TestApplication<'_> {
+    impl Drop for TestApplication {
         fn drop(&mut self) {
             GLOBAL_STATE.with(|g| {
                 let g = &mut *g.borrow_mut();
@@ -151,61 +166,64 @@
         }
     }
 
-    unsafe fn make_test_application(event_loop: *mut EventLoop) -> Box<TestApplication<'static>> {
+    unsafe fn make_test_application(event_loop: *mut EventLoop) -> Box<TestApplication> {
         GLOBAL_STATE.with(|g| {
             let g = &mut *g.borrow_mut();
             g.creation_count += 1;
         });
-        Box::new(TestApplication::new(EventLoopRuntime::new(event_loop)))
+        Box::new(TestApplication::new(CppEventLoopHolder(event_loop)))
     }
 
-    pub struct TypedTestApplication<'event_loop> {
-        _runtime: EventLoopRuntime<'event_loop>,
+    pub struct TypedTestApplication {
+        _runtime: EventLoopRuntimeHolder<CppEventLoopHolder>,
         ping_fetcher: Fetcher<Ping<'static>>,
     }
 
-    impl<'event_loop> TypedTestApplication<'event_loop> {
-        fn new(mut runtime: EventLoopRuntime<'event_loop>) -> Self {
-            let mut ping_watcher = runtime.make_watcher::<Ping<'static>>("/test").unwrap();
-            let mut pong_sender = runtime.make_sender::<Pong<'static>>("/test").unwrap();
-            let on_run = runtime.on_run();
-            runtime.spawn(async move {
-                on_run.await;
-                GLOBAL_STATE.with(|g| {
-                    let g = &mut *g.borrow_mut();
-                    assert_eq!(g.creation_count, g.drop_count + 1);
-                    assert_eq!(g.drop_count, g.on_run_count);
-                    assert_eq!(g.drop_count, g.before_count);
-                    assert_eq!(g.drop_count, g.watcher_count);
-                    assert_eq!(g.drop_count, g.after_count);
-                    g.on_run_count += 1;
-                });
-                loop {
-                    let context = ping_watcher.next().await;
-                    assert!(!context.monotonic_event_time().is_min_time());
-                    assert!(!context.message().is_none());
+    impl TypedTestApplication {
+        fn new(event_loop: CppEventLoopHolder) -> Self {
+            let mut ping_fetcher = None;
+            let runtime = EventLoopRuntimeHolder::new(event_loop, |runtime| {
+                let mut ping_watcher = runtime.make_watcher::<Ping<'static>>("/test").unwrap();
+                let mut pong_sender = runtime.make_sender::<Pong<'static>>("/test").unwrap();
+                let on_run = runtime.on_run();
+                runtime.spawn(async move {
+                    on_run.borrow().await;
                     GLOBAL_STATE.with(|g| {
                         let g = &mut *g.borrow_mut();
                         assert_eq!(g.creation_count, g.drop_count + 1);
-                        assert_eq!(g.creation_count, g.on_run_count);
-                        assert_eq!(g.creation_count, g.before_count);
+                        assert_eq!(g.drop_count, g.on_run_count);
+                        assert_eq!(g.drop_count, g.before_count);
                         assert_eq!(g.drop_count, g.watcher_count);
                         assert_eq!(g.drop_count, g.after_count);
-                        g.watcher_count += 1;
+                        g.on_run_count += 1;
                     });
-                    let ping: Ping<'_> = context.message().unwrap();
+                    loop {
+                        let context = ping_watcher.next().await;
+                        assert!(!context.monotonic_event_time().is_min_time());
+                        assert!(!context.message().is_none());
+                        GLOBAL_STATE.with(|g| {
+                            let g = &mut *g.borrow_mut();
+                            assert_eq!(g.creation_count, g.drop_count + 1);
+                            assert_eq!(g.creation_count, g.on_run_count);
+                            assert_eq!(g.creation_count, g.before_count);
+                            assert_eq!(g.drop_count, g.watcher_count);
+                            assert_eq!(g.drop_count, g.after_count);
+                            g.watcher_count += 1;
+                        });
+                        let ping: Ping<'_> = context.message().unwrap();
 
-                    let mut builder = pong_sender.make_builder();
-                    let mut pong = PongBuilder::new(builder.fbb());
-                    pong.add_value(ping.value());
-                    let pong = pong.finish();
-                    builder.send(pong).expect("send should succeed");
-                }
+                        let mut builder = pong_sender.make_builder();
+                        let mut pong = PongBuilder::new(builder.fbb());
+                        pong.add_value(ping.value());
+                        let pong = pong.finish();
+                        builder.send(pong).expect("send should succeed");
+                    }
+                });
+                ping_fetcher = Some(runtime.make_fetcher("/test").unwrap());
             });
-            let ping_fetcher = runtime.make_fetcher("/test").unwrap();
             Self {
                 _runtime: runtime,
-                ping_fetcher,
+                ping_fetcher: ping_fetcher.unwrap(),
             }
         }
 
@@ -245,7 +263,7 @@
         }
     }
 
-    impl Drop for TypedTestApplication<'_> {
+    impl Drop for TypedTestApplication {
         fn drop(&mut self) {
             GLOBAL_STATE.with(|g| {
                 let g = &mut *g.borrow_mut();
@@ -259,44 +277,46 @@
         }
     }
 
-    unsafe fn make_typed_test_application(
-        event_loop: *mut EventLoop,
-    ) -> Box<TypedTestApplication<'static>> {
+    unsafe fn make_typed_test_application(event_loop: *mut EventLoop) -> Box<TypedTestApplication> {
         GLOBAL_STATE.with(|g| {
             let g = &mut *g.borrow_mut();
             g.creation_count += 1;
         });
-        Box::new(TypedTestApplication::new(EventLoopRuntime::new(event_loop)))
+        Box::new(TypedTestApplication::new(CppEventLoopHolder(event_loop)))
     }
 
-    struct PanicApplication<'event_loop> {
-        _runtime: EventLoopRuntime<'event_loop>,
+    struct PanicApplication {
+        _runtime: EventLoopRuntimeHolder<CppEventLoopHolder>,
     }
 
-    impl<'event_loop> PanicApplication<'event_loop> {
-        fn new(mut runtime: EventLoopRuntime<'event_loop>) -> Self {
-            runtime.spawn(async move {
-                panic!("Test Rust panic");
+    impl PanicApplication {
+        fn new(event_loop: CppEventLoopHolder) -> Self {
+            let runtime = EventLoopRuntimeHolder::new(event_loop, |runtime| {
+                runtime.spawn(async move {
+                    panic!("Test Rust panic");
+                });
             });
 
             Self { _runtime: runtime }
         }
     }
 
-    unsafe fn make_panic_application(event_loop: *mut EventLoop) -> Box<PanicApplication<'static>> {
-        Box::new(PanicApplication::new(EventLoopRuntime::new(event_loop)))
+    unsafe fn make_panic_application(event_loop: *mut EventLoop) -> Box<PanicApplication> {
+        Box::new(PanicApplication::new(CppEventLoopHolder(event_loop)))
     }
 
-    struct PanicOnRunApplication<'event_loop> {
-        _runtime: EventLoopRuntime<'event_loop>,
+    struct PanicOnRunApplication {
+        _runtime: EventLoopRuntimeHolder<CppEventLoopHolder>,
     }
 
-    impl<'event_loop> PanicOnRunApplication<'event_loop> {
-        fn new(mut runtime: EventLoopRuntime<'event_loop>) -> Self {
-            let on_run = runtime.on_run();
-            runtime.spawn(async move {
-                on_run.await;
-                panic!("Test Rust panic");
+    impl PanicOnRunApplication {
+        fn new(event_loop: CppEventLoopHolder) -> Self {
+            let runtime = EventLoopRuntimeHolder::new(event_loop, |runtime| {
+                let on_run = runtime.on_run();
+                runtime.spawn(async move {
+                    on_run.borrow().await;
+                    panic!("Test Rust panic");
+                });
             });
 
             Self { _runtime: runtime }
@@ -305,55 +325,49 @@
 
     unsafe fn make_panic_on_run_application(
         event_loop: *mut EventLoop,
-    ) -> Box<PanicOnRunApplication<'static>> {
-        Box::new(PanicOnRunApplication::new(EventLoopRuntime::new(
-            event_loop,
-        )))
+    ) -> Box<PanicOnRunApplication> {
+        Box::new(PanicOnRunApplication::new(CppEventLoopHolder(event_loop)))
     }
 
     #[cxx::bridge(namespace = "aos::events::testing")]
     mod ffi_bridge {
         extern "Rust" {
-            unsafe fn make_test_application(
-                event_loop: *mut EventLoop,
-            ) -> Box<TestApplication<'static>>;
+            unsafe fn make_test_application(event_loop: *mut EventLoop) -> Box<TestApplication>;
 
             unsafe fn make_typed_test_application(
                 event_loop: *mut EventLoop,
-            ) -> Box<TypedTestApplication<'static>>;
+            ) -> Box<TypedTestApplication>;
 
-            unsafe fn make_panic_application(
-                event_loop: *mut EventLoop,
-            ) -> Box<PanicApplication<'static>>;
+            unsafe fn make_panic_application(event_loop: *mut EventLoop) -> Box<PanicApplication>;
 
             unsafe fn make_panic_on_run_application(
                 event_loop: *mut EventLoop,
-            ) -> Box<PanicOnRunApplication<'static>>;
+            ) -> Box<PanicOnRunApplication>;
 
             fn completed_test_count() -> u32;
             fn started_test_count() -> u32;
         }
 
         extern "Rust" {
-            type TestApplication<'a>;
+            type TestApplication;
 
             fn before_sending(&mut self);
             fn after_sending(&mut self);
         }
 
         extern "Rust" {
-            type TypedTestApplication<'a>;
+            type TypedTestApplication;
 
             fn before_sending(&mut self);
             fn after_sending(&mut self);
         }
 
         extern "Rust" {
-            type PanicApplication<'a>;
+            type PanicApplication;
         }
 
         extern "Rust" {
-            type PanicOnRunApplication<'a>;
+            type PanicOnRunApplication;
         }
 
         unsafe extern "C++" {
diff --git a/aos/events/logging/log_backend_test.cc b/aos/events/logging/log_backend_test.cc
index d3c83cc..1e95c10 100644
--- a/aos/events/logging/log_backend_test.cc
+++ b/aos/events/logging/log_backend_test.cc
@@ -128,7 +128,17 @@
 
 TEST(QueueAlignmentTest, Cases) {
   QueueAligner aligner;
-  uint8_t *start = nullptr;
+
+  // Get a 512-byte-aligned pointer to a buffer. That buffer needs to be at
+  // least 3 sectors big for the purposes of this test.
+  uint8_t buffer[FileHandler::kSector * 4];
+  void *aligned_start = buffer;
+  size_t size = sizeof(buffer);
+  ASSERT_TRUE(std::align(FileHandler::kSector, FileHandler::kSector * 3,
+                         aligned_start, size) != nullptr);
+  ASSERT_GE(size, FileHandler::kSector * 3);
+
+  uint8_t *start = static_cast<uint8_t *>(aligned_start);
   {
     // Only prefix
     std::vector<absl::Span<const uint8_t>> queue;
diff --git a/aos/events/logging/logfile_utils_out_of_space_test.sh b/aos/events/logging/logfile_utils_out_of_space_test.sh
index f412e0d..6a32347 100755
--- a/aos/events/logging/logfile_utils_out_of_space_test.sh
+++ b/aos/events/logging/logfile_utils_out_of_space_test.sh
@@ -23,7 +23,7 @@
 rm -rf "${TMPFS}"
 mkdir "${TMPFS}"
 
-function test {
+function run_test {
   SIZE="$1"
   echo "Running test with ${SIZE}..." >&2
   unshare --mount --map-root-user bash <<END
@@ -37,10 +37,10 @@
 }
 
 # Run out of space exactly at the beginning of a block.
-test 81920
+run_test 81920
 
 # Run out of space 1 byte into a block.
-test 81921
+run_test 81921
 
 # Run out of space in the middle of a block.
-test 87040
+run_test 87040
diff --git a/aos/events/logging/multinode_logger_test_lib.h b/aos/events/logging/multinode_logger_test_lib.h
index 63604d6..8f64f66 100644
--- a/aos/events/logging/multinode_logger_test_lib.h
+++ b/aos/events/logging/multinode_logger_test_lib.h
@@ -76,13 +76,13 @@
 };
 
 constexpr std::string_view kCombinedConfigSha1() {
-  return "32514f3a686e5f8936cc4651e7c81350112f7be8d80dcb8d4afaa29d233c5619";
+  return "71eb8341221fbabefb4ddde43bcebf794fd5855e3ad77786a1db0f9e27a39091";
 }
 constexpr std::string_view kSplitConfigSha1() {
-  return "416da222c09d83325c6f453591d34c7ef12c12c2dd129ddeea657c4bec61b7fd";
+  return "f61d45dc0bda026e852e2da9b3e5c2c7f1c89c9f7958cfba3d02e2c960416f04";
 }
 constexpr std::string_view kReloggedSplitConfigSha1() {
-  return "3fe428684a38298d3323ef087f44517574da3f07dd84b3740829156d6d870108";
+  return "3d8fd3d13955b517ee3d66a50b5e4dd7a13fd648f469d16910990418bcfc6beb";
 }
 
 LoggerState MakeLoggerState(NodeEventLoopFactory *node,
diff --git a/aos/events/ping.rs b/aos/events/ping.rs
index b9725b8..b285860 100644
--- a/aos/events/ping.rs
+++ b/aos/events/ping.rs
@@ -1,87 +1,24 @@
-use aos_configuration as config;
-use aos_events_event_loop_runtime::{EventLoopRuntime, Sender, Watcher};
-use aos_events_shm_event_loop::ShmEventLoop;
-use core::cell::Cell;
-use core::future::Future;
-use core::time::Duration;
-use futures::never::Never;
+use aos::configuration;
+use aos::events::shm_event_loop::ShmEventLoop;
+use aos::init::Init;
+use clap::Parser;
+use ping_lib::PingTask;
 use std::path::Path;
 
-use ping_rust_fbs::aos::examples as ping;
-use pong_rust_fbs::aos::examples as pong;
+/// Ping portion of a ping/pong system.
+#[derive(Parser, Debug)]
+struct App {
+    /// Time to sleep between pings.
+    #[arg(long, default_value_t = 10000, value_name = "MICROS")]
+    sleep: u64,
+}
 
 fn main() {
-    aos_init::init();
-    let config = config::read_config_from(Path::new("pingpong_config.json")).unwrap();
+    let app = App::init();
+    let config = configuration::read_config_from(Path::new("pingpong_config.json")).unwrap();
     let ping = PingTask::new();
     ShmEventLoop::new(&config).run_with(|runtime| {
-        let task = ping.tasks(runtime);
-        runtime.spawn(task);
+        runtime.set_realtime_priority(5);
+        runtime.spawn(ping.tasks(*runtime, app.sleep));
     });
 }
-
-#[derive(Debug)]
-struct PingTask {
-    counter: Cell<i32>,
-}
-
-impl PingTask {
-    pub fn new() -> Self {
-        Self {
-            counter: Cell::new(0),
-        }
-    }
-
-    /// Returns a future with all the tasks for the ping process
-    pub fn tasks(&self, event_loop: &mut EventLoopRuntime) -> impl Future<Output = Never> + '_ {
-        let ping = self.ping(event_loop);
-        let handle_pong = self.handle_pong(event_loop);
-
-        async move {
-            futures::join!(ping, handle_pong);
-            unreachable!("Let's hope `never_type` gets stabilized soon :)");
-        }
-    }
-
-    fn ping(&self, event_loop: &mut EventLoopRuntime) -> impl Future<Output = Never> + '_ {
-        // The sender is used to send messages back to the pong channel.
-        let mut ping_sender: Sender<ping::Ping> = event_loop.make_sender("/test").unwrap();
-        let startup = event_loop.on_run();
-
-        let mut interval = event_loop.add_interval(Duration::from_secs(1));
-
-        async move {
-            // Wait for startup.
-            startup.await;
-            loop {
-                interval.tick().await;
-                self.counter.set(self.counter.get() + 1);
-                let mut builder = ping_sender.make_builder();
-                let mut ping = ping::PingBuilder::new(builder.fbb());
-                let iter = self.counter.get();
-                ping.add_value(iter);
-                let ping = ping.finish();
-                builder.send(ping).expect("Can't send ping");
-            }
-        }
-    }
-
-    fn handle_pong(&self, event_loop: &mut EventLoopRuntime) -> impl Future<Output = Never> + '_ {
-        // The watcher gives us incoming ping messages.
-        let mut pong_watcher: Watcher<pong::Pong> = event_loop.make_watcher("/test").unwrap();
-        let startup = event_loop.on_run();
-
-        async move {
-            // Wait for startup.
-            startup.await;
-            loop {
-                let pong = dbg!(pong_watcher.next().await);
-                assert_eq!(
-                    pong.message().unwrap().value(),
-                    self.counter.get(),
-                    "Missed a reply"
-                );
-            }
-        }
-    }
-}
diff --git a/aos/events/ping_lib.rs b/aos/events/ping_lib.rs
new file mode 100644
index 0000000..ce4d844
--- /dev/null
+++ b/aos/events/ping_lib.rs
@@ -0,0 +1,64 @@
+use aos::events::event_loop_runtime::{EventLoopRuntime, Sender, Watcher};
+use core::cell::Cell;
+use core::time::Duration;
+use futures::never::Never;
+use std::borrow::Borrow;
+
+use ping_rust_fbs::aos::examples as ping;
+use pong_rust_fbs::aos::examples as pong;
+
+#[derive(Debug)]
+pub struct PingTask {
+    counter: Cell<i32>,
+}
+
+impl PingTask {
+    pub fn new() -> Self {
+        Self {
+            counter: Cell::new(0),
+        }
+    }
+
+    /// Returns a future with all the tasks for the ping process
+    #[allow(unreachable_code)]
+    pub async fn tasks(&self, event_loop: EventLoopRuntime<'_>, sleep: u64) -> Never {
+        futures::join!(self.ping(&event_loop, sleep), self.handle_pong(&event_loop));
+        unreachable!("Let's hope `never_type` gets stabilized soon :)");
+    }
+
+    pub async fn ping(&self, event_loop: &EventLoopRuntime<'_>, sleep: u64) -> Never {
+        // The sender is used to send messages back to the pong channel.
+        let mut ping_sender: Sender<ping::Ping> = event_loop.make_sender("/test").unwrap();
+        let mut interval = event_loop.add_interval(Duration::from_micros(sleep));
+
+        let on_run = event_loop.on_run();
+        on_run.borrow().await;
+
+        loop {
+            interval.tick().await;
+            self.counter.set(self.counter.get() + 1);
+            let mut builder = ping_sender.make_builder();
+            let mut ping = ping::PingBuilder::new(builder.fbb());
+            let iter = self.counter.get();
+            log::trace!("Ping: {iter}");
+            ping.add_value(iter);
+            ping.add_send_time(event_loop.monotonic_now().into());
+            let ping = ping.finish();
+            builder.send(ping).expect("Can't send ping");
+        }
+    }
+
+    pub async fn handle_pong(&self, event_loop: &EventLoopRuntime<'_>) -> Never {
+        // The watcher gives us incoming ping messages.
+        let mut pong_watcher: Watcher<pong::Pong> = event_loop.make_watcher("/test").unwrap();
+
+        let on_run = event_loop.on_run();
+        on_run.borrow().await;
+        loop {
+            let pong = pong_watcher.next().await;
+            let pong = pong.message().unwrap();
+            log::trace!("Got pong: {}", pong.value());
+            assert_eq!(pong.value(), self.counter.get(), "Missed a reply");
+        }
+    }
+}
diff --git a/aos/events/pingpong_test.rs b/aos/events/pingpong_test.rs
new file mode 100644
index 0000000..70d8772
--- /dev/null
+++ b/aos/events/pingpong_test.rs
@@ -0,0 +1,114 @@
+#[cfg(test)]
+mod tests {
+    use std::{cell::Cell, time::Duration};
+
+    use aos::configuration::read_config_from;
+    use aos::events::event_loop_runtime::{EventLoopRuntimeHolder, Watcher};
+    use aos::events::simulated_event_loop::{SimulatedEventLoopFactory, SimulatedEventLoopHolder};
+    use aos::testing::init::test_init;
+    use ping_lib::PingTask;
+    use ping_rust_fbs::aos::examples as ping;
+    use pong_rust_fbs::aos::examples as pong;
+    use runfiles::Runfiles;
+
+    // We use this trait to simplify leaking memory. For now, the event loop only allows
+    // data with a `'static` lifetime. Until that restriction is lifted, we may leak
+    // some memory in tests.
+    trait Leak: Sized {
+        fn leak(self) -> &'static mut Self {
+            Box::leak(Box::new(self))
+        }
+    }
+
+    impl<T> Leak for T {}
+
+    #[allow(unused)]
+    struct PingPongTest {
+        ping_event_loop: EventLoopRuntimeHolder<SimulatedEventLoopHolder>,
+        pong_event_loop: EventLoopRuntimeHolder<SimulatedEventLoopHolder>,
+        event_loop_factory: SimulatedEventLoopFactory<'static>,
+    }
+
+    impl PingPongTest {
+        pub fn init() -> PingPongTest {
+            test_init();
+            let r = Runfiles::create().unwrap();
+            let config =
+                read_config_from(&r.rlocation("org_frc971/aos/events/pingpong_config.json"))
+                    .unwrap()
+                    .leak();
+            let mut event_loop_factory = SimulatedEventLoopFactory::new(config);
+
+            let ping_event_loop = event_loop_factory.make_runtime("ping", None, |runtime| {
+                let ping = PingTask::new();
+                runtime.spawn(async move { ping.tasks(runtime, 10000).await });
+            });
+
+            let pong_event_loop = event_loop_factory.make_runtime("pong", None, |runtime| {
+                runtime.spawn(async move { pong_lib::pong(runtime).await })
+            });
+            PingPongTest {
+                event_loop_factory,
+                ping_event_loop,
+                pong_event_loop,
+            }
+        }
+
+        pub fn event_loop(&mut self) -> &mut SimulatedEventLoopFactory<'static> {
+            &mut self.event_loop_factory
+        }
+    }
+
+    #[test]
+    fn starts() {
+        let mut pingpong = PingPongTest::init();
+        pingpong.event_loop().run_for(Duration::from_secs(10));
+    }
+
+    #[test]
+    fn always_replies() {
+        let mut pingpong = PingPongTest::init();
+
+        // For now, the simulated event loop requires all references in the tasks
+        // to be `'static`, so we leak them for now until the restriction is lifted.
+        let ping_count: &Cell<i32> = Cell::new(1).leak();
+        let pong_count: &Cell<i32> = Cell::new(1).leak();
+
+        let _test_runtime = pingpong.event_loop().make_runtime("test", None, |runtime| {
+            let count_pings = async move {
+                let mut ping_watcher: Watcher<ping::Ping> = runtime.make_watcher("/test").unwrap();
+                loop {
+                    let ping = ping_watcher.next().await;
+                    assert_eq!(ping.message().unwrap().value(), ping_count.get());
+                    ping_count.set(ping_count.get() + 1);
+                }
+            };
+            let count_pongs = async move {
+                let mut pong_watcher: Watcher<pong::Pong> = runtime.make_watcher("/test").unwrap();
+                loop {
+                    let pong = pong_watcher.next().await;
+                    assert_eq!(pong.message().unwrap().value(), pong_count.get());
+                    pong_count.set(pong_count.get() + 1);
+                }
+            };
+
+            runtime.spawn(async move {
+                futures::join!(count_pings, count_pongs);
+                unreachable!();
+            });
+        });
+
+        pingpong.event_loop().run_for(Duration::from_secs(10));
+
+        // We run at t=0 and t=10 seconds, which means we run 1 extra time (Note that we started
+        // the count at 1, not 0).
+        assert_eq!(ping_count.get(), 1002);
+        assert_eq!(pong_count.get(), 1002);
+    }
+}
+
+// TODO(adam.snaider): Remove once we don't leak.
+#[no_mangle]
+extern "C" fn __asan_default_options() -> *const u8 {
+    "detect_leaks=0\0".as_ptr()
+}
diff --git a/aos/events/pong.rs b/aos/events/pong.rs
index b817ac2..c2c316b 100644
--- a/aos/events/pong.rs
+++ b/aos/events/pong.rs
@@ -1,42 +1,16 @@
-use aos_configuration as config;
-use aos_events_event_loop_runtime::{EventLoopRuntime, Sender, Watcher};
-use aos_events_shm_event_loop::ShmEventLoop;
-use core::future::Future;
-use futures::never::Never;
+use aos::configuration;
+use aos::events::shm_event_loop::ShmEventLoop;
+use aos::init::{DefaultApp, Init};
 use std::path::Path;
 
-use ping_rust_fbs::aos::examples as ping;
-use pong_rust_fbs::aos::examples as pong;
+use pong_lib::pong;
 
 fn main() {
-    aos_init::init();
-    let config = config::read_config_from(Path::new("pingpong_config.json")).unwrap();
+    let _ = DefaultApp::init();
+    let config = configuration::read_config_from(Path::new("pingpong_config.json")).unwrap();
     ShmEventLoop::new(&config).run_with(|runtime| {
-        let task = pong(runtime);
+        let task = pong(*runtime);
+        runtime.set_realtime_priority(5);
         runtime.spawn(task);
     });
 }
-
-/// Responds to ping messages with an equivalent pong.
-fn pong(event_loop: &mut EventLoopRuntime) -> impl Future<Output = Never> {
-    // The watcher gives us incoming ping messages.
-    let mut ping_watcher: Watcher<ping::Ping> = event_loop.make_watcher("/test").unwrap();
-
-    // The sender is used to send messages back to the pong channel.
-    let mut pong_sender: Sender<pong::Pong> = event_loop.make_sender("/test").unwrap();
-    // Wait for startup.
-    let startup = event_loop.on_run();
-
-    async move {
-        startup.await;
-        loop {
-            let ping = dbg!(ping_watcher.next().await);
-
-            let mut builder = pong_sender.make_builder();
-            let mut pong = pong::PongBuilder::new(builder.fbb());
-            pong.add_value(ping.message().unwrap().value());
-            let pong = pong.finish();
-            builder.send(pong).expect("Can't send pong reponse");
-        }
-    }
-}
diff --git a/aos/events/pong_lib.rs b/aos/events/pong_lib.rs
new file mode 100644
index 0000000..8a0587a
--- /dev/null
+++ b/aos/events/pong_lib.rs
@@ -0,0 +1,30 @@
+use aos::events::event_loop_runtime::{EventLoopRuntime, Sender, Watcher};
+use futures::never::Never;
+use std::borrow::Borrow;
+
+use ping_rust_fbs::aos::examples as ping;
+use pong_rust_fbs::aos::examples as pong;
+
+/// Responds to ping messages with an equivalent pong.
+pub async fn pong(event_loop: EventLoopRuntime<'_>) -> Never {
+    // The watcher gives us incoming ping messages.
+    let mut ping_watcher: Watcher<ping::Ping> = event_loop.make_watcher("/test").unwrap();
+
+    // The sender is used to send messages back to the pong channel.
+    let mut pong_sender: Sender<pong::Pong> = event_loop.make_sender("/test").unwrap();
+
+    let on_run = event_loop.on_run();
+    on_run.borrow().await;
+    loop {
+        let ping = ping_watcher.next().await;
+        let ping = ping.message().unwrap();
+        log::info!("Got ping: {}", ping.value());
+
+        let mut builder = pong_sender.make_builder();
+        let mut pong = pong::PongBuilder::new(builder.fbb());
+        pong.add_value(ping.value());
+        pong.add_initial_send_time(event_loop.monotonic_now().into());
+        let pong = pong.finish();
+        builder.send(pong).expect("Can't send pong reponse");
+    }
+}
diff --git a/aos/events/shm_event_loop.rs b/aos/events/shm_event_loop.rs
index 880f72b..ee6df18 100644
--- a/aos/events/shm_event_loop.rs
+++ b/aos/events/shm_event_loop.rs
@@ -1,6 +1,7 @@
 pub use aos_configuration::{Configuration, ConfigurationExt};
-pub use aos_events_event_loop_runtime::EventLoop;
-pub use aos_events_event_loop_runtime::{CppExitHandle, EventLoopRuntime, ExitHandle};
+pub use aos_events_event_loop_runtime::{
+    CppEventLoop, CppEventLoopRuntime, CppExitHandle, EventLoopRuntime, ExitHandle,
+};
 
 use aos_configuration_fbs::aos::Configuration as RustConfiguration;
 use aos_flatbuffers::{transmute_table_to, Flatbuffer};
@@ -20,7 +21,7 @@
 
 extern_cpp_type!("aos::ExitHandle", crate::CppExitHandle)
 extern_cpp_type!("aos::Configuration", crate::Configuration)
-extern_cpp_type!("aos::EventLoop", crate::EventLoop)
+extern_cpp_type!("aos::EventLoop", crate::CppEventLoop)
 );
 
 /// A Rust-owned C++ `ShmEventLoop` object.
@@ -65,6 +66,7 @@
     /// # use aos_events_shm_event_loop::*;
     /// use ping_rust_fbs::aos::examples as ping;
     /// use pong_rust_fbs::aos::examples as pong;
+    /// use std::borrow::Borrow;
     /// use std::cell::Cell;
     /// use std::path::Path;
     /// use aos_configuration::read_config_from;
@@ -81,7 +83,7 @@
     ///   let on_run = runtime.on_run();
     ///   // Sends a single ping message.
     ///   let send_task = async move {
-    ///     on_run.await;
+    ///     on_run.borrow().await;
     ///     let mut builder = sender.make_builder();
     ///     let mut ping = ping::PingBuilder::new(builder.fbb());
     ///     ping.add_value(10);
@@ -163,14 +165,16 @@
     /// ```
     pub fn run_with<'env, F>(mut self, fun: F)
     where
-        F: for<'event_loop> FnOnce(&mut Scoped<'event_loop, 'env, EventLoopRuntime<'event_loop>>),
+        F: for<'event_loop> FnOnce(Scoped<'event_loop, 'env, EventLoopRuntime<'event_loop>>),
     {
         // SAFETY: The runtime and the event loop (i.e. self) both get destroyed at the end of this
         // scope: first the runtime followed by the event loop. The runtime gets exclusive access
         // during initialization in `fun` while the event loop remains unused.
-        let runtime = unsafe { EventLoopRuntime::new(self.inner.as_mut().event_loop_mut()) };
-        let mut runtime = Scoped::new(runtime);
-        fun(&mut runtime);
+        let cpp_runtime =
+            unsafe { CppEventLoopRuntime::new(self.inner.as_mut().event_loop_mut()).within_box() };
+        let runtime = unsafe { EventLoopRuntime::new(&cpp_runtime) };
+        let runtime = Scoped::new(runtime);
+        fun(runtime);
         self.run();
     }
 
@@ -195,6 +199,7 @@
 /// any `'scope`, this allows the compiler to propagate lifetime bounds which
 /// outlive any of the possible `'scope`. This is the simplest way to express
 /// this concept to the compiler right now.
+#[derive(Clone, Copy)]
 pub struct Scoped<'scope, 'env: 'scope, T: 'scope> {
     data: T,
     _env: PhantomData<fn(&'env ()) -> &'env ()>,
@@ -235,6 +240,7 @@
     use aos_events_event_loop_runtime::{Sender, Watcher};
     use aos_test_init::test_init;
     use ping_rust_fbs::aos::examples as ping;
+    use std::borrow::Borrow;
     use std::sync::atomic::{AtomicUsize, Ordering};
     use std::sync::Barrier;
 
@@ -259,12 +265,11 @@
                 let mut event_loop = ShmEventLoop::new(config);
                 let exit_handle = event_loop.make_exit_handle();
                 event_loop.run_with(|runtime| {
-                    let mut watcher: Watcher<ping::Ping> = runtime
-                        .make_watcher("/test")
-                        .expect("Can't create `Ping` watcher");
-                    let on_run = runtime.on_run();
                     runtime.spawn(async move {
-                        on_run.await;
+                        let mut watcher: Watcher<ping::Ping> = runtime
+                            .make_watcher("/test")
+                            .expect("Can't create `Ping` watcher");
+                        runtime.on_run().borrow().await;
                         barrier.wait();
                         let ping = watcher.next().await;
                         assert_eq!(ping.message().unwrap().value(), VALUE);
@@ -277,12 +282,11 @@
                 let mut event_loop = ShmEventLoop::new(config);
                 let exit_handle = event_loop.make_exit_handle();
                 event_loop.run_with(|runtime| {
-                    let mut sender: Sender<ping::Ping> = runtime
-                        .make_sender("/test")
-                        .expect("Can't create `Ping` sender");
-                    let on_run = runtime.on_run();
                     runtime.spawn(async move {
-                        on_run.await;
+                        let mut sender: Sender<ping::Ping> = runtime
+                            .make_sender("/test")
+                            .expect("Can't create `Ping` sender");
+                        runtime.on_run().borrow().await;
                         // Give the waiting thread a chance to start.
                         barrier.wait();
                         let mut sender = sender.make_builder();
diff --git a/aos/events/simulated_event_loop.rs b/aos/events/simulated_event_loop.rs
index 90c4c86..c75f34e 100644
--- a/aos/events/simulated_event_loop.rs
+++ b/aos/events/simulated_event_loop.rs
@@ -1,11 +1,11 @@
-use std::{marker::PhantomData, pin::Pin, ptr};
+use std::{marker::PhantomData, pin::Pin, ptr, time::Duration};
 
 use autocxx::WithinBox;
 use cxx::UniquePtr;
 
 pub use aos_configuration::{Channel, Configuration, ConfigurationExt, Node};
 use aos_configuration_fbs::aos::Configuration as RustConfiguration;
-pub use aos_events_event_loop_runtime::{CppExitHandle, EventLoop, ExitHandle};
+pub use aos_events_event_loop_runtime::{CppEventLoop, CppExitHandle, ExitHandle};
 use aos_events_event_loop_runtime::{EventLoopHolder, EventLoopRuntime, EventLoopRuntimeHolder};
 use aos_flatbuffers::{transmute_table_to, Flatbuffer};
 
@@ -20,7 +20,7 @@
 extern_cpp_type!("aos::ExitHandle", crate::CppExitHandle)
 extern_cpp_type!("aos::Configuration", crate::Configuration)
 extern_cpp_type!("aos::Node", crate::Node)
-extern_cpp_type!("aos::EventLoop", crate::EventLoop)
+extern_cpp_type!("aos::EventLoop", crate::CppEventLoop)
 );
 
 /// A Rust-owned C++ `SimulatedEventLoopFactory` object.
@@ -60,9 +60,9 @@
     /// Creates a Rust-owned EventLoop.
     ///
     /// You probably don't want to call this directly if you're creating a Rust application. This
-    /// is intended for creating C++ applications. Use [`make_runtime`] instead when creating Rust
+    /// is intended for creating C++ applications. Use [`Self::make_runtime`] instead when creating Rust
     /// applications.
-    pub fn make_event_loop(&mut self, name: &str, node: Option<&Node>) -> UniquePtr<EventLoop> {
+    pub fn make_event_loop(&mut self, name: &str, node: Option<&Node>) -> UniquePtr<CppEventLoop> {
         // SAFETY:
         // * `self` has a valid C++ object.
         // * C++ doesn't need the lifetimes of `name` or `node` to last any longer than this method
@@ -77,7 +77,7 @@
         }
     }
 
-    /// Creates an [`EventLoopRuntime`] wrapper which also owns its underlying [`EventLoop`].
+    /// Creates an [`EventLoopRuntime`] wrapper which also owns its underlying [`CppEventLoop`].
     ///
     /// All setup must be performed with `fun`, which is called before this function returns. `fun`
     /// may create further objects to use in async functions via [`EventLoop.spawn`] etc, but it is
@@ -91,7 +91,7 @@
         fun: F,
     ) -> EventLoopRuntimeHolder<SimulatedEventLoopHolder>
     where
-        F: for<'event_loop> FnOnce(&mut EventLoopRuntime<'event_loop>),
+        F: for<'event_loop> FnOnce(EventLoopRuntime<'event_loop>),
     {
         let event_loop = self.make_event_loop(name, node);
         // SAFETY: We just created this EventLoop, so we are the exclusive owner of it.
@@ -107,16 +107,25 @@
         self.as_mut().Run();
     }
 
+    pub fn run_for(&mut self, duration: Duration) {
+        self.as_mut().RunFor(
+            duration
+                .as_nanos()
+                .try_into()
+                .expect("Out of range: Internal clock uses 64 bits"),
+        );
+    }
+
     // TODO(Brian): Expose OnStartup. Just take a callback for creating things, and rely on
     // dropping the created objects instead of OnShutdown.
     // pub fn spawn_on_startup(&mut self, spawner: impl FnMut());
 }
 
-pub struct SimulatedEventLoopHolder(UniquePtr<EventLoop>);
+pub struct SimulatedEventLoopHolder(UniquePtr<CppEventLoop>);
 
 impl SimulatedEventLoopHolder {
     /// SAFETY: `event_loop` must be the exclusive owner of the underlying EventLoop.
-    pub unsafe fn new(event_loop: UniquePtr<EventLoop>) -> Self {
+    pub unsafe fn new(event_loop: UniquePtr<CppEventLoop>) -> Self {
         Self(event_loop)
     }
 }
@@ -124,12 +133,11 @@
 // SAFETY: The UniquePtr functions we're using here mirror most of the EventLoopHolder requirements
 // exactly. Safety requirements on [`SimulatedEventLoopHolder.new`] take care of the rest.
 unsafe impl EventLoopHolder for SimulatedEventLoopHolder {
-    fn into_raw(self) -> *mut ffi::aos::EventLoop {
-        self.0.into_raw()
-    }
-
-    unsafe fn from_raw(raw: *mut ffi::aos::EventLoop) -> Self {
-        Self(UniquePtr::from_raw(raw))
+    fn as_raw(&self) -> *const CppEventLoop {
+        self.0
+            .as_ref()
+            .map(|event_loop| event_loop as *const CppEventLoop)
+            .unwrap_or(core::ptr::null())
     }
 }
 
diff --git a/aos/events/simulated_event_loop_for_rust.h b/aos/events/simulated_event_loop_for_rust.h
index a8d630c..663d163 100644
--- a/aos/events/simulated_event_loop_for_rust.h
+++ b/aos/events/simulated_event_loop_for_rust.h
@@ -17,6 +17,9 @@
   }
 
   void Run() { factory_.Run(); }
+  void RunFor(int64_t nanos) {
+    factory_.RunFor(std::chrono::nanoseconds(nanos));
+  }
 
   std::unique_ptr<ExitHandle> MakeExitHandle() {
     return factory_.MakeExitHandle();
diff --git a/aos/flatbuffers.h b/aos/flatbuffers.h
index a299fe9..607303c 100644
--- a/aos/flatbuffers.h
+++ b/aos/flatbuffers.h
@@ -118,6 +118,11 @@
   // make attempts to use it fail more obviously.
   void Wipe() { memset(span().data(), 0, span().size()); }
 
+  // Returns true if the flatbuffer is valid. Returns false if either:
+  // * The flatbuffer is incorrectly constructed (e.g., it points to memory
+  // locations outside of the current memory buffer).
+  // * The flatbuffer is too complex, and the flatbuffer verifier chosen to bail
+  // when attempting to traverse the tree of tables.
   bool Verify() const {
     if (span().size() < 4u) {
       return false;
diff --git a/aos/flatbuffers/BUILD b/aos/flatbuffers/BUILD
index 08d548f..32f1d39 100644
--- a/aos/flatbuffers/BUILD
+++ b/aos/flatbuffers/BUILD
@@ -102,6 +102,7 @@
         ":test_schema",
         "//aos:flatbuffers",
         "//aos:json_to_flatbuffer",
+        "//aos/flatbuffers/test_dir:include_reflection_fbs",
         "//aos/flatbuffers/test_dir:type_coverage_fbs",
         "//aos/testing:googletest",
         "//aos/testing:path",
diff --git a/aos/flatbuffers/builder.h b/aos/flatbuffers/builder.h
index db89d10..36225c0 100644
--- a/aos/flatbuffers/builder.h
+++ b/aos/flatbuffers/builder.h
@@ -77,6 +77,9 @@
   FlatbufferSpan<typename T::Flatbuffer> AsFlatbufferSpan() {
     return {buffer()};
   }
+  FlatbufferSpan<const typename T::Flatbuffer> AsFlatbufferSpan() const {
+    return {buffer()};
+  }
 
   // Returns true if the flatbuffer is validly constructed. Should always return
   // true (barring some sort of memory corruption). Exposed for convenience.
diff --git a/aos/flatbuffers/reflection/BUILD.bazel b/aos/flatbuffers/reflection/BUILD.bazel
new file mode 100644
index 0000000..475f2a2
--- /dev/null
+++ b/aos/flatbuffers/reflection/BUILD.bazel
@@ -0,0 +1,18 @@
+load("@aspect_bazel_lib//lib:copy_file.bzl", "copy_file")
+load("//aos/flatbuffers:generate.bzl", "static_flatbuffer")
+
+copy_file(
+    name = "reflection_fbs_copy",
+    src = "@com_github_google_flatbuffers//reflection:reflection_fbs_schema",
+    out = "reflection.fbs",
+)
+
+# This autogenerates both a reflection_static.h and a reflection_generated.h.
+# However, in order to avoid having two conflicting headers floating around,
+# we forcibly override the #include to use flatbuffers/reflection_generated.h
+# in static_flatbuffers.cc
+static_flatbuffer(
+    name = "reflection_fbs",
+    srcs = ["reflection.fbs"],
+    visibility = ["//visibility:public"],
+)
diff --git a/aos/flatbuffers/static_flatbuffers.cc b/aos/flatbuffers/static_flatbuffers.cc
index 8d8942c..c1b617f 100644
--- a/aos/flatbuffers/static_flatbuffers.cc
+++ b/aos/flatbuffers/static_flatbuffers.cc
@@ -19,6 +19,8 @@
   bool is_inline = true;
   // Whether this is a struct or not.
   bool is_struct = false;
+  // Whether this is a repeated type (vector or string).
+  bool is_repeated = false;
   // Full C++ type of this field.
   std::string full_type = "";
   // Full flatbuffer type for this field.
@@ -118,6 +120,22 @@
 
 const std::string IncludePathForFbs(
     std::string_view fbs_file, std::string_view include_suffix = "static") {
+  // Special case for the reflection_generated.h, which is checked into the
+  // repo.
+  // Note that we *do* autogenerated the reflection_static.h but that because
+  // it uses a special import path, we end up overriding the include anyways
+  // (note that we could muck around with the paths on the bazel side to instead
+  // get a cc_library with the correct include paths specified, although it is
+  // not clear that that would be any simpler than the extra else-if).
+  if (fbs_file == "reflection/reflection.fbs") {
+    if (include_suffix == "generated") {
+      return "flatbuffers/reflection_generated.h";
+    } else if (include_suffix == "static") {
+      return "aos/flatbuffers/reflection/reflection_static.h";
+    } else {
+      LOG(FATAL) << "This should be unreachable.";
+    }
+  }
   fbs_file.remove_suffix(4);
   return absl::StrCat(fbs_file, "_", include_suffix, ".h");
 }
@@ -151,12 +169,14 @@
       // straightforwards.
       field->is_inline = true;
       field->is_struct = false;
+      field->is_repeated = false;
       field->full_type =
           ScalarOrEnumType(schema, type->base_type(), type->index());
       return;
     case reflection::BaseType::String: {
       field->is_inline = false;
       field->is_struct = false;
+      field->is_repeated = true;
       field->full_type =
           absl::StrFormat("::aos::fbs::String<%d>",
                           GetLengthAttributeOrZero(field_fbs, "static_length"));
@@ -166,6 +186,7 @@
       // We need to extract the name of the elements of the vector.
       std::string element_type;
       bool elements_are_inline = true;
+      field->is_repeated = true;
       if (type->base_type() == reflection::BaseType::Vector) {
         switch (type->element()) {
           case reflection::BaseType::Obj: {
@@ -207,6 +228,7 @@
       const reflection::Object *object = GetObject(schema, type->index());
       field->is_inline = object->is_struct();
       field->is_struct = object->is_struct();
+      field->is_repeated = false;
       const std::string flatbuffer_name =
           FlatbufferNameToCppName(object->name()->string_view());
       if (field->is_inline) {
@@ -437,27 +459,97 @@
                          absl::StrJoin(clearers, "\n"));
 }
 
+// Creates the FromFlatbuffer() method that copies from a flatbuffer object API
+// object (i.e., the FlatbufferT types).
+std::string MakeObjectCopier(const std::vector<FieldData> &fields) {
+  std::vector<std::string> copiers;
+  for (const FieldData &field : fields) {
+    if (field.is_struct) {
+      // Structs are stored as unique_ptr<FooStruct>
+      copiers.emplace_back(absl::StrFormat(R"code(
+      if (other.%s) {
+        set_%s(*other.%s);
+      }
+      )code",
+                                           field.name, field.name, field.name));
+    } else if (field.is_inline) {
+      // Inline non-struct elements are stored as FooType.
+      copiers.emplace_back(absl::StrFormat(R"code(
+      set_%s(other.%s);
+      )code",
+                                           field.name, field.name));
+    } else if (field.is_repeated) {
+      // strings are stored as std::string's.
+      // vectors are stored as std::vector's.
+      copiers.emplace_back(absl::StrFormat(R"code(
+      // Unconditionally copy strings/vectors, even if it will just end up
+      // being 0-length (this maintains consistency with the flatbuffer Pack()
+      // behavior).
+      if (!CHECK_NOTNULL(add_%s())->FromFlatbuffer(other.%s)) {
+        // Fail if we were unable to copy (e.g., if we tried to copy in a long
+        // vector and do not have the space for it).
+        return false;
+      }
+      )code",
+                                           field.name, field.name));
+    } else {
+      // Tables are stored as unique_ptr<FooTable>
+      copiers.emplace_back(absl::StrFormat(R"code(
+      if (other.%s) {
+        if (!CHECK_NOTNULL(add_%s())->FromFlatbuffer(*other.%s)) {
+          // Fail if we were unable to copy (e.g., if we tried to copy in a long
+          // vector and do not have the space for it).
+          return false;
+        }
+      }
+      )code",
+                                           field.name, field.name, field.name));
+    }
+  }
+  return absl::StrFormat(
+      R"code(
+  // Copies the contents of the provided flatbuffer into this flatbuffer,
+  // returning true on success.
+  // Because the Flatbuffer Object API does not provide any concept of an
+  // optionally populated scalar field, all scalar fields will be populated
+  // after a call to FromFlatbufferObject().
+  // This is a deep copy, and will call FromFlatbufferObject on
+  // any constituent objects.
+  [[nodiscard]] bool FromFlatbuffer(const Flatbuffer::NativeTableType &other) {
+    Clear();
+    %s
+    return true;
+  }
+  [[nodiscard]] bool FromFlatbuffer(const flatbuffers::unique_ptr<Flatbuffer::NativeTableType>& other) {
+    return FromFlatbuffer(*other);
+  }
+)code",
+      absl::StrJoin(copiers, "\n"));
+}
+
+// Creates the FromFlatbuffer() method that copies from an actual flatbuffer
+// object.
 std::string MakeCopier(const std::vector<FieldData> &fields) {
   std::vector<std::string> copiers;
   for (const FieldData &field : fields) {
     if (field.is_struct) {
       copiers.emplace_back(absl::StrFormat(R"code(
-      if (other->has_%s()) {
-        set_%s(*other->%s());
+      if (other.has_%s()) {
+        set_%s(*other.%s());
       }
       )code",
                                            field.name, field.name, field.name));
     } else if (field.is_inline) {
       copiers.emplace_back(absl::StrFormat(R"code(
-      if (other->has_%s()) {
-        set_%s(other->%s());
+      if (other.has_%s()) {
+        set_%s(other.%s());
       }
       )code",
                                            field.name, field.name, field.name));
     } else {
       copiers.emplace_back(absl::StrFormat(R"code(
-      if (other->has_%s()) {
-        if (!CHECK_NOTNULL(add_%s())->FromFlatbuffer(other->%s())) {
+      if (other.has_%s()) {
+        if (!CHECK_NOTNULL(add_%s())->FromFlatbuffer(other.%s())) {
           // Fail if we were unable to copy (e.g., if we tried to copy in a long
           // vector and do not have the space for it).
           return false;
@@ -473,11 +565,16 @@
   // returning true on success.
   // This is a deep copy, and will call FromFlatbuffer on any constituent
   // objects.
-  [[nodiscard]] bool FromFlatbuffer(const Flatbuffer *other) {
+  [[nodiscard]] bool FromFlatbuffer(const Flatbuffer &other) {
     Clear();
     %s
     return true;
   }
+  // Equivalent to FromFlatbuffer(const Flatbuffer&); this overload is provided
+  // to ease implementation of the aos::fbs::Vector internals.
+  [[nodiscard]] bool FromFlatbuffer(const Flatbuffer *other) {
+    return FromFlatbuffer(*CHECK_NOTNULL(other));
+  }
 )code",
       absl::StrJoin(copiers, "\n"));
 }
@@ -689,6 +786,7 @@
   public:
   // The underlying "raw" flatbuffer type for this type.
   typedef %s Flatbuffer;
+  typedef flatbuffers::unique_ptr<Flatbuffer::NativeTableType> FlatbufferObjectType;
   // Returns this object as a flatbuffer type. This reference may not be valid
   // following mutations to the underlying flatbuffer, due to how memory may get
   // may get moved around.
@@ -699,6 +797,7 @@
 %s
 %s
 %s
+%s
   private:
 %s
 %s
@@ -708,7 +807,7 @@
   )code",
       type_namespace, type_name, FlatbufferNameToCppName(fbs_type_name),
       constants, MakeConstructor(type_name), type_name, accessors,
-      MakeFullClearer(fields), MakeCopier(fields),
+      MakeFullClearer(fields), MakeCopier(fields), MakeObjectCopier(fields),
       MakeMoveConstructor(type_name), members, MakeSubObjectList(fields));
 
   GeneratedObject result;
diff --git a/aos/flatbuffers/static_flatbuffers_test.cc b/aos/flatbuffers/static_flatbuffers_test.cc
index fd95db3..52fa01e 100644
--- a/aos/flatbuffers/static_flatbuffers_test.cc
+++ b/aos/flatbuffers/static_flatbuffers_test.cc
@@ -10,6 +10,7 @@
 #include "aos/flatbuffers.h"
 #include "aos/flatbuffers/builder.h"
 #include "aos/flatbuffers/interesting_schemas.h"
+#include "aos/flatbuffers/test_dir/include_reflection_static.h"
 #include "aos/flatbuffers/test_dir/type_coverage_static.h"
 #include "aos/flatbuffers/test_schema.h"
 #include "aos/flatbuffers/test_static.h"
@@ -1042,4 +1043,55 @@
   TestMemory(builder.buffer());
 }
 
+// Uses a small example to manually verify that we can copy from the flatbuffer
+// object API.
+TEST_F(StaticFlatbuffersTest, ObjectApiCopy) {
+  aos::fbs::testing::TestTableT object_t;
+  object_t.scalar = 971;
+  object_t.vector_of_strings.push_back("971");
+  object_t.vector_of_structs.push_back({1, 2});
+  object_t.subtable = std::make_unique<SubTableT>();
+  aos::fbs::VectorAllocator allocator;
+  Builder<TestTableStatic> builder(&allocator);
+  ASSERT_TRUE(builder->FromFlatbuffer(object_t));
+  ASSERT_TRUE(builder.AsFlatbufferSpan().Verify());
+  // Note that vectors and strings get set to zero-length, but present, values.
+  EXPECT_EQ(
+      "{ \"scalar\": 971, \"vector_of_scalars\": [  ], \"string\": \"\", "
+      "\"vector_of_strings\": [ \"971\" ], \"subtable\": { \"foo\": 0, "
+      "\"baz\": 0.0 }, \"vector_aligned\": [  ], \"vector_of_structs\": [ { "
+      "\"x\": 1.0, \"y\": 2.0 } ], \"vector_of_tables\": [  ], "
+      "\"unspecified_length_vector\": [  ], \"unspecified_length_string\": "
+      "\"\", \"unspecified_length_vector_of_strings\": [  ] }",
+      aos::FlatbufferToJson(builder.AsFlatbufferSpan()));
+}
+
+// More completely covers our object API copying by comparing the flatbuffer
+// Pack() methods to our FromFlatbuffer() methods.
+TEST_F(StaticFlatbuffersTest, FlatbufferObjectTypeCoverage) {
+  VerifyJson<aos::testing::ConfigurationStatic>("{\n\n}");
+  std::string populated_config =
+      aos::util::ReadFileToStringOrDie(aos::testing::ArtifactPath(
+          "aos/flatbuffers/test_dir/type_coverage.json"));
+  Builder<aos::testing::ConfigurationStatic> json_builder =
+      aos::JsonToStaticFlatbuffer<aos::testing::ConfigurationStatic>(
+          populated_config);
+  aos::testing::ConfigurationT object_t;
+  json_builder->AsFlatbuffer().UnPackTo(&object_t);
+
+  Builder<aos::testing::ConfigurationStatic> from_object_static;
+  ASSERT_TRUE(from_object_static->FromFlatbuffer(object_t));
+  flatbuffers::FlatBufferBuilder fbb;
+  fbb.Finish(aos::testing::Configuration::Pack(fbb, &object_t));
+  aos::FlatbufferDetachedBuffer<aos::testing::Configuration> from_object_raw =
+      fbb.Release();
+  EXPECT_EQ(aos::FlatbufferToJson(from_object_raw, {.multi_line = true}),
+            aos::FlatbufferToJson(from_object_static, {.multi_line = true}));
+}
+
+// Tests that we can build code that uses the reflection types.
+TEST_F(StaticFlatbuffersTest, IncludeReflectionTypes) {
+  VerifyJson<::aos::testing::UseSchemaStatic>("{\n\n}");
+}
+
 }  // namespace aos::fbs::testing
diff --git a/aos/flatbuffers/static_vector.h b/aos/flatbuffers/static_vector.h
index 7349a8d..6133075 100644
--- a/aos/flatbuffers/static_vector.h
+++ b/aos/flatbuffers/static_vector.h
@@ -203,6 +203,11 @@
       typename internal::InlineWrapper<T, kInline>::FlatbufferType;
   using ConstFlatbufferType =
       typename internal::InlineWrapper<T, kInline>::ConstFlatbufferType;
+  // FlatbufferObjectType corresponds to the type used by the flatbuffer
+  // "object" API (i.e. the FlatbufferT types).
+  // This type will be something unintelligble for inline types.
+  using FlatbufferObjectType =
+      typename internal::InlineWrapper<T, kInline>::FlatbufferObjectType;
   // flatbuffers::Vector type that corresponds to this Vector.
   typedef flatbuffers::Vector<FlatbufferType> Flatbuffer;
   typedef const flatbuffers::Vector<ConstFlatbufferType> ConstFlatbuffer;
@@ -329,7 +334,70 @@
   // we can allocate through reserve()).
   // This is a deep copy, and will call FromFlatbuffer on any constituent
   // objects.
-  [[nodiscard]] bool FromFlatbuffer(ConstFlatbuffer *vector);
+  [[nodiscard]] bool FromFlatbuffer(ConstFlatbuffer *vector) {
+    return FromFlatbuffer(*CHECK_NOTNULL(vector));
+  }
+  [[nodiscard]] bool FromFlatbuffer(ConstFlatbuffer &vector);
+  // The remaining FromFlatbuffer() overloads are for when using the flatbuffer
+  // "object" API, which uses std::vector's for representing vectors.
+  [[nodiscard]] bool FromFlatbuffer(const std::vector<InlineType> &vector) {
+    static_assert(kInline);
+    return FromData(vector.data(), vector.size());
+  }
+  // Overload for vectors of bools, since the standard library may not use a
+  // full byte per vector element.
+  [[nodiscard]] bool FromFlatbuffer(const std::vector<bool> &vector) {
+    static_assert(kInline);
+    // We won't be able to do a clean memcpy because std::vector<bool> may be
+    // implemented using bit-packing.
+    return FromIterator(vector.cbegin(), vector.cend());
+  }
+  // Overload for non-inline types. Note that to avoid having this overload get
+  // resolved with inline types, we make FlatbufferObjectType != InlineType.
+  [[nodiscard]] bool FromFlatbuffer(
+      const std::vector<FlatbufferObjectType> &vector) {
+    static_assert(!kInline);
+    return FromNotInlineIterable(vector);
+  }
+
+  // Copies values from the provided data pointer into the vector, resizing the
+  // vector as needed to match. Returns false on failure (e.g., if the
+  // underlying allocator has insufficient space to perform the copy). Only
+  // works for inline data types.
+  [[nodiscard]] bool FromData(const InlineType *input_data, size_t input_size) {
+    static_assert(kInline);
+    if (!reserve(input_size)) {
+      return false;
+    }
+
+    // We will be overwriting the whole vector very shortly; there is no need to
+    // clear the buffer to zero.
+    resize_inline(input_size, SetZero::kNo);
+
+    memcpy(inline_data(), input_data, size() * sizeof(InlineType));
+    return true;
+  }
+
+  // Copies values from the provided iterators into the vector, resizing the
+  // vector as needed to match. Returns false on failure (e.g., if the
+  // underlying allocator has insufficient space to perform the copy). Only
+  // works for inline data types.
+  // Does not attempt any optimizations if the iterators meet the
+  // std::contiguous_iterator concept; instead, it simply copies each element
+  // out one-by-one.
+  template <typename Iterator>
+  [[nodiscard]] bool FromIterator(Iterator begin, Iterator end) {
+    static_assert(kInline);
+    resize(0);
+    for (Iterator it = begin; it != end; ++it) {
+      if (!reserve(size() + 1)) {
+        return false;
+      }
+      // Should never fail, due to the reserve() above.
+      CHECK(emplace_back(*it));
+    }
+    return true;
+  }
 
   // Returns the element at the provided index. index must be less than size().
   const T &at(size_t index) const {
@@ -569,29 +637,22 @@
   }
   // Implementation that handles copying from a flatbuffers::Vector of an inline
   // data type.
-  [[nodiscard]] bool FromInlineFlatbuffer(ConstFlatbuffer *vector) {
-    if (!reserve(CHECK_NOTNULL(vector)->size())) {
-      return false;
-    }
-
-    // We will be overwriting the whole vector very shortly; there is no need to
-    // clear the buffer to zero.
-    resize_inline(vector->size(), SetZero::kNo);
-
-    memcpy(inline_data(), vector->Data(), size() * sizeof(InlineType));
-    return true;
+  [[nodiscard]] bool FromInlineFlatbuffer(ConstFlatbuffer &vector) {
+    return FromData(reinterpret_cast<const InlineType *>(vector.Data()),
+                    vector.size());
   }
 
   // Implementation that handles copying from a flatbuffers::Vector of a
   // not-inline data type.
-  [[nodiscard]] bool FromNotInlineFlatbuffer(const Flatbuffer *vector) {
-    if (!reserve(vector->size())) {
+  template <typename Iterable>
+  [[nodiscard]] bool FromNotInlineIterable(const Iterable &vector) {
+    if (!reserve(vector.size())) {
       return false;
     }
     // "Clear" the vector.
     resize_not_inline(0);
 
-    for (const typename T::Flatbuffer *entry : *vector) {
+    for (const auto &entry : vector) {
       if (!CHECK_NOTNULL(emplace_back())->FromFlatbuffer(entry)) {
         return false;
       }
@@ -599,6 +660,10 @@
     return true;
   }
 
+  [[nodiscard]] bool FromNotInlineFlatbuffer(const Flatbuffer &vector) {
+    return FromNotInlineIterable(vector);
+  }
+
   // In order to allow for easy partial template specialization, we use a
   // non-member class to call FromInline/FromNotInlineFlatbuffer and
   // resize_inline/resize_not_inline. There are not actually any great ways to
@@ -659,6 +724,7 @@
  public:
   typedef Vector<char, kStaticLength, true, 0, true> VectorType;
   typedef flatbuffers::String Flatbuffer;
+  typedef std::string FlatbufferObjectType;
   String(std::span<uint8_t> buffer, ResizeableObject *parent)
       : VectorType(buffer, parent) {}
   virtual ~String() {}
@@ -667,6 +733,10 @@
     VectorType::resize_inline(string.size(), SetZero::kNo);
     memcpy(VectorType::data(), string.data(), string.size());
   }
+  using VectorType::FromFlatbuffer;
+  [[nodiscard]] bool FromFlatbuffer(const std::string &string) {
+    return VectorType::FromData(string.data(), string.size());
+  }
   std::string_view string_view() const {
     return std::string_view(VectorType::data(), VectorType::size());
   }
@@ -690,12 +760,13 @@
   typedef T ObjectType;
   typedef flatbuffers::Offset<typename T::Flatbuffer> FlatbufferType;
   typedef flatbuffers::Offset<typename T::Flatbuffer> ConstFlatbufferType;
+  typedef T::FlatbufferObjectType FlatbufferObjectType;
   static_assert((T::kSize % T::kAlign) == 0);
   static constexpr size_t kDataAlign = T::kAlign;
   static constexpr size_t kDataSize = T::kSize;
   template <typename StaticVector>
   static bool FromFlatbuffer(
-      StaticVector *to, const typename StaticVector::ConstFlatbuffer *from) {
+      StaticVector *to, const typename StaticVector::ConstFlatbuffer &from) {
     return to->FromNotInlineFlatbuffer(from);
   }
   template <typename StaticVector>
@@ -712,11 +783,12 @@
   typedef T ObjectType;
   typedef T FlatbufferType;
   typedef T ConstFlatbufferType;
+  typedef T *FlatbufferObjectType;
   static constexpr size_t kDataAlign = alignof(T);
   static constexpr size_t kDataSize = sizeof(T);
   template <typename StaticVector>
   static bool FromFlatbuffer(
-      StaticVector *to, const typename StaticVector::ConstFlatbuffer *from) {
+      StaticVector *to, const typename StaticVector::ConstFlatbuffer &from) {
     return to->FromInlineFlatbuffer(from);
   }
   template <typename StaticVector>
@@ -731,11 +803,12 @@
   typedef uint8_t ObjectType;
   typedef uint8_t FlatbufferType;
   typedef uint8_t ConstFlatbufferType;
+  typedef uint8_t *FlatbufferObjectType;
   static constexpr size_t kDataAlign = 1u;
   static constexpr size_t kDataSize = 1u;
   template <typename StaticVector>
   static bool FromFlatbuffer(
-      StaticVector *to, const typename StaticVector::ConstFlatbuffer *from) {
+      StaticVector *to, const typename StaticVector::ConstFlatbuffer &from) {
     return to->FromInlineFlatbuffer(from);
   }
   template <typename StaticVector>
@@ -753,11 +826,12 @@
   typedef T ObjectType;
   typedef T *FlatbufferType;
   typedef const T *ConstFlatbufferType;
+  typedef T *FlatbufferObjectType;
   static constexpr size_t kDataAlign = alignof(T);
   static constexpr size_t kDataSize = sizeof(T);
   template <typename StaticVector>
   static bool FromFlatbuffer(
-      StaticVector *to, const typename StaticVector::ConstFlatbuffer *from) {
+      StaticVector *to, const typename StaticVector::ConstFlatbuffer &from) {
     return to->FromInlineFlatbuffer(from);
   }
   template <typename StaticVector>
@@ -770,7 +844,7 @@
 template <typename T, size_t kStaticLength, bool kInline, size_t kForceAlign,
           bool kNullTerminate>
 bool Vector<T, kStaticLength, kInline, kForceAlign,
-            kNullTerminate>::FromFlatbuffer(ConstFlatbuffer *vector) {
+            kNullTerminate>::FromFlatbuffer(ConstFlatbuffer &vector) {
   return internal::InlineWrapper<T, kInline>::FromFlatbuffer(this, vector);
 }
 
diff --git a/aos/flatbuffers/test_dir/BUILD b/aos/flatbuffers/test_dir/BUILD
index 76f5fb4..a6275a5 100644
--- a/aos/flatbuffers/test_dir/BUILD
+++ b/aos/flatbuffers/test_dir/BUILD
@@ -1,6 +1,13 @@
 load("//aos/flatbuffers:generate.bzl", "static_flatbuffer")
 
 static_flatbuffer(
+    name = "include_reflection_fbs",
+    srcs = ["include_reflection.fbs"],
+    visibility = ["//visibility:public"],
+    deps = ["//aos/flatbuffers/reflection:reflection_fbs"],
+)
+
+static_flatbuffer(
     name = "include_fbs",
     srcs = ["include.fbs"],
     visibility = ["//visibility:public"],
diff --git a/aos/flatbuffers/test_dir/include_reflection.fbs b/aos/flatbuffers/test_dir/include_reflection.fbs
new file mode 100644
index 0000000..7eda4f5
--- /dev/null
+++ b/aos/flatbuffers/test_dir/include_reflection.fbs
@@ -0,0 +1,9 @@
+include "reflection/reflection.fbs";
+
+namespace aos.testing;
+
+table UseSchema {
+  schema:reflection.Schema (id: 0);
+}
+
+root_type UseSchema;
diff --git a/aos/flatbuffers/test_dir/sample_test_static.h b/aos/flatbuffers/test_dir/sample_test_static.h
index a6362d7..57ac57a 100644
--- a/aos/flatbuffers/test_dir/sample_test_static.h
+++ b/aos/flatbuffers/test_dir/sample_test_static.h
@@ -14,6 +14,8 @@
  public:
   // The underlying "raw" flatbuffer type for this type.
   typedef aos::fbs::testing::MinimallyAlignedTable Flatbuffer;
+  typedef flatbuffers::unique_ptr<Flatbuffer::NativeTableType>
+      FlatbufferObjectType;
   // Returns this object as a flatbuffer type. This reference may not be valid
   // following mutations to the underlying flatbuffer, due to how memory may get
   // may get moved around.
@@ -135,15 +137,39 @@
   // returning true on success.
   // This is a deep copy, and will call FromFlatbuffer on any constituent
   // objects.
-  [[nodiscard]] bool FromFlatbuffer(const Flatbuffer *other) {
+  [[nodiscard]] bool FromFlatbuffer(const Flatbuffer &other) {
     Clear();
 
-    if (other->has_field()) {
-      set_field(other->field());
+    if (other.has_field()) {
+      set_field(other.field());
     }
 
     return true;
   }
+  // Equivalent to FromFlatbuffer(const Flatbuffer&); this overload is provided
+  // to ease implementation of the aos::fbs::Vector internals.
+  [[nodiscard]] bool FromFlatbuffer(const Flatbuffer *other) {
+    return FromFlatbuffer(*CHECK_NOTNULL(other));
+  }
+
+  // Copies the contents of the provided flatbuffer into this flatbuffer,
+  // returning true on success.
+  // Because the Flatbuffer Object API does not provide any concept of an
+  // optionally populated scalar field, all scalar fields will be populated
+  // after a call to FromFlatbufferObject().
+  // This is a deep copy, and will call FromFlatbufferObject on
+  // any constituent objects.
+  [[nodiscard]] bool FromFlatbuffer(const Flatbuffer::NativeTableType &other) {
+    Clear();
+
+    set_field(other.field);
+
+    return true;
+  }
+  [[nodiscard]] bool FromFlatbuffer(
+      const flatbuffers::unique_ptr<Flatbuffer::NativeTableType> &other) {
+    return FromFlatbuffer(*other);
+  }
 
  private:
   // We need to provide a MoveConstructor to allow this table to be
@@ -168,6 +194,8 @@
  public:
   // The underlying "raw" flatbuffer type for this type.
   typedef aos::fbs::testing::SubTable Flatbuffer;
+  typedef flatbuffers::unique_ptr<Flatbuffer::NativeTableType>
+      FlatbufferObjectType;
   // Returns this object as a flatbuffer type. This reference may not be valid
   // following mutations to the underlying flatbuffer, due to how memory may get
   // may get moved around.
@@ -314,19 +342,45 @@
   // returning true on success.
   // This is a deep copy, and will call FromFlatbuffer on any constituent
   // objects.
-  [[nodiscard]] bool FromFlatbuffer(const Flatbuffer *other) {
+  [[nodiscard]] bool FromFlatbuffer(const Flatbuffer &other) {
     Clear();
 
-    if (other->has_baz()) {
-      set_baz(other->baz());
+    if (other.has_baz()) {
+      set_baz(other.baz());
     }
 
-    if (other->has_foo()) {
-      set_foo(other->foo());
+    if (other.has_foo()) {
+      set_foo(other.foo());
     }
 
     return true;
   }
+  // Equivalent to FromFlatbuffer(const Flatbuffer&); this overload is provided
+  // to ease implementation of the aos::fbs::Vector internals.
+  [[nodiscard]] bool FromFlatbuffer(const Flatbuffer *other) {
+    return FromFlatbuffer(*CHECK_NOTNULL(other));
+  }
+
+  // Copies the contents of the provided flatbuffer into this flatbuffer,
+  // returning true on success.
+  // Because the Flatbuffer Object API does not provide any concept of an
+  // optionally populated scalar field, all scalar fields will be populated
+  // after a call to FromFlatbufferObject().
+  // This is a deep copy, and will call FromFlatbufferObject on
+  // any constituent objects.
+  [[nodiscard]] bool FromFlatbuffer(const Flatbuffer::NativeTableType &other) {
+    Clear();
+
+    set_baz(other.baz);
+
+    set_foo(other.foo);
+
+    return true;
+  }
+  [[nodiscard]] bool FromFlatbuffer(
+      const flatbuffers::unique_ptr<Flatbuffer::NativeTableType> &other) {
+    return FromFlatbuffer(*other);
+  }
 
  private:
   // We need to provide a MoveConstructor to allow this table to be
@@ -354,6 +408,8 @@
  public:
   // The underlying "raw" flatbuffer type for this type.
   typedef aos::fbs::testing::TestTable Flatbuffer;
+  typedef flatbuffers::unique_ptr<Flatbuffer::NativeTableType>
+      FlatbufferObjectType;
   // Returns this object as a flatbuffer type. This reference may not be valid
   // following mutations to the underlying flatbuffer, due to how memory may get
   // may get moved around.
@@ -1060,109 +1116,108 @@
   // returning true on success.
   // This is a deep copy, and will call FromFlatbuffer on any constituent
   // objects.
-  [[nodiscard]] bool FromFlatbuffer(const Flatbuffer *other) {
+  [[nodiscard]] bool FromFlatbuffer(const Flatbuffer &other) {
     Clear();
 
-    if (other->has_included_table()) {
+    if (other.has_included_table()) {
       if (!CHECK_NOTNULL(add_included_table())
-               ->FromFlatbuffer(other->included_table())) {
+               ->FromFlatbuffer(other.included_table())) {
         // Fail if we were unable to copy (e.g., if we tried to copy in a long
         // vector and do not have the space for it).
         return false;
       }
     }
 
-    if (other->has_scalar()) {
-      set_scalar(other->scalar());
+    if (other.has_scalar()) {
+      set_scalar(other.scalar());
     }
 
-    if (other->has_string()) {
-      if (!CHECK_NOTNULL(add_string())->FromFlatbuffer(other->string())) {
+    if (other.has_string()) {
+      if (!CHECK_NOTNULL(add_string())->FromFlatbuffer(other.string())) {
         // Fail if we were unable to copy (e.g., if we tried to copy in a long
         // vector and do not have the space for it).
         return false;
       }
     }
 
-    if (other->has_substruct()) {
-      set_substruct(*other->substruct());
+    if (other.has_substruct()) {
+      set_substruct(*other.substruct());
     }
 
-    if (other->has_subtable()) {
-      if (!CHECK_NOTNULL(add_subtable())->FromFlatbuffer(other->subtable())) {
+    if (other.has_subtable()) {
+      if (!CHECK_NOTNULL(add_subtable())->FromFlatbuffer(other.subtable())) {
         // Fail if we were unable to copy (e.g., if we tried to copy in a long
         // vector and do not have the space for it).
         return false;
       }
     }
 
-    if (other->has_unspecified_length_string()) {
+    if (other.has_unspecified_length_string()) {
       if (!CHECK_NOTNULL(add_unspecified_length_string())
-               ->FromFlatbuffer(other->unspecified_length_string())) {
+               ->FromFlatbuffer(other.unspecified_length_string())) {
         // Fail if we were unable to copy (e.g., if we tried to copy in a long
         // vector and do not have the space for it).
         return false;
       }
     }
 
-    if (other->has_unspecified_length_vector()) {
+    if (other.has_unspecified_length_vector()) {
       if (!CHECK_NOTNULL(add_unspecified_length_vector())
-               ->FromFlatbuffer(other->unspecified_length_vector())) {
+               ->FromFlatbuffer(other.unspecified_length_vector())) {
         // Fail if we were unable to copy (e.g., if we tried to copy in a long
         // vector and do not have the space for it).
         return false;
       }
     }
 
-    if (other->has_unspecified_length_vector_of_strings()) {
+    if (other.has_unspecified_length_vector_of_strings()) {
       if (!CHECK_NOTNULL(add_unspecified_length_vector_of_strings())
-               ->FromFlatbuffer(
-                   other->unspecified_length_vector_of_strings())) {
+               ->FromFlatbuffer(other.unspecified_length_vector_of_strings())) {
         // Fail if we were unable to copy (e.g., if we tried to copy in a long
         // vector and do not have the space for it).
         return false;
       }
     }
 
-    if (other->has_vector_aligned()) {
+    if (other.has_vector_aligned()) {
       if (!CHECK_NOTNULL(add_vector_aligned())
-               ->FromFlatbuffer(other->vector_aligned())) {
+               ->FromFlatbuffer(other.vector_aligned())) {
         // Fail if we were unable to copy (e.g., if we tried to copy in a long
         // vector and do not have the space for it).
         return false;
       }
     }
 
-    if (other->has_vector_of_scalars()) {
+    if (other.has_vector_of_scalars()) {
       if (!CHECK_NOTNULL(add_vector_of_scalars())
-               ->FromFlatbuffer(other->vector_of_scalars())) {
+               ->FromFlatbuffer(other.vector_of_scalars())) {
         // Fail if we were unable to copy (e.g., if we tried to copy in a long
         // vector and do not have the space for it).
         return false;
       }
     }
 
-    if (other->has_vector_of_strings()) {
+    if (other.has_vector_of_strings()) {
       if (!CHECK_NOTNULL(add_vector_of_strings())
-               ->FromFlatbuffer(other->vector_of_strings())) {
+               ->FromFlatbuffer(other.vector_of_strings())) {
         // Fail if we were unable to copy (e.g., if we tried to copy in a long
         // vector and do not have the space for it).
         return false;
       }
     }
 
-    if (other->has_vector_of_structs()) {
+    if (other.has_vector_of_structs()) {
       if (!CHECK_NOTNULL(add_vector_of_structs())
-               ->FromFlatbuffer(other->vector_of_structs())) {
+               ->FromFlatbuffer(other.vector_of_structs())) {
         // Fail if we were unable to copy (e.g., if we tried to copy in a long
         // vector and do not have the space for it).
         return false;
       }
     }
 
-    if (other->has_vector_of_tables()) {
+    if (other.has_vector_of_tables()) {
       if (!CHECK_NOTNULL(add_vector_of_tables())
-               ->FromFlatbuffer(other->vector_of_tables())) {
+               ->FromFlatbuffer(other.vector_of_tables())) {
         // Fail if we were unable to copy (e.g., if we tried to copy in a long
         // vector and do not have the space for it).
         return false;
@@ -1171,6 +1226,140 @@
 
     return true;
   }
+  // Equivalent to FromFlatbuffer(const Flatbuffer&); this overload is provided
+  // to ease implementation of the aos::fbs::Vector internals.
+  [[nodiscard]] bool FromFlatbuffer(const Flatbuffer *other) {
+    return FromFlatbuffer(*CHECK_NOTNULL(other));
+  }
+
+  // Copies the contents of the provided flatbuffer into this flatbuffer,
+  // returning true on success.
+  // Because the Flatbuffer Object API does not provide any concept of an
+  // optionally populated scalar field, all scalar fields will be populated
+  // after a call to FromFlatbufferObject().
+  // This is a deep copy, and will call FromFlatbufferObject on
+  // any constituent objects.
+  [[nodiscard]] bool FromFlatbuffer(const Flatbuffer::NativeTableType &other) {
+    Clear();
+
+    if (other.included_table) {
+      if (!CHECK_NOTNULL(add_included_table())
+               ->FromFlatbuffer(*other.included_table)) {
+        // Fail if we were unable to copy (e.g., if we tried to copy in a long
+        // vector and do not have the space for it).
+        return false;
+      }
+    }
+
+    set_scalar(other.scalar);
+
+    // Unconditionally copy strings/vectors, even if it will just end up
+    // being 0-length (this maintains consistency with the flatbuffer Pack()
+    // behavior).
+    if (!CHECK_NOTNULL(add_string())->FromFlatbuffer(other.string)) {
+      // Fail if we were unable to copy (e.g., if we tried to copy in a long
+      // vector and do not have the space for it).
+      return false;
+    }
+
+    if (other.substruct) {
+      set_substruct(*other.substruct);
+    }
+
+    if (other.subtable) {
+      if (!CHECK_NOTNULL(add_subtable())->FromFlatbuffer(*other.subtable)) {
+        // Fail if we were unable to copy (e.g., if we tried to copy in a long
+        // vector and do not have the space for it).
+        return false;
+      }
+    }
+
+    // Unconditionally copy strings/vectors, even if it will just end up
+    // being 0-length (this maintains consistency with the flatbuffer Pack()
+    // behavior).
+    if (!CHECK_NOTNULL(add_unspecified_length_string())
+             ->FromFlatbuffer(other.unspecified_length_string)) {
+      // Fail if we were unable to copy (e.g., if we tried to copy in a long
+      // vector and do not have the space for it).
+      return false;
+    }
+
+    // Unconditionally copy strings/vectors, even if it will just end up
+    // being 0-length (this maintains consistency with the flatbuffer Pack()
+    // behavior).
+    if (!CHECK_NOTNULL(add_unspecified_length_vector())
+             ->FromFlatbuffer(other.unspecified_length_vector)) {
+      // Fail if we were unable to copy (e.g., if we tried to copy in a long
+      // vector and do not have the space for it).
+      return false;
+    }
+
+    // Unconditionally copy strings/vectors, even if it will just end up
+    // being 0-length (this maintains consistency with the flatbuffer Pack()
+    // behavior).
+    if (!CHECK_NOTNULL(add_unspecified_length_vector_of_strings())
+             ->FromFlatbuffer(other.unspecified_length_vector_of_strings)) {
+      // Fail if we were unable to copy (e.g., if we tried to copy in a long
+      // vector and do not have the space for it).
+      return false;
+    }
+
+    // Unconditionally copy strings/vectors, even if it will just end up
+    // being 0-length (this maintains consistency with the flatbuffer Pack()
+    // behavior).
+    if (!CHECK_NOTNULL(add_vector_aligned())
+             ->FromFlatbuffer(other.vector_aligned)) {
+      // Fail if we were unable to copy (e.g., if we tried to copy in a long
+      // vector and do not have the space for it).
+      return false;
+    }
+
+    // Unconditionally copy strings/vectors, even if it will just end up
+    // being 0-length (this maintains consistency with the flatbuffer Pack()
+    // behavior).
+    if (!CHECK_NOTNULL(add_vector_of_scalars())
+             ->FromFlatbuffer(other.vector_of_scalars)) {
+      // Fail if we were unable to copy (e.g., if we tried to copy in a long
+      // vector and do not have the space for it).
+      return false;
+    }
+
+    // Unconditionally copy strings/vectors, even if it will just end up
+    // being 0-length (this maintains consistency with the flatbuffer Pack()
+    // behavior).
+    if (!CHECK_NOTNULL(add_vector_of_strings())
+             ->FromFlatbuffer(other.vector_of_strings)) {
+      // Fail if we were unable to copy (e.g., if we tried to copy in a long
+      // vector and do not have the space for it).
+      return false;
+    }
+
+    // Unconditionally copy strings/vectors, even if it will just end up
+    // being 0-length (this maintains consistency with the flatbuffer Pack()
+    // behavior).
+    if (!CHECK_NOTNULL(add_vector_of_structs())
+             ->FromFlatbuffer(other.vector_of_structs)) {
+      // Fail if we were unable to copy (e.g., if we tried to copy in a long
+      // vector and do not have the space for it).
+      return false;
+    }
+
+    // Unconditionally copy strings/vectors, even if it will just end up
+    // being 0-length (this maintains consistency with the flatbuffer Pack()
+    // behavior).
+    if (!CHECK_NOTNULL(add_vector_of_tables())
+             ->FromFlatbuffer(other.vector_of_tables)) {
+      // Fail if we were unable to copy (e.g., if we tried to copy in a long
+      // vector and do not have the space for it).
+      return false;
+    }
+
+    return true;
+  }
+  [[nodiscard]] bool FromFlatbuffer(
+      const flatbuffers::unique_ptr<Flatbuffer::NativeTableType> &other) {
+    return FromFlatbuffer(*other);
+  }
 
  private:
   // We need to provide a MoveConstructor to allow this table to be
diff --git a/aos/init.cc b/aos/init.cc
index 3ca7314..579ad33 100644
--- a/aos/init.cc
+++ b/aos/init.cc
@@ -45,25 +45,6 @@
   initialized = true;
 }
 
-void InitFromRust(const char *argv0) {
-  CHECK(!IsInitialized()) << "Only initialize once.";
-
-  FLAGS_logtostderr = true;
-
-  google::InitGoogleLogging(argv0);
-
-  // TODO(Brian): Provide a way for Rust to configure C++ flags.
-  char fake_argv0_val[] = "rust";
-  char *fake_argv0 = fake_argv0_val;
-  char **fake_argv = &fake_argv0;
-  int fake_argc = 1;
-  gflags::ParseCommandLineFlags(&fake_argc, &fake_argv, true);
-
-  // TODO(Brian): Where should Rust binaries be configured to write coredumps?
-
-  // TODO(Brian): Figure out what to do with allocator hooks for C++ and Rust.
-
-  initialized = true;
-}
+void MarkInitialized() { initialized = true; }
 
 }  // namespace aos
diff --git a/aos/init.h b/aos/init.h
index 29480eb..b9270b3 100644
--- a/aos/init.h
+++ b/aos/init.h
@@ -10,10 +10,10 @@
 // ShmEventLoop can confirm the world was initialized before running.
 bool IsInitialized();
 
-// A special initialization function that initializes the C++ parts in a way
-// compatible with Rust. This requires careful coordination with `:init_rs`, do
-// not use it from anywhere else.
-void InitFromRust(const char *argv0);
+// Marks the system as initialized. This is only meant to be used from
+// init_for_rust. DO NOT call this from anywhere else, use InitGoogle
+// instead.
+void MarkInitialized();
 
 }  // namespace aos
 
diff --git a/aos/init.rs b/aos/init.rs
index 13066f1..fe0408d 100644
--- a/aos/init.rs
+++ b/aos/init.rs
@@ -1,20 +1,275 @@
-use std::{ffi::CString, sync::Once};
+//! AOS Initialization
+//!
+//! This module "links" the C++ library and the Rust application together.
+//! In particular it provides the [`Init`] trait which is implemented for
+//! any struct that implements [`clap::Parser`]. The reason for this is that
+//! an important part of initializing the C++ library involves setting the
+//! gFlags which get resolved dynamically thanks to their reflection API.
+//!
+//! # Examples
+//!
+//! ```no_run
+//! use aos_init::Init;
+//! use clap::Parser;
+//!
+//! #[derive(Parser, Debug)]
+//! struct App {
+//!     /// Time to sleep between pings.
+//!     #[arg(long, default_value_t = 10000, value_name = "MICROS")]
+//!     sleep: u64,
+//! }
+//!
+//! fn main() {
+//!     // Initializes AOS and returns the App struct with the parsed CLI flags
+//!     let app: App = App::init();
+//!     // At this point your flags are parsed and AOS is initialized.
+//! }
+//! ```
+//! You can also use [`DefaultApp`] to avoid having to specify your own CLI options if you don't
+//! need them. For example:
+//!
+//! ```no_run
+//! use aos_init::{DefaultApp, Init};
+//! use clap::Parser;
+//!
+//! fn main() {
+//!     // Initializes AOS. DefaultApp doesn't have any flags to parse.
+//!     let _ = DefaultApp::init();
+//!     // At this point AOS is initialized and you can create event loop.
+//! }
+//!```
+
+use std::{
+    env,
+    ffi::{CString, OsStr, OsString},
+    os::unix::prelude::OsStrExt,
+    sync::Once,
+};
+
+use clap::{
+    error::{ContextKind, ContextValue},
+    Arg, ArgAction, Error, Parser,
+};
 
 autocxx::include_cpp! (
-#include "aos/init.h"
+#include "aos/init_for_rust.h"
 
 safety!(unsafe)
 
 generate!("aos::InitFromRust")
+generate!("aos::GetCppFlags")
+generate!("aos::FlagInfo")
+generate!("aos::SetCommandLineOption")
+generate!("aos::GetCommandLineOption")
 );
 
-/// Initializes AOS.
-pub fn init() {
-    static ONCE: Once = Once::new();
-    ONCE.call_once(|| {
-        let argv0 = std::env::args().next().expect("must have argv[0]");
-        let argv0 = CString::new(argv0).expect("argv[0] may not have NUL");
-        // SAFETY: argv0 is a NUL-terminated string.
-        unsafe { ffi::aos::InitFromRust(argv0.as_ptr()) };
-    });
+// Intended to be used only from here and test_init. Don't use it anywhere else please.
+#[doc(hidden)]
+pub mod internal {
+    use super::*;
+    /// Generic initialization for production and tests.
+    ///
+    /// Sets up the C++ side of things. Notably, it doesn't setup the command line flags.
+    pub fn init() {
+        static ONCE: Once = Once::new();
+        ONCE.call_once(|| {
+            // We leak the `CString` with `into_raw`. It's not sound for C++ to free
+            // it but `InitGoogleLogging` requries that it is long-lived.
+            let argv0 = std::env::args()
+                .map(|arg| CString::new(arg).expect("Arg may not have NUL"))
+                .next()
+                .expect("Missing argv[0]?")
+                .into_raw();
+            // SAFETY: argv0 is a well-defined CString.
+            unsafe {
+                ffi::aos::InitFromRust(argv0);
+            }
+        });
+    }
+}
+
+/// An application that doesn't need custom command line flags.
+///
+/// If you need your own command line flags, use any struct that derives [`clap::Parser`] instead.
+#[derive(Parser, Debug)]
+pub struct DefaultApp {}
+
+/// Trait used to append C++ gFlags to a clap CLI.
+pub trait Init: Parser {
+    /// Initializes an AOS application.
+    ///
+    /// Parses the command line flags and runs the initialization logic.
+    fn init() -> Self {
+        let this = Self::parse_with_cpp_flags();
+        // Rust logs to stderr by default. Make that true for C++ as that will be easier than
+        // managing one or multiple files across FFI. We can pipe the stderr to a file to get
+        // a log file if we want.
+        CxxFlag::set_option("logtostderr", "true".as_ref())
+            .expect("Error setting C++ flag: logtostderr");
+        internal::init();
+        // Non-test initialization below
+        env_logger::init();
+        this
+    }
+
+    /// Parses the comannd line arguments while also setting the C++ gFlags.
+    fn parse_with_cpp_flags() -> Self {
+        Self::parse_with_cpp_flags_from(env::args_os())
+    }
+
+    /// Like [`Init::parse_with_cpp_flags`] but read from an iterator.
+    fn parse_with_cpp_flags_from<I, T>(itr: I) -> Self
+    where
+        I: IntoIterator<Item = T>,
+        T: Into<OsString> + Clone,
+    {
+        let cxxflags = ffi::aos::GetCppFlags();
+        let cxxflags: Vec<CxxFlag> = cxxflags
+            .iter()
+            .map(|flag| CxxFlag::from(flag))
+            .filter(|flag| flag.name != "help" && flag.name != "version")
+            .collect();
+
+        let mut command = Self::command()
+            .next_help_heading("Flags from C++")
+            .args(cxxflags.iter().cloned());
+
+        let matches = command.clone().get_matches_from(itr);
+
+        for cxxflag in cxxflags {
+            let Some(mut value) = matches.get_raw(&cxxflag.name) else {
+                continue;
+            };
+            // We grab the last match as GFlags does.
+            let value = value.next_back().unwrap();
+            cxxflag.set(value).unwrap_or_else(|_| {
+                let mut error = Error::new(clap::error::ErrorKind::InvalidValue);
+
+                // Let user know how they messed up.
+                error.insert(
+                    ContextKind::InvalidArg,
+                    ContextValue::String(format!("--{}", cxxflag.name)),
+                );
+                error.insert(
+                    ContextKind::InvalidValue,
+                    ContextValue::String(
+                        value
+                            .to_owned()
+                            .into_string()
+                            .expect("Invalid UTF-8 String"),
+                    ),
+                );
+                error.format(&mut command).exit()
+            })
+        }
+
+        match Self::from_arg_matches(&matches) {
+            Ok(flags) => flags,
+            Err(e) => e.format(&mut command).exit(),
+        }
+    }
+}
+
+impl<T: Parser> Init for T {}
+
+#[derive(Clone)]
+#[allow(unused)]
+struct CxxFlag {
+    name: String,
+    ty: String,
+    description: String,
+    default_value: String,
+    filename: String,
+}
+
+#[derive(Debug)]
+struct SetFlagError;
+
+impl CxxFlag {
+    /// Sets the command gFlag to the specified value.
+    fn set(&self, value: &OsStr) -> Result<(), SetFlagError> {
+        Self::set_option(&self.name, value)
+    }
+
+    /// Sets the command gFlag to the specified value.
+    fn set_option(name: &str, value: &OsStr) -> Result<(), SetFlagError> {
+        unsafe {
+            let name = CString::new(name.clone()).expect("Flag name may not have NUL");
+            let value = CString::new(value.as_bytes()).expect("Arg may not have NUL");
+            if ffi::aos::SetCommandLineOption(name.as_ptr(), value.as_ptr()) {
+                Ok(())
+            } else {
+                Err(SetFlagError)
+            }
+        }
+    }
+
+    #[allow(dead_code)]
+    fn get_option(name: &str) -> String {
+        unsafe {
+            let name = CString::new(name).expect("Flag may not have NUL");
+            ffi::aos::GetCommandLineOption(name.as_ptr()).to_string()
+        }
+    }
+}
+
+impl From<&ffi::aos::FlagInfo> for CxxFlag {
+    fn from(value: &ffi::aos::FlagInfo) -> Self {
+        Self {
+            name: value.name().to_string(),
+            ty: value.ty().to_string(),
+            description: value.description().to_string(),
+            default_value: value.default_value().to_string(),
+            filename: value.filename().to_string(),
+        }
+    }
+}
+
+impl From<CxxFlag> for Arg {
+    fn from(value: CxxFlag) -> Self {
+        assert_ne!(&value.name, "help");
+        Arg::new(&value.name)
+            .long(&value.name)
+            .help(&value.description)
+            .default_value(&value.default_value)
+            .action(ArgAction::Set)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::sync::Mutex;
+
+    use super::*;
+
+    #[derive(Parser)]
+    #[command()]
+    struct App {
+        #[arg(long)]
+        myarg: u64,
+    }
+
+    // We are sharing global state through gFlags. Use a mutex to prevent races.
+    static MUTEX: Mutex<()> = Mutex::new(());
+
+    #[test]
+    fn simple_rust() {
+        let _guard = MUTEX.lock();
+        let app = App::parse_with_cpp_flags_from(&["mytest", "--myarg", "23"]);
+        assert_eq!(app.myarg, 23);
+    }
+
+    #[test]
+    fn set_cxx_flag() {
+        let _guard = MUTEX.lock();
+        let app = App::parse_with_cpp_flags_from(&[
+            "mytest",
+            "--alsologtostderr",
+            "true",
+            "--myarg",
+            "23",
+        ]);
+        assert_eq!(app.myarg, 23);
+        assert_eq!(CxxFlag::get_option("alsologtostderr"), "true");
+    }
 }
diff --git a/aos/init_for_rust.cc b/aos/init_for_rust.cc
new file mode 100644
index 0000000..9af4d83
--- /dev/null
+++ b/aos/init_for_rust.cc
@@ -0,0 +1,49 @@
+#include "aos/init_for_rust.h"
+
+#include "gflags/gflags.h"
+#include "glog/logging.h"
+
+#include "aos/init.h"
+
+namespace aos {
+
+void InitFromRust(const char *argv0) {
+  CHECK(!IsInitialized()) << "Only initialize once.";
+
+  google::InitGoogleLogging(argv0);
+
+  // TODO(Brian): Where should Rust binaries be configured to write coredumps?
+
+  // TODO(Brian): Figure out what to do with allocator hooks for C++ and Rust.
+
+  MarkInitialized();
+}
+
+std::vector<FlagInfo> GetCppFlags() {
+  std::vector<gflags::CommandLineFlagInfo> info;
+  gflags::GetAllFlags(&info);
+  std::vector<FlagInfo> out;
+  for (const auto &flag : info) {
+    FlagInfo out_flag = {
+        .name_ = flag.name,
+        .type_ = flag.type,
+        .description_ = flag.description,
+        .default_value_ = flag.default_value,
+        .filename_ = flag.filename,
+    };
+    out.push_back(out_flag);
+  }
+  return out;
+}
+
+bool SetCommandLineOption(const char *name, const char *value) {
+  return !gflags::SetCommandLineOption(name, value).empty();
+}
+
+std::string GetCommandLineOption(const char *name) {
+  std::string out;
+  CHECK(gflags::GetCommandLineOption(name, &out)) << "Unknown flag " << name;
+  return out;
+}
+
+}  // namespace aos
diff --git a/aos/init_for_rust.h b/aos/init_for_rust.h
new file mode 100644
index 0000000..86bfd66
--- /dev/null
+++ b/aos/init_for_rust.h
@@ -0,0 +1,47 @@
+#ifndef AOS_INIT_FOR_RUST_H_
+#define AOS_INIT_FOR_RUST_H_
+
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "aos/for_rust.h"
+#include "cxx.h"
+
+namespace aos {
+
+struct FlagInfo {
+  std::string name_;
+  std::string type_;
+  std::string description_;
+  std::string default_value_;
+  std::string filename_;
+
+  rust::Str name() const {
+    return StringViewToRustStr(std::string_view(name_));
+  }
+  rust::Str ty() const { return StringViewToRustStr(std::string_view(type_)); }
+  rust::Str description() const {
+    return StringViewToRustStr(std::string_view(description_));
+  }
+  rust::Str default_value() const {
+    return StringViewToRustStr(std::string_view(default_value_));
+  }
+  rust::Str filename() const {
+    return StringViewToRustStr(std::string_view(filename_));
+  }
+};
+
+// A special initialization function that initializes the C++ parts in a way
+// compatible with Rust. This requires careful coordination with `:init_rs`, do
+// not use it from anywhere else.
+void InitFromRust(const char *argv0);
+
+std::vector<FlagInfo> GetCppFlags();
+
+bool SetCommandLineOption(const char *name, const char *value);
+std::string GetCommandLineOption(const char *name);
+
+}  // namespace aos
+
+#endif  // AOS_INIT_FOR_RUST_H_
diff --git a/aos/json_to_flatbuffer.h b/aos/json_to_flatbuffer.h
index 6ef3544..deafaa7 100644
--- a/aos/json_to_flatbuffer.h
+++ b/aos/json_to_flatbuffer.h
@@ -75,6 +75,12 @@
       Flatbuffer<T>::MiniReflectTypeTable(), json_options);
 }
 
+template <typename T, typename Enable = T::Flatbuffer>
+inline ::std::string FlatbufferToJson(const fbs::Builder<T> &flatbuffer,
+                                      JsonOptions json_options = {}) {
+  return FlatbufferToJson(flatbuffer.AsFlatbufferSpan(), json_options);
+}
+
 // Converts a flatbuffer::Table to JSON.
 template <typename T>
 typename std::enable_if<
diff --git a/aos/logging/log_namer.cc b/aos/logging/log_namer.cc
index 16ba0f2..412c74e 100644
--- a/aos/logging/log_namer.cc
+++ b/aos/logging/log_namer.cc
@@ -76,7 +76,7 @@
     PLOG(FATAL) << "couldn't create final name";
   }
   // Fix basename formatting.
-  LOG(INFO) << "Created log file (" << filename << "). Previous file was ("
+  LOG(INFO) << "Created log file (" << *filename << "). Previous file was ("
             << directory << "/" << previous << ").";
 }
 
diff --git a/aos/network/sctp_lib.cc b/aos/network/sctp_lib.cc
index 829ae67..cf50ad6 100644
--- a/aos/network/sctp_lib.cc
+++ b/aos/network/sctp_lib.cc
@@ -4,6 +4,7 @@
 #include <linux/sctp.h>
 #include <net/if.h>
 #include <netdb.h>
+#include <netinet/ip.h>
 #include <sys/socket.h>
 #include <sys/stat.h>
 #include <sys/types.h>
@@ -26,6 +27,25 @@
 DEFINE_bool(disable_ipv6, false, "disable ipv6");
 DEFINE_int32(rmem, 0, "If nonzero, set rmem to this size.");
 
+// The Type of Service.
+// https://www.tucny.com/Home/dscp-tos
+//
+// We want to set the highest precedence (i.e. critical) with minimal delay.  We
+// also want to be able to stuff the packets into bucket 0 for queue
+// disciplining. Experiments show that 176 works for this. Other values (e.g.
+// DSCP class EF) cannot be stuffed into bucket 0 (for unknown reasons).
+//
+// Note that the two least significant bits are reserved and should always set
+// to zero. Those two bits are the "Explicit Congestion Notification" bits. They
+// are controlled by the IP stack itself (and used by the router). We don't
+// control that via the TOS value we set here.
+DEFINE_int32(
+    sctp_tos, 176,
+    "The Type-Of-Service value to use. Defaults to a critical priority. "
+    "Always set values here whose two least significant bits are set to zero. "
+    "When using tcpdump, the `tos` field may show the least significant two "
+    "bits set to something other than zero.");
+
 namespace aos::message_bridge {
 
 namespace {
@@ -272,6 +292,13 @@
   LOG(INFO) << "socket(" << Family(sockaddr_local)
             << ", SOCK_SEQPACKET, IPPROTOSCTP) = " << fd_;
   {
+    // Set up Type-Of-Service.
+    //
+    // See comments for the --sctp_tos flag for more information.
+    int tos = IPTOS_DSCP(FLAGS_sctp_tos);
+    PCHECK(setsockopt(fd_, IPPROTO_IP, IP_TOS, &tos, sizeof(tos)) == 0);
+  }
+  {
     // Per https://tools.ietf.org/html/rfc6458
     // Setting this to !0 allows event notifications to be interleaved
     // with data if enabled. This typically only matters during congestion.
diff --git a/aos/starter/starterd_lib.cc b/aos/starter/starterd_lib.cc
index 95210c0..38d519d 100644
--- a/aos/starter/starterd_lib.cc
+++ b/aos/starter/starterd_lib.cc
@@ -19,6 +19,7 @@
 DEFINE_uint32(queue_initialization_threads, 0,
               "Number of threads to spin up to initialize the queue.  0 means "
               "use the main thread.");
+DECLARE_bool(enable_ftrace);
 
 namespace aos::starter {
 
@@ -214,6 +215,11 @@
   if (info.ssi_signo == SIGCHLD) {
     // SIGCHLD messages can be collapsed if multiple are received, so all
     // applications must check their status.
+    if (FLAGS_enable_ftrace) {
+      ftrace_.FormatMessage("SIGCHLD");
+      ftrace_.TurnOffOrDie();
+    }
+
     for (auto iter = applications_.begin(); iter != applications_.end();) {
       if (iter->second.MaybeHandleSignal()) {
         iter = applications_.erase(iter);
diff --git a/aos/starter/starterd_lib.h b/aos/starter/starterd_lib.h
index 3779c84..1ffc782 100644
--- a/aos/starter/starterd_lib.h
+++ b/aos/starter/starterd_lib.h
@@ -77,6 +77,8 @@
   aos::PhasedLoopHandler *status_timer_;
   aos::TimerHandler *cleanup_timer_;
 
+  aos::Ftrace ftrace_;
+
   int status_count_ = 0;
   const int max_status_count_;
 
diff --git a/aos/starter/subprocess.cc b/aos/starter/subprocess.cc
index 9606adc..d677922 100644
--- a/aos/starter/subprocess.cc
+++ b/aos/starter/subprocess.cc
@@ -266,6 +266,8 @@
   if (application->has_memory_limit() && application->memory_limit() > 0) {
     SetMemoryLimit(application->memory_limit());
   }
+
+  set_stop_grace_period(std::chrono::nanoseconds(application->stop_time()));
 }
 
 void Application::DoStart() {
@@ -496,8 +498,7 @@
 
       // Watchdog timer to SIGKILL application if it is still running 1 second
       // after SIGINT
-      stop_timer_->Schedule(event_loop_->monotonic_now() +
-                            std::chrono::seconds(1));
+      stop_timer_->Schedule(event_loop_->monotonic_now() + stop_grace_period_);
       queue_restart_ = restart;
       OnChange();
       break;
diff --git a/aos/starter/subprocess.h b/aos/starter/subprocess.h
index ca7ef9d..eb36874 100644
--- a/aos/starter/subprocess.h
+++ b/aos/starter/subprocess.h
@@ -139,6 +139,14 @@
   void set_capture_stderr(bool capture);
   void set_run_as_sudo(bool value) { run_as_sudo_ = value; }
 
+  // Sets the time for a process to stop gracefully. If an application is asked
+  // to stop, but doesn't stop within the specified time limit, then it is
+  // forcefully killed. Defaults to 1 second unless overridden by the
+  // aos::Application instance in the constructor.
+  void set_stop_grace_period(std::chrono::nanoseconds stop_grace_period) {
+    stop_grace_period_ = stop_grace_period;
+  }
+
   bool autostart() const { return autostart_; }
 
   bool autorestart() const { return autorestart_; }
@@ -222,6 +230,7 @@
   std::optional<uid_t> user_;
   std::optional<gid_t> group_;
   bool run_as_sudo_ = false;
+  std::chrono::nanoseconds stop_grace_period_ = std::chrono::seconds(1);
 
   bool capture_stdout_ = false;
   PipePair stdout_pipes_;
diff --git a/aos/starter/subprocess_reliable_test.cc b/aos/starter/subprocess_reliable_test.cc
index 0460bc3..701de11 100644
--- a/aos/starter/subprocess_reliable_test.cc
+++ b/aos/starter/subprocess_reliable_test.cc
@@ -3,6 +3,7 @@
 
 #include <filesystem>
 
+#include "gmock/gmock.h"
 #include "gtest/gtest.h"
 
 #include "aos/events/shm_event_loop.h"
@@ -124,4 +125,55 @@
   ASSERT_TRUE(std::filesystem::exists(shutdown_signal_file));
 }
 
+// Validates that a process that is known to take a while to stop can shut down
+// gracefully without being killed.
+TEST(SubprocessTest, CanSlowlyStopGracefully) {
+  const std::string config_file =
+      ::aos::testing::ArtifactPath("aos/events/pingpong_config.json");
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config =
+      aos::configuration::ReadConfig(config_file);
+  aos::ShmEventLoop event_loop(&config.message());
+
+  // Use a file to signal that the subprocess has started up properly and that
+  // the exit handler has been installed. Otherwise we risk killing the process
+  // uncleanly before the signal handler got installed.
+  auto signal_dir = std::filesystem::path(aos::testing::TestTmpDir()) /
+                    "slow_death_startup_file_signals";
+  ASSERT_TRUE(std::filesystem::create_directory(signal_dir));
+  auto startup_signal_file = signal_dir / "startup";
+
+  // Create an application that should never get killed automatically. It should
+  // have plenty of time to shut down on its own. In this case, we use 2 seconds
+  // to mean "plenty of time".
+  auto application = std::make_unique<Application>("/bin/bash", "/bin/bash",
+                                                   &event_loop, [] {});
+  application->set_args(
+      {"-c",
+       absl::StrCat(
+           "trap 'echo got int; sleep 2; echo shutting down; exit 0' SIGINT; "
+           "while true; do sleep 0.1; touch ",
+           startup_signal_file.string(), "; done;")});
+  application->set_capture_stdout(true);
+  application->set_stop_grace_period(std::chrono::seconds(999));
+  application->AddOnChange([&] {
+    if (application->status() == aos::starter::State::STOPPED) {
+      event_loop.Exit();
+    }
+  });
+  application->Start();
+  event_loop
+      .AddTimer([&] {
+        if (std::filesystem::exists(startup_signal_file)) {
+          // Now that the subprocess has properly started up, let's kill it.
+          application->Stop();
+        }
+      })
+      ->Schedule(event_loop.monotonic_now(), std::chrono::milliseconds(100));
+  event_loop.Run();
+
+  EXPECT_EQ(application->exit_code(), 0);
+  EXPECT_THAT(application->GetStdout(), ::testing::HasSubstr("got int"));
+  EXPECT_THAT(application->GetStdout(), ::testing::HasSubstr("shutting down"));
+}
+
 }  // namespace aos::starter::testing
diff --git a/aos/test_init.rs b/aos/test_init.rs
index a8380e2..d17d2ef 100644
--- a/aos/test_init.rs
+++ b/aos/test_init.rs
@@ -1,4 +1,4 @@
-use aos_init::init;
+use std::sync::Once;
 
 autocxx::include_cpp! (
 #include "aos/testing/tmpdir.h"
@@ -15,7 +15,11 @@
 ///
 /// Panics if non-test initialization has already been performed.
 pub fn test_init() {
-    init();
-    ffi::aos::testing::SetTestShmBase();
-    // TODO(Brian): Do we want any of the other stuff that `:gtest_main` has?
+    static ONCE: Once = Once::new();
+    ONCE.call_once(|| {
+        aos_init::internal::init();
+        ffi::aos::testing::SetTestShmBase();
+        env_logger::builder().is_test(true).init();
+        // TODO(Brian): Do we want any of the other stuff that `:gtest_main` has?
+    });
 }
diff --git a/aos/testing/BUILD b/aos/testing/BUILD
index 02459e4..eb9e2b2 100644
--- a/aos/testing/BUILD
+++ b/aos/testing/BUILD
@@ -1,3 +1,5 @@
+load("//tools/rust:defs.bzl", "rust_library")
+
 cc_library(
     name = "googletest",
     testonly = True,
@@ -131,3 +133,17 @@
         "@com_google_absl//absl/strings",
     ],
 )
+
+rust_library(
+    name = "aos_rs",
+    testonly = True,
+    srcs = ["aos.rs"],
+    crate_name = "aos",
+    gen_docs = False,
+    gen_doctests = False,
+    visibility = ["//visibility:public"],
+    deps = [
+        "//aos:aos_rs",
+        "//aos:test_init_rs",
+    ],
+)
diff --git a/aos/testing/aos.rs b/aos/testing/aos.rs
new file mode 100644
index 0000000..6c7795d
--- /dev/null
+++ b/aos/testing/aos.rs
@@ -0,0 +1,7 @@
+// Reexport all of the [`aos`] crate
+pub use aos::*;
+
+/// Utilities for testing an AOS application.
+pub mod testing {
+    pub use aos_test_init as init;
+}
diff --git a/aos/time/time.cc b/aos/time/time.cc
index 6a25aa9..e75fe45 100644
--- a/aos/time/time.cc
+++ b/aos/time/time.cc
@@ -6,6 +6,7 @@
 #include <cstring>
 #include <ctime>
 #include <iomanip>
+#include <sstream>
 
 #ifdef __linux__
 
@@ -79,6 +80,18 @@
   return stream;
 }
 
+std::string ToString(const aos::monotonic_clock::time_point &now) {
+  std::ostringstream stream;
+  stream << now;
+  return stream.str();
+}
+
+std::string ToString(const aos::realtime_clock::time_point &now) {
+  std::ostringstream stream;
+  stream << now;
+  return stream.str();
+}
+
 #ifdef __linux__
 std::optional<monotonic_clock::time_point> monotonic_clock::FromString(
     const std::string_view now) {
@@ -170,6 +183,16 @@
           : std::chrono::duration_cast<std::chrono::seconds>(
                 now.time_since_epoch());
 
+  // We can run into some corner cases where the seconds value is large enough
+  // to cause the conversion to nanoseconds to overflow. That is undefined
+  // behaviour so we prevent it with this check here.
+  if (int64_t result;
+      __builtin_mul_overflow(seconds.count(), 1'000'000'000, &result)) {
+    stream << "(unrepresentable realtime " << now.time_since_epoch().count()
+           << ")";
+    return stream;
+  }
+
   std::time_t seconds_t = seconds.count();
   stream << std::put_time(localtime_r(&seconds_t, &tm), "%Y-%m-%d_%H-%M-%S.")
          << std::setfill('0') << std::setw(9)
diff --git a/aos/time/time.h b/aos/time/time.h
index 1a5cbd1..8462625 100644
--- a/aos/time/time.h
+++ b/aos/time/time.h
@@ -81,6 +81,9 @@
 std::ostream &operator<<(std::ostream &stream,
                          const aos::realtime_clock::time_point &now);
 
+std::string ToString(const aos::monotonic_clock::time_point &now);
+std::string ToString(const aos::realtime_clock::time_point &now);
+
 namespace time {
 #ifdef __linux__
 
diff --git a/aos/time/time_test.cc b/aos/time/time_test.cc
index 63a145b..97116b0 100644
--- a/aos/time/time_test.cc
+++ b/aos/time/time_test.cc
@@ -208,8 +208,9 @@
     std::stringstream s;
     s << t;
 
-    EXPECT_EQ(s.str(), "1677-09-21_00-12-43.145224192");
-    EXPECT_EQ(realtime_clock::FromString(s.str()).value(), t);
+    // min_time happens to be unrepresentable because of rounding and signed
+    // integer overflow.
+    EXPECT_EQ(s.str(), "(unrepresentable realtime -9223372036854775808)");
   }
 
   {
@@ -224,4 +225,12 @@
   }
 }
 
+// Test that ToString works for monotonic and realtime time points.
+TEST(TimeTest, ToStringTimePoints) {
+  EXPECT_EQ(ToString(realtime_clock::epoch() + std::chrono::hours(5 * 24) +
+                     std::chrono::seconds(11) + std::chrono::milliseconds(5)),
+            "1970-01-06_00-00-11.005000000");
+  EXPECT_EQ(ToString(monotonic_clock::min_time), "-9223372036.854775808sec");
+}
+
 }  // namespace aos::time::testing
diff --git a/aos/util/error_counter.h b/aos/util/error_counter.h
index 3e69a71..1e04571 100644
--- a/aos/util/error_counter.h
+++ b/aos/util/error_counter.h
@@ -41,6 +41,16 @@
     return offset;
   }
 
+  template <typename Static>
+  static void InitializeStaticFbs(Static *builder) {
+    CHECK(builder->reserve(kNumErrors));
+    for (size_t ii = 0; ii < kNumErrors; ++ii) {
+      auto element = CHECK_NOTNULL(builder->emplace_back());
+      element->set_error(static_cast<Error>(ii));
+      element->set_count(0);
+    }
+  }
+
   void set_mutable_vector(
       flatbuffers::Vector<flatbuffers::Offset<Count>> *vector) {
     vector_ = vector;
@@ -91,6 +101,14 @@
     return offset;
   }
 
+  template <typename Static>
+  void PopulateCountsStaticFbs(Static *builder) const {
+    ErrorCounter<Error, Count>::InitializeStaticFbs(builder);
+    for (size_t ii = 0; ii < kNumErrors; ++ii) {
+      builder->at(ii).set_count(error_counts_.at(ii));
+    }
+  }
+
   void IncrementError(Error error) {
     DCHECK_LT(static_cast<size_t>(error), error_counts_.size());
     error_counts_.at(static_cast<size_t>(error))++;
diff --git a/aos/util/mcap_logger_test.cc b/aos/util/mcap_logger_test.cc
index c6febf9..3684e93 100644
--- a/aos/util/mcap_logger_test.cc
+++ b/aos/util/mcap_logger_test.cc
@@ -81,6 +81,20 @@
                     "values": {
                         "items": {
                             "properties": {
+                                "attributes": {
+                                    "items": {
+                                        "properties": {
+                                            "key": {
+                                                "type": "string"
+                                            },
+                                            "value": {
+                                                "type": "string"
+                                            }
+                                        },
+                                        "type": "object"
+                                    },
+                                    "type": "array"
+                                },
                                 "documentation": {
                                     "items": {
                                         "type": "string"
diff --git a/aos/util/trapezoid_profile.cc b/aos/util/trapezoid_profile.cc
index 938ca75..05e79ca 100644
--- a/aos/util/trapezoid_profile.cc
+++ b/aos/util/trapezoid_profile.cc
@@ -1,54 +1,70 @@
 #include "aos/util/trapezoid_profile.h"
 
-#include "aos/logging/logging.h"
-
-using ::Eigen::Matrix;
+#include "glog/logging.h"
 
 namespace aos::util {
 
-TrapezoidProfile::TrapezoidProfile(::std::chrono::nanoseconds delta_time)
-    : maximum_acceleration_(0), maximum_velocity_(0), timestep_(delta_time) {
+AsymmetricTrapezoidProfile::AsymmetricTrapezoidProfile(
+    ::std::chrono::nanoseconds delta_time)
+    : timestep_(delta_time) {
   output_.setZero();
 }
 
-void TrapezoidProfile::UpdateVals(double acceleration, double delta_time) {
+void AsymmetricTrapezoidProfile::UpdateVals(double acceleration,
+                                            double delta_time) {
   output_(0) +=
       output_(1) * delta_time + 0.5 * acceleration * delta_time * delta_time;
   output_(1) += acceleration * delta_time;
 }
 
-const Matrix<double, 2, 1> &TrapezoidProfile::Update(double goal_position,
-                                                     double goal_velocity) {
-  CalculateTimes(goal_position - output_(0), goal_velocity);
+const Eigen::Matrix<double, 2, 1> &AsymmetricTrapezoidProfile::Update(
+    double goal_position, double goal_velocity) {
+  CalculateTimes(goal_position - output_(0), goal_velocity, output_);
 
   double next_timestep = ::aos::time::DurationInSeconds(timestep_);
 
+  if (deceleration_reversal_time_ > next_timestep) {
+    UpdateVals(deceleration_reversal_, next_timestep);
+    return output_;
+  }
+
+  UpdateVals(deceleration_reversal_, deceleration_reversal_time_);
+  next_timestep -= deceleration_reversal_time_;
+
   if (acceleration_time_ > next_timestep) {
     UpdateVals(acceleration_, next_timestep);
-  } else {
-    UpdateVals(acceleration_, acceleration_time_);
+    return output_;
+  }
 
-    next_timestep -= acceleration_time_;
-    if (constant_time_ > next_timestep) {
-      UpdateVals(0, next_timestep);
-    } else {
-      UpdateVals(0, constant_time_);
-      next_timestep -= constant_time_;
-      if (deceleration_time_ > next_timestep) {
-        UpdateVals(deceleration_, next_timestep);
-      } else {
-        UpdateVals(deceleration_, deceleration_time_);
-        next_timestep -= deceleration_time_;
-        UpdateVals(0, next_timestep);
-      }
+  UpdateVals(acceleration_, acceleration_time_);
+  next_timestep -= acceleration_time_;
+
+  if (constant_time_ > next_timestep) {
+    UpdateVals(0, next_timestep);
+    return output_;
+  }
+
+  UpdateVals(0, constant_time_);
+  next_timestep -= constant_time_;
+  if (deceleration_time_ > next_timestep) {
+    UpdateVals(deceleration_, next_timestep);
+  } else {
+    UpdateVals(deceleration_, deceleration_time_);
+    next_timestep -= deceleration_time_;
+    UpdateVals(0, next_timestep);
+
+    if (next_timestep >= 0 && goal_velocity == 0) {
+      output_(0) = goal_position;
+      output_(1) = goal_velocity;
     }
   }
 
   return output_;
 }
 
-void TrapezoidProfile::CalculateTimes(double distance_to_target,
-                                      double goal_velocity) {
+void AsymmetricTrapezoidProfile::CalculateTimes(
+    double distance_to_target, double goal_velocity,
+    Eigen::Matrix<double, 2, 1> current) {
   if (distance_to_target == 0) {
     // We're there. Stop everything.
     // TODO(aschuh): Deal with velocity not right.
@@ -57,39 +73,69 @@
     constant_time_ = 0;
     deceleration_time_ = 0;
     deceleration_ = 0;
+    deceleration_reversal_time_ = 0;
+    deceleration_reversal_ = 0;
     return;
   } else if (distance_to_target < 0) {
     // Recurse with everything inverted.
-    output_(1) *= -1;
-    CalculateTimes(-distance_to_target, -goal_velocity);
-    output_(1) *= -1;
+    current(1) *= -1;
+    CalculateTimes(-distance_to_target, -goal_velocity, current);
     acceleration_ *= -1;
     deceleration_ *= -1;
+    deceleration_reversal_ *= -1;
     return;
   }
 
   constant_time_ = 0;
   acceleration_ = maximum_acceleration_;
+
+  // Calculate the fastest speed we could get going to by the distance to
+  // target.  We will have normalized everything out to be a positive distance
+  // by now so we never have to deal with going "backwards".
   double maximum_acceleration_velocity =
       distance_to_target * 2 * std::abs(acceleration_) +
-      output_(1) * output_(1);
-  if (maximum_acceleration_velocity > 0) {
-    maximum_acceleration_velocity = sqrt(maximum_acceleration_velocity);
-  } else {
-    maximum_acceleration_velocity = -sqrt(-maximum_acceleration_velocity);
-  }
+      current(1) * current(1);
+  CHECK_GE(maximum_acceleration_velocity, 0);
+  maximum_acceleration_velocity = sqrt(maximum_acceleration_velocity);
 
-  // Since we know what we'd have to do if we kept after it to decelerate, we
-  // know the sign of the acceleration.
+  // If we could get going faster than the target, we will need to decelerate
+  // after accelerating.
   if (maximum_acceleration_velocity > goal_velocity) {
-    deceleration_ = -maximum_acceleration_;
+    deceleration_ = -maximum_deceleration_;
   } else {
+    // We couldn't get up to speed by the destination.  Set our decel to
+    // accelerate to keep accelerating to get up to speed.
+    //
+    // Note: goal_velocity != 0 isn't well tested, use at your own risk.
+    LOG(FATAL) << "Untested";
     deceleration_ = maximum_acceleration_;
   }
 
+  // If we are going away from the goal, we will need to change directions.
+  if (current(1) < 0) {
+    deceleration_reversal_time_ = current(1) / deceleration_;
+    deceleration_reversal_ = -deceleration_;
+  } else if ((goal_velocity - current(1)) * (goal_velocity + current(1)) <
+             2.0 * deceleration_ * distance_to_target) {
+    // Then, can we stop in time if we get after it?  If so, we don't need to
+    // decel first before running the profile.
+    deceleration_reversal_time_ = -current(1) / deceleration_;
+    deceleration_reversal_ = deceleration_;
+  } else {
+    // Otherwise, we are all good and don't need to handle reversing.
+    deceleration_reversal_time_ = 0.0;
+    deceleration_reversal_ = 0.0;
+  }
+
+  current(0) += current(1) * deceleration_reversal_time_ +
+                0.5 * deceleration_reversal_ * deceleration_reversal_time_ *
+                    deceleration_reversal_time_;
+  current(1) += deceleration_reversal_ * deceleration_reversal_time_;
+  // OK, now we've compensated for slowing down.
+
   // We now know the top velocity we can get to.
-  double top_velocity = sqrt(
-      (distance_to_target + (output_(1) * output_(1)) / (2.0 * acceleration_) +
+  const double top_velocity = sqrt(
+      (distance_to_target + (current(1) * current(1)) / (2.0 * acceleration_) +
        (goal_velocity * goal_velocity) / (2.0 * deceleration_)) /
       (-1.0 / (2.0 * deceleration_) + 1.0 / (2.0 * acceleration_)));
 
@@ -97,24 +143,32 @@
   // how long to go at constant velocity.
   if (top_velocity > maximum_velocity_) {
     acceleration_time_ =
-        (maximum_velocity_ - output_(1)) / maximum_acceleration_;
-    constant_time_ =
-        (distance_to_target + (goal_velocity * goal_velocity -
-                               maximum_velocity_ * maximum_velocity_) /
-                                  (2.0 * maximum_acceleration_)) /
-        maximum_velocity_;
+        (maximum_velocity_ - current(1)) / maximum_acceleration_;
+    constant_time_ = ((-0.5 * maximum_acceleration_ * acceleration_time_ *
+                           acceleration_time_ -
+                       current(1) * acceleration_time_) +
+                      distance_to_target +
+                      (goal_velocity * goal_velocity -
+                       maximum_velocity_ * maximum_velocity_) /
+                          (2.0 * maximum_deceleration_)) /
+                     maximum_velocity_;
   } else {
-    acceleration_time_ = (top_velocity - output_(1)) / acceleration_;
+    acceleration_time_ = (top_velocity - current(1)) / acceleration_;
   }
 
-  AOS_CHECK_GT(top_velocity, -maximum_velocity_);
+  CHECK_GT(top_velocity, -maximum_velocity_);
 
-  if (output_(1) > maximum_velocity_) {
+  if (current(1) > maximum_velocity_) {
     constant_time_ = 0;
     acceleration_time_ = 0;
   }
 
-  deceleration_time_ = (goal_velocity - top_velocity) / deceleration_;
+  deceleration_time_ =
+      (goal_velocity - ::std::min(top_velocity, maximum_velocity_)) /
+      deceleration_;
+  if (acceleration_time_ <= 0) acceleration_time_ = 0;
+  if (constant_time_ <= 0) constant_time_ = 0;
+  if (deceleration_time_ <= 0) deceleration_time_ = 0;
 }
 
 }  // namespace aos::util
diff --git a/aos/util/trapezoid_profile.h b/aos/util/trapezoid_profile.h
index 6669777..621ff30 100644
--- a/aos/util/trapezoid_profile.h
+++ b/aos/util/trapezoid_profile.h
@@ -9,57 +9,116 @@
 namespace aos::util {
 
 // Calculates a trapezoidal motion profile (like for a control loop's goals).
-// Supports having the end speed and position changed in the middle.
+// Supports having the destination position, acceleration, and velocity caps
+// changed in the middle, and for having different accelerations and
+// decelerations.
 //
 // The only units assumption that this class makes is that the unit of time is
 // seconds.
-class TrapezoidProfile {
+class AsymmetricTrapezoidProfile {
  public:
-  // delta_time is how long between each call to Update.
-  TrapezoidProfile(::std::chrono::nanoseconds delta_time);
+  // Constructs a profile.  delta_time is the timestep to assume when solving.
+  AsymmetricTrapezoidProfile(::std::chrono::nanoseconds delta_time);
 
-  // Updates the state.
+  // Updates the state to provide the next position and velocity to go to to
+  // follow the profile.
   const Eigen::Matrix<double, 2, 1> &Update(double goal_position,
                                             double goal_velocity);
-  // Useful for preventing windup etc.
+
+  // Updates the internal position.  Useful for handling windup when a loop
+  // saturates or gets disabled.
   void MoveCurrentState(const Eigen::Matrix<double, 2, 1> &current) {
     output_ = current;
   }
 
-  // Useful for preventing windup etc.
+  // Adjusts the internal position by the provided position delta.
   void MoveGoal(double dx) { output_(0, 0) += dx; }
 
+  // Sets the internal position to the provided position.
   void SetGoal(double x) { output_(0, 0) = x; }
 
   void set_maximum_acceleration(double maximum_acceleration) {
     maximum_acceleration_ = maximum_acceleration;
   }
+  void set_maximum_deceleration(double maximum_deceleration) {
+    maximum_deceleration_ = maximum_deceleration;
+  }
+
   void set_maximum_velocity(double maximum_velocity) {
     maximum_velocity_ = maximum_velocity;
   }
 
  private:
-  // Basic kinematics to update output_, given that we are going to accelerate
-  // by acceleration over delta_time.
+  // Updates output_ to match the basic kinematics, given that we are going to
+  // accelerate by acceleration over delta_time.
   void UpdateVals(double acceleration, double delta_time);
   // Calculates how long to go for each segment.
-  void CalculateTimes(double distance_to_target, double goal_velocity);
+  void CalculateTimes(double distance_to_target, double goal_velocity,
+                      Eigen::Matrix<double, 2, 1> current);
   // output_ is where it should go at time_.
   Eigen::Matrix<double, 2, 1> output_;
 
+  // Time and acceleration to slow down if we need to reverse directions.
+  double deceleration_reversal_time_;
+  double deceleration_reversal_;
+
+  // Time and acceleration to speed up with.
   double acceleration_time_;
   double acceleration_;
+  // Time to go at max speed at.
   double constant_time_;
+  // Time and acceleration to slow down with.
   double deceleration_time_;
   double deceleration_;
 
-  double maximum_acceleration_;
-  double maximum_velocity_;
+  double maximum_acceleration_ = 0;
+  double maximum_deceleration_ = 0;
+  double maximum_velocity_ = 0;
 
   // How long between calls to Update.
   ::std::chrono::nanoseconds timestep_;
 
-  DISALLOW_COPY_AND_ASSIGN(TrapezoidProfile);
+  DISALLOW_COPY_AND_ASSIGN(AsymmetricTrapezoidProfile);
+};
+
+// Class to implement a AsymmetricTrapezoidProfile where both acceleration and
+// deceleration match.
+class TrapezoidProfile {
+ public:
+  TrapezoidProfile(::std::chrono::nanoseconds delta_time)
+      : asymmetric_trapezoid_profile_(delta_time) {}
+
+  // Updates the state to provide the next position and velocity to go to to
+  // follow the profile.
+  const Eigen::Matrix<double, 2, 1> &Update(double goal_position,
+                                            double goal_velocity) {
+    return asymmetric_trapezoid_profile_.Update(goal_position, goal_velocity);
+  }
+
+  // Updates the internal position.  Useful for handling windup when a loop
+  // saturates or gets disabled.
+  void MoveCurrentState(const Eigen::Matrix<double, 2, 1> &current) {
+    asymmetric_trapezoid_profile_.MoveCurrentState(current);
+  }
+
+  // Adjusts the internal position by the provided position delta.
+  void MoveGoal(double dx) { asymmetric_trapezoid_profile_.MoveGoal(dx); }
+
+  // Sets the internal position to the provided position.
+  void SetGoal(double x) { asymmetric_trapezoid_profile_.SetGoal(x); }
+
+  void set_maximum_acceleration(double maximum_acceleration) {
+    asymmetric_trapezoid_profile_.set_maximum_acceleration(
+        maximum_acceleration);
+    asymmetric_trapezoid_profile_.set_maximum_deceleration(
+        maximum_acceleration);
+  }
+  void set_maximum_velocity(double maximum_velocity) {
+    asymmetric_trapezoid_profile_.set_maximum_velocity(maximum_velocity);
+  }
+
+ private:
+  AsymmetricTrapezoidProfile asymmetric_trapezoid_profile_;
 };
 
 }  // namespace aos::util
diff --git a/aos/util/trapezoid_profile_test.cc b/aos/util/trapezoid_profile_test.cc
index 92b9996..54ffeb5 100644
--- a/aos/util/trapezoid_profile_test.cc
+++ b/aos/util/trapezoid_profile_test.cc
@@ -1,6 +1,7 @@
 #include "aos/util/trapezoid_profile.h"
 
 #include "Eigen/Dense"
+#include "glog/logging.h"
 #include "gtest/gtest.h"
 
 namespace aos::util::testing {
@@ -10,9 +11,10 @@
   EIGEN_MAKE_ALIGNED_OPERATOR_NEW;
 
  protected:
-  TrapezoidProfileTest() : profile_(delta_time) {
+  TrapezoidProfileTest() : profile_(kDeltaTime) {
     position_.setZero();
     profile_.set_maximum_acceleration(0.75);
+    profile_.set_maximum_deceleration(0.75);
     profile_.set_maximum_velocity(1.75);
   }
 
@@ -21,9 +23,19 @@
     position_ = profile_.Update(goal_position, goal_velocity);
   }
 
+  void RunFor(double goal_position, double goal_velocity,
+              std::chrono::nanoseconds duration) {
+    while (duration > std::chrono::nanoseconds(0)) {
+      position_ = profile_.Update(goal_position, goal_velocity);
+      duration -= kDeltaTime;
+    }
+
+    ASSERT_EQ(duration.count(), 0);
+  }
+
   const Eigen::Matrix<double, 2, 1> &position() { return position_; }
 
-  TrapezoidProfile profile_;
+  AsymmetricTrapezoidProfile profile_;
 
   ::testing::AssertionResult At(double position, double velocity) {
     static const double kDoubleNear = 0.00001;
@@ -40,33 +52,31 @@
   }
 
  private:
-  static constexpr ::std::chrono::nanoseconds delta_time =
+  static constexpr ::std::chrono::nanoseconds kDeltaTime =
       ::std::chrono::milliseconds(10);
 
   Eigen::Matrix<double, 2, 1> position_;
 };
 
-constexpr ::std::chrono::nanoseconds TrapezoidProfileTest::delta_time;
+constexpr ::std::chrono::nanoseconds TrapezoidProfileTest::kDeltaTime;
 
 TEST_F(TrapezoidProfileTest, ReachesGoal) {
-  for (int i = 0; i < 450; ++i) {
-    RunIteration(3, 0);
-  }
+  RunFor(3, 0, std::chrono::milliseconds(4500));
   EXPECT_TRUE(At(3, 0));
 }
 
-// Tests that decresing the maximum velocity in the middle when it is already
+// Tests that decreasing the maximum velocity in the middle when it is already
 // moving faster than the new max is handled correctly.
 TEST_F(TrapezoidProfileTest, ContinousUnderVelChange) {
   profile_.set_maximum_velocity(1.75);
-  RunIteration(12.0, 0);
+  RunFor(12.0, 0, std::chrono::milliseconds(10));
   double last_pos = position()(0);
   double last_vel = 1.75;
   for (int i = 0; i < 1600; ++i) {
     if (i == 400) {
       profile_.set_maximum_velocity(0.75);
     }
-    RunIteration(12.0, 0);
+    RunFor(12.0, 0, std::chrono::milliseconds(10));
     if (i >= 400) {
       EXPECT_TRUE(::std::abs(last_pos - position()(0)) <= 1.75 * 0.01);
       EXPECT_NEAR(last_vel, ::std::abs(last_pos - position()(0)), 0.0001);
@@ -79,43 +89,212 @@
 
 // There is some somewhat tricky code for dealing with going backwards.
 TEST_F(TrapezoidProfileTest, Backwards) {
-  for (int i = 0; i < 400; ++i) {
-    RunIteration(-2, 0);
-  }
+  RunFor(-2, 0, std::chrono::milliseconds(4000));
   EXPECT_TRUE(At(-2, 0));
 }
 
 TEST_F(TrapezoidProfileTest, SwitchGoalInMiddle) {
-  for (int i = 0; i < 200; ++i) {
-    RunIteration(-2, 0);
-  }
+  RunFor(-2, 0, std::chrono::milliseconds(2000));
   EXPECT_FALSE(At(-2, 0));
-  for (int i = 0; i < 550; ++i) {
-    RunIteration(0, 0);
-  }
+  RunFor(0, 0, std::chrono::milliseconds(5500));
   EXPECT_TRUE(At(0, 0));
 }
 
 // Checks to make sure that it hits top speed.
 TEST_F(TrapezoidProfileTest, TopSpeed) {
-  for (int i = 0; i < 200; ++i) {
-    RunIteration(4, 0);
-  }
+  RunFor(4, 0, std::chrono::milliseconds(2000));
   EXPECT_NEAR(1.5, position()(1), 10e-5);
-  for (int i = 0; i < 2000; ++i) {
-    RunIteration(4, 0);
-  }
+  RunFor(4, 0, std::chrono::milliseconds(20000));
   EXPECT_TRUE(At(4, 0));
 }
 
 // Tests that the position and velocity exactly match at the end.  Some code we
 // have assumes this to be true as a simplification.
 TEST_F(TrapezoidProfileTest, ExactlyReachesGoal) {
-  for (int i = 0; i < 450; ++i) {
-    RunIteration(1, 0);
-  }
+  RunFor(1, 0, std::chrono::milliseconds(4500));
   EXPECT_EQ(position()(1), 0.0);
   EXPECT_EQ(position()(0), 1.0);
 }
 
+// Tests that we can move a goal without the trajectory teleporting.  The goal
+// needs to move to something we haven't already passed, but will blow by.
+TEST_F(TrapezoidProfileTest, MoveGoal) {
+  profile_.set_maximum_acceleration(2.0);
+  profile_.set_maximum_deceleration(2.0);
+  profile_.set_maximum_velocity(2.0);
+
+  RunFor(5.0, 0, std::chrono::seconds(1));
+  EXPECT_TRUE(At(1.0, 2.0));
+  RunFor(5.0, 0, std::chrono::seconds(1));
+  EXPECT_TRUE(At(3.0, 2.0));
+  RunFor(3.5, 0, std::chrono::seconds(1));
+  EXPECT_TRUE(At(4.0, 0.0));
+  RunFor(3.5, 0, std::chrono::seconds(1));
+  EXPECT_TRUE(At(3.5, 0.0));
+}
+
+// Tests that we can move a goal back before where we currently are without
+// teleporting.
+TEST_F(TrapezoidProfileTest, MoveGoalFar) {
+  profile_.set_maximum_acceleration(2.0);
+  profile_.set_maximum_deceleration(2.0);
+  profile_.set_maximum_velocity(2.0);
+
+  RunFor(5.0, 0, std::chrono::seconds(1));
+  EXPECT_TRUE(At(1.0, 2.0));
+  RunFor(5.0, 0, std::chrono::seconds(1));
+  EXPECT_TRUE(At(3.0, 2.0));
+  RunFor(2.5, 0, std::chrono::seconds(1));
+  EXPECT_TRUE(At(4.0, 0.0));
+  RunFor(2.5, 0, std::chrono::seconds(2));
+  EXPECT_TRUE(At(2.5, 0.0));
+}
+
+// Tests that we can move a goal without the trajectory teleporting.  The goal
+// needs to move to something we haven't already passed, but will blow by.  Do
+// this one in the negative direction.
+TEST_F(TrapezoidProfileTest, MoveGoalNegative) {
+  profile_.set_maximum_acceleration(2.0);
+  profile_.set_maximum_deceleration(2.0);
+  profile_.set_maximum_velocity(2.0);
+
+  RunFor(-5.0, 0, std::chrono::seconds(1));
+  EXPECT_TRUE(At(-1.0, -2.0));
+  RunFor(-5.0, 0, std::chrono::seconds(1));
+  EXPECT_TRUE(At(-3.0, -2.0));
+  RunFor(-3.5, 0, std::chrono::seconds(1));
+  EXPECT_TRUE(At(-4.0, 0.0));
+  RunFor(-3.5, 0, std::chrono::seconds(1));
+  EXPECT_TRUE(At(-3.5, 0.0));
+}
+
+// Tests that we can move a goal back before where we currently are without
+// teleporting.  Do this one in the negative direction.
+TEST_F(TrapezoidProfileTest, MoveGoalNegativeFar) {
+  profile_.set_maximum_acceleration(2.0);
+  profile_.set_maximum_deceleration(2.0);
+  profile_.set_maximum_velocity(2.0);
+
+  RunFor(-5.0, 0, std::chrono::seconds(1));
+  EXPECT_TRUE(At(-1.0, -2.0));
+  RunFor(-5.0, 0, std::chrono::seconds(1));
+  EXPECT_TRUE(At(-3.0, -2.0));
+  RunFor(-2.5, 0, std::chrono::seconds(1));
+  EXPECT_TRUE(At(-4.0, 0.0));
+  RunFor(-2.5, 0, std::chrono::seconds(2));
+  EXPECT_TRUE(At(-2.5, 0.0));
+}
+
+// Tests that we can execute a profile with acceleration and deceleration not
+// matching in magnitude.
+TEST_F(TrapezoidProfileTest, AsymmetricAccelDecel) {
+  // Accelerates up until t=1.  Will be at x=0.5
+  profile_.set_maximum_acceleration(1.0);
+  // Decelerates in t=0.5  Will take x=0.25
+  profile_.set_maximum_deceleration(2.0);
+  profile_.set_maximum_velocity(1.0);
+
+  RunFor(1.75, 0, std::chrono::seconds(1));
+
+  EXPECT_TRUE(At(0.5, 1.0));
+
+  RunFor(1.75, 0, std::chrono::seconds(1));
+  EXPECT_TRUE(At(1.5, 1.0));
+  RunFor(1.75, 0, std::chrono::milliseconds(500));
+  EXPECT_TRUE(At(1.75, 0.0));
+}
+
+// Tests that we can execute a profile with acceleration and deceleration not
+// matching in magnitude, and hitting saturation.
+TEST_F(TrapezoidProfileTest, AsymmetricAccelDecelUnconstrained) {
+  // Accelerates up until t=1.  Will be at x=0.5
+  profile_.set_maximum_acceleration(1.0);
+  // Decelerates in t=0.5  Will take x=0.25
+  profile_.set_maximum_deceleration(2.0);
+  profile_.set_maximum_velocity(2.0);
+
+  RunFor(0.75, 0, std::chrono::seconds(1));
+  EXPECT_TRUE(At(0.5, 1.0));
+
+  RunFor(0.75, 0, std::chrono::milliseconds(500));
+  EXPECT_TRUE(At(0.75, 0.0));
+}
+
+// Tests that we can execute a profile with acceleration and deceleration not
+// matching in magnitude, and hitting saturation.
+TEST_F(TrapezoidProfileTest, AsymmetricAccelDecelUnconstrainedNegative) {
+  // Accelerates up until t=1.  Will be at x=0.5
+  profile_.set_maximum_acceleration(1.0);
+  // Decelerates in t=0.5  Will take x=0.25
+  profile_.set_maximum_deceleration(2.0);
+  profile_.set_maximum_velocity(2.0);
+
+  RunFor(-0.75, 0, std::chrono::seconds(1));
+  EXPECT_TRUE(At(-0.5, -1.0));
+
+  RunFor(-0.75, 0, std::chrono::milliseconds(500));
+  EXPECT_TRUE(At(-0.75, 0.0));
+}
+
+// Tests that we can execute a profile with acceleration and deceleration not
+// matching in magnitude when going in the negative direction.
+TEST_F(TrapezoidProfileTest, AsymmetricAccelDecelNegative) {
+  // Accelerates up until t=1.  Will be at x=0.5
+  profile_.set_maximum_acceleration(1.0);
+  // Decelerates in t=0.5  Will take x=0.25
+  profile_.set_maximum_deceleration(2.0);
+  profile_.set_maximum_velocity(1.0);
+
+  RunFor(-1.75, 0, std::chrono::seconds(1));
+
+  EXPECT_TRUE(At(-0.5, -1.0));
+
+  RunFor(-1.75, 0, std::chrono::seconds(1));
+  EXPECT_TRUE(At(-1.5, -1.0));
+  RunFor(-1.75, 0, std::chrono::milliseconds(500));
+  EXPECT_TRUE(At(-1.75, 0.0));
+}
+
+// Tests that we can move the goal when an asymmetric profile is executing.
+TEST_F(TrapezoidProfileTest, AsymmetricAccelDecelMoveGoal) {
+  // Accelerates up until t=1.  Will be at x=0.5
+  profile_.set_maximum_acceleration(1.0);
+  // Decelerates in t=0.5  Will take x=0.25
+  profile_.set_maximum_deceleration(2.0);
+  profile_.set_maximum_velocity(1.0);
+
+  RunFor(1.75, 0, std::chrono::seconds(1));
+
+  EXPECT_TRUE(At(0.5, 1.0));
+
+  RunFor(1.75, 0, std::chrono::seconds(1));
+  EXPECT_TRUE(At(1.5, 1.0));
+  RunFor(1.6, 0, std::chrono::milliseconds(500));
+  EXPECT_TRUE(At(1.75, 0.0));
+  RunFor(1.6, 0, std::chrono::milliseconds(520));
+  RunFor(1.6, 0, std::chrono::milliseconds(2500));
+  EXPECT_TRUE(At(1.6, 0.0));
+}
+
+// Tests that we can move the goal when an asymmetric profile is executing in
+// the negative direction.
+TEST_F(TrapezoidProfileTest, AsymmetricAccelDecelMoveGoalFar) {
+  // Accelerates up until t=1.  Will be at x=0.5
+  profile_.set_maximum_acceleration(1.0);
+  // Decelerates in t=0.5  Will take x=0.25
+  profile_.set_maximum_deceleration(2.0);
+  profile_.set_maximum_velocity(1.0);
+
+  RunFor(1.75, 0, std::chrono::seconds(1));
+
+  EXPECT_TRUE(At(0.5, 1.0));
+
+  RunFor(1.75, 0, std::chrono::seconds(1));
+  EXPECT_TRUE(At(1.5, 1.0));
+  RunFor(1.0, 0, std::chrono::milliseconds(500));
+  EXPECT_TRUE(At(1.75, 0.0));
+  RunFor(1.0, 0, std::chrono::milliseconds(2500));
+  EXPECT_TRUE(At(1.0, 0.0));
+}
+
 }  // namespace aos::util::testing
diff --git a/aos/uuid.h b/aos/uuid.h
index 8dd93d4..a2cf8ae 100644
--- a/aos/uuid.h
+++ b/aos/uuid.h
@@ -8,6 +8,7 @@
 
 #include "absl/types/span.h"
 #include "flatbuffers/flatbuffers.h"
+#include "glog/logging.h"
 
 namespace aos {
 
@@ -66,6 +67,11 @@
   flatbuffers::Offset<flatbuffers::Vector<uint8_t>> PackVector(
       flatbuffers::FlatBufferBuilder *fbb) const;
 
+  template <typename T>
+  void PackStaticVector(T *static_vector) const {
+    CHECK(static_vector->FromData(data_.data(), data_.size()));
+  }
+
   // Returns a human-readable string representing this UUID.
   //
   // This is done without any memory allocation, which means it's returned in a
diff --git a/build_tests/BUILD b/build_tests/BUILD
index f0ff94d..282365e 100644
--- a/build_tests/BUILD
+++ b/build_tests/BUILD
@@ -4,7 +4,7 @@
 load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_go_library", "flatbuffer_py_library")
 load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
 load("//tools/build_rules:apache.bzl", "apache_wrapper")
-load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test")
+load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library")
 load("//tools/build_rules:autocxx.bzl", "autocxx_library")
 
 cc_test(
@@ -153,12 +153,6 @@
     target_compatible_with = ["//tools/platforms/rust:has_support"],
 )
 
-rust_test(
-    name = "hello_lib_test",
-    crate = ":hello_lib",
-    target_compatible_with = ["//tools/platforms/rust:has_support"],
-)
-
 rust_binary(
     name = "rust_hello",
     srcs = ["rust_hello.rs"],
@@ -199,15 +193,6 @@
     target_compatible_with = ["//tools/platforms/rust:has_support"],
 )
 
-rust_test(
-    name = "hello_autocxx_test",
-    crate = ":hello_autocxx",
-    # TODO: Make Rust play happy with pic vs nopic. Details at:
-    # https://github.com/bazelbuild/rules_rust/issues/118
-    rustc_flags = ["-Crelocation-model=static"],
-    target_compatible_with = ["//tools/platforms/rust:has_support"],
-)
-
 py_test(
     name = "upstream_python_test",
     srcs = [
diff --git a/circuit_boards/CAN_boards/CAN_Terminator/3D model/70555-0036_stp/705550036.stp b/circuit_boards/CAN_boards/CAN_Terminator/3D model/70555-0036_stp/705550036.stp
new file mode 100644
index 0000000..4c49dbe
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/3D model/70555-0036_stp/705550036.stp
@@ -0,0 +1,4390 @@
+ISO-10303-21;

+HEADER;

+FILE_DESCRIPTION((''),'2;1');

+FILE_NAME('705550036','2016-12-06T',('ghp'),(''),

+'PRO/ENGINEER BY PARAMETRIC TECHNOLOGY CORPORATION, 2012310',

+'PRO/ENGINEER BY PARAMETRIC TECHNOLOGY CORPORATION, 2012310','');

+FILE_SCHEMA(('CONFIG_CONTROL_DESIGN'));

+ENDSEC;

+DATA;

+#1=DIRECTION('',(1.E0,0.E0,0.E0));

+#2=VECTOR('',#1,7.E-2);

+#3=CARTESIAN_POINT('',(-1.65E-1,-1.E-1,-2.25E-1));

+#4=LINE('',#3,#2);

+#5=DIRECTION('',(7.071067811866E-1,-7.071067811865E-1,0.E0));

+#6=VECTOR('',#5,2.121320343560E-2);

+#7=CARTESIAN_POINT('',(-9.5E-2,-1.E-1,-2.25E-1));

+#8=LINE('',#7,#6);

+#9=DIRECTION('',(7.071067811865E-1,7.071067811865E-1,0.E0));

+#10=VECTOR('',#9,2.121320343560E-2);

+#11=CARTESIAN_POINT('',(-8.E-2,-1.15E-1,-2.25E-1));

+#12=LINE('',#11,#10);

+#13=DIRECTION('',(1.E0,0.E0,0.E0));

+#14=VECTOR('',#13,1.3E-1);

+#15=CARTESIAN_POINT('',(-6.5E-2,-1.E-1,-2.25E-1));

+#16=LINE('',#15,#14);

+#17=DIRECTION('',(7.071067811865E-1,-7.071067811865E-1,0.E0));

+#18=VECTOR('',#17,2.121320343560E-2);

+#19=CARTESIAN_POINT('',(6.5E-2,-1.E-1,-2.25E-1));

+#20=LINE('',#19,#18);

+#21=DIRECTION('',(7.071067811865E-1,7.071067811865E-1,0.E0));

+#22=VECTOR('',#21,2.121320343560E-2);

+#23=CARTESIAN_POINT('',(8.E-2,-1.15E-1,-2.25E-1));

+#24=LINE('',#23,#22);

+#25=DIRECTION('',(1.E0,0.E0,0.E0));

+#26=VECTOR('',#25,7.E-2);

+#27=CARTESIAN_POINT('',(9.5E-2,-1.E-1,-2.25E-1));

+#28=LINE('',#27,#26);

+#29=DIRECTION('',(0.E0,1.E0,0.E0));

+#30=VECTOR('',#29,5.E-2);

+#31=CARTESIAN_POINT('',(1.65E-1,-1.E-1,-2.25E-1));

+#32=LINE('',#31,#30);

+#33=DIRECTION('',(-1.E0,0.E0,0.E0));

+#34=VECTOR('',#33,2.5E-2);

+#35=CARTESIAN_POINT('',(1.65E-1,-5.E-2,-2.25E-1));

+#36=LINE('',#35,#34);

+#37=DIRECTION('',(0.E0,1.E0,0.E0));

+#38=VECTOR('',#37,1.5E-1);

+#39=CARTESIAN_POINT('',(1.4E-1,-5.E-2,-2.25E-1));

+#40=LINE('',#39,#38);

+#41=DIRECTION('',(-1.E0,0.E0,0.E0));

+#42=VECTOR('',#41,2.8E-1);

+#43=CARTESIAN_POINT('',(1.4E-1,1.E-1,-2.25E-1));

+#44=LINE('',#43,#42);

+#45=DIRECTION('',(0.E0,-1.E0,0.E0));

+#46=VECTOR('',#45,1.5E-1);

+#47=CARTESIAN_POINT('',(-1.4E-1,1.E-1,-2.25E-1));

+#48=LINE('',#47,#46);

+#49=DIRECTION('',(-1.E0,0.E0,0.E0));

+#50=VECTOR('',#49,2.5E-2);

+#51=CARTESIAN_POINT('',(-1.4E-1,-5.E-2,-2.25E-1));

+#52=LINE('',#51,#50);

+#53=DIRECTION('',(0.E0,-1.E0,0.E0));

+#54=VECTOR('',#53,5.E-2);

+#55=CARTESIAN_POINT('',(-1.65E-1,-5.E-2,-2.25E-1));

+#56=LINE('',#55,#54);

+#57=DIRECTION('',(0.E0,1.E0,0.E0));

+#58=VECTOR('',#57,1.2E-1);

+#59=CARTESIAN_POINT('',(-1.E-1,-6.E-2,-2.25E-1));

+#60=LINE('',#59,#58);

+#61=DIRECTION('',(1.E0,0.E0,0.E0));

+#62=VECTOR('',#61,2.E-1);

+#63=CARTESIAN_POINT('',(-1.E-1,6.E-2,-2.25E-1));

+#64=LINE('',#63,#62);

+#65=DIRECTION('',(0.E0,1.E0,0.E0));

+#66=VECTOR('',#65,1.2E-1);

+#67=CARTESIAN_POINT('',(1.E-1,-6.E-2,-2.25E-1));

+#68=LINE('',#67,#66);

+#69=DIRECTION('',(1.E0,0.E0,0.E0));

+#70=VECTOR('',#69,2.E-1);

+#71=CARTESIAN_POINT('',(-1.E-1,-6.E-2,-2.25E-1));

+#72=LINE('',#71,#70);

+#73=DIRECTION('',(5.E-1,0.E0,8.660254037845E-1));

+#74=VECTOR('',#73,1.028468088848E-1);

+#75=CARTESIAN_POINT('',(1.06E-1,-1.E-1,-5.5E-3));

+#76=LINE('',#75,#74);

+#77=DIRECTION('',(8.660254037844E-1,0.E0,5.E-1));

+#78=VECTOR('',#77,2.8E-3);

+#79=CARTESIAN_POINT('',(1.574234044424E-1,-1.E-1,8.356794919243E-2));

+#80=LINE('',#79,#78);

+#81=DIRECTION('',(-5.E-1,0.E0,8.660254037844E-1));

+#82=VECTOR('',#81,2.E-3);

+#83=CARTESIAN_POINT('',(1.598482755730E-1,-1.E-1,8.496794919243E-2));

+#84=LINE('',#83,#82);

+#85=DIRECTION('',(-8.660254037844E-1,0.E0,-5.E-1));

+#86=VECTOR('',#85,2.8E-3);

+#87=CARTESIAN_POINT('',(1.588482755730E-1,-1.E-1,8.67E-2));

+#88=LINE('',#87,#86);

+#89=DIRECTION('',(-1.E0,0.E0,0.E0));

+#90=VECTOR('',#89,6.142340444241E-2);

+#91=CARTESIAN_POINT('',(1.564234044424E-1,-1.E-1,8.53E-2));

+#92=LINE('',#91,#90);

+#93=DIRECTION('',(0.E0,0.E0,1.E0));

+#94=VECTOR('',#93,1.397E-1);

+#95=CARTESIAN_POINT('',(9.5E-2,-1.E-1,8.53E-2));

+#96=LINE('',#95,#94);

+#97=DIRECTION('',(0.E0,0.E0,1.E0));

+#98=VECTOR('',#97,2.350884572681E-1);

+#99=CARTESIAN_POINT('',(9.5E-2,-1.E-1,-2.25E-1));

+#100=LINE('',#99,#98);

+#101=DIRECTION('',(5.E-1,0.E0,-8.660254037845E-1));

+#102=VECTOR('',#101,1.8E-2);

+#103=CARTESIAN_POINT('',(9.5E-2,-1.E-1,1.008845726812E-2));

+#104=LINE('',#103,#102);

+#105=DIRECTION('',(0.E0,0.E0,-1.E0));

+#106=VECTOR('',#105,2.8E-3);

+#107=CARTESIAN_POINT('',(1.04E-1,-1.E-1,-5.5E-3));

+#108=LINE('',#107,#106);

+#109=DIRECTION('',(1.E0,0.E0,0.E0));

+#110=VECTOR('',#109,2.E-3);

+#111=CARTESIAN_POINT('',(1.04E-1,-1.E-1,-8.3E-3));

+#112=LINE('',#111,#110);

+#113=DIRECTION('',(0.E0,0.E0,1.E0));

+#114=VECTOR('',#113,2.8E-3);

+#115=CARTESIAN_POINT('',(1.06E-1,-1.E-1,-8.3E-3));

+#116=LINE('',#115,#114);

+#117=DIRECTION('',(-5.E-1,0.E0,8.660254037845E-1));

+#118=VECTOR('',#117,1.028468088848E-1);

+#119=CARTESIAN_POINT('',(-1.06E-1,-1.E-1,-5.5E-3));

+#120=LINE('',#119,#118);

+#121=DIRECTION('',(0.E0,0.E0,1.E0));

+#122=VECTOR('',#121,2.8E-3);

+#123=CARTESIAN_POINT('',(-1.06E-1,-1.E-1,-8.3E-3));

+#124=LINE('',#123,#122);

+#125=DIRECTION('',(-1.E0,0.E0,0.E0));

+#126=VECTOR('',#125,2.E-3);

+#127=CARTESIAN_POINT('',(-1.04E-1,-1.E-1,-8.3E-3));

+#128=LINE('',#127,#126);

+#129=DIRECTION('',(0.E0,0.E0,-1.E0));

+#130=VECTOR('',#129,2.8E-3);

+#131=CARTESIAN_POINT('',(-1.04E-1,-1.E-1,-5.5E-3));

+#132=LINE('',#131,#130);

+#133=DIRECTION('',(-5.E-1,0.E0,-8.660254037845E-1));

+#134=VECTOR('',#133,1.8E-2);

+#135=CARTESIAN_POINT('',(-9.5E-2,-1.E-1,1.008845726812E-2));

+#136=LINE('',#135,#134);

+#137=DIRECTION('',(0.E0,0.E0,1.E0));

+#138=VECTOR('',#137,2.350884572681E-1);

+#139=CARTESIAN_POINT('',(-9.5E-2,-1.E-1,-2.25E-1));

+#140=LINE('',#139,#138);

+#141=DIRECTION('',(0.E0,0.E0,1.E0));

+#142=VECTOR('',#141,4.5E-1);

+#143=CARTESIAN_POINT('',(-1.65E-1,-1.E-1,-2.25E-1));

+#144=LINE('',#143,#142);

+#145=DIRECTION('',(0.E0,0.E0,1.E0));

+#146=VECTOR('',#145,1.397E-1);

+#147=CARTESIAN_POINT('',(-9.5E-2,-1.E-1,8.53E-2));

+#148=LINE('',#147,#146);

+#149=DIRECTION('',(1.E0,0.E0,0.E0));

+#150=VECTOR('',#149,6.142340444241E-2);

+#151=CARTESIAN_POINT('',(-1.564234044424E-1,-1.E-1,8.53E-2));

+#152=LINE('',#151,#150);

+#153=DIRECTION('',(8.660254037844E-1,0.E0,-5.E-1));

+#154=VECTOR('',#153,2.8E-3);

+#155=CARTESIAN_POINT('',(-1.588482755730E-1,-1.E-1,8.67E-2));

+#156=LINE('',#155,#154);

+#157=DIRECTION('',(5.E-1,0.E0,8.660254037844E-1));

+#158=VECTOR('',#157,2.E-3);

+#159=CARTESIAN_POINT('',(-1.598482755730E-1,-1.E-1,8.496794919243E-2));

+#160=LINE('',#159,#158);

+#161=DIRECTION('',(-8.660254037844E-1,0.E0,5.E-1));

+#162=VECTOR('',#161,2.8E-3);

+#163=CARTESIAN_POINT('',(-1.574234044424E-1,-1.E-1,8.356794919243E-2));

+#164=LINE('',#163,#162);

+#165=DIRECTION('',(0.E0,0.E0,-1.E0));

+#166=VECTOR('',#165,3.5E-1);

+#167=CARTESIAN_POINT('',(-2.5E-2,-1.E-1,2.25E-1));

+#168=LINE('',#167,#166);

+#169=DIRECTION('',(1.E0,0.E0,0.E0));

+#170=VECTOR('',#169,5.E-2);

+#171=CARTESIAN_POINT('',(-2.5E-2,-1.E-1,-1.25E-1));

+#172=LINE('',#171,#170);

+#173=DIRECTION('',(0.E0,0.E0,-1.E0));

+#174=VECTOR('',#173,3.5E-1);

+#175=CARTESIAN_POINT('',(2.5E-2,-1.E-1,2.25E-1));

+#176=LINE('',#175,#174);

+#177=DIRECTION('',(0.E0,0.E0,1.E0));

+#178=VECTOR('',#177,1.397E-1);

+#179=CARTESIAN_POINT('',(6.5E-2,-1.E-1,8.53E-2));

+#180=LINE('',#179,#178);

+#181=DIRECTION('',(-1.E0,0.E0,0.E0));

+#182=VECTOR('',#181,1.142340444241E-2);

+#183=CARTESIAN_POINT('',(6.5E-2,-1.E-1,8.53E-2));

+#184=LINE('',#183,#182);

+#185=DIRECTION('',(-8.660254037844E-1,0.E0,5.E-1));

+#186=VECTOR('',#185,2.8E-3);

+#187=CARTESIAN_POINT('',(5.357659555759E-2,-1.E-1,8.53E-2));

+#188=LINE('',#187,#186);

+#189=DIRECTION('',(-5.E-1,0.E0,-8.660254037845E-1));

+#190=VECTOR('',#189,2.E-3);

+#191=CARTESIAN_POINT('',(5.115172442699E-2,-1.E-1,8.67E-2));

+#192=LINE('',#191,#190);

+#193=DIRECTION('',(8.660254037845E-1,0.E0,-5.E-1));

+#194=VECTOR('',#193,2.8E-3);

+#195=CARTESIAN_POINT('',(5.015172442699E-2,-1.E-1,8.496794919243E-2));

+#196=LINE('',#195,#194);

+#197=DIRECTION('',(5.E-1,0.E0,-8.660254037845E-1));

+#198=VECTOR('',#197,2.484680888483E-2);

+#199=CARTESIAN_POINT('',(5.257659555759E-2,-1.E-1,8.356794919243E-2));

+#200=LINE('',#199,#198);

+#201=DIRECTION('',(0.E0,0.E0,1.E0));

+#202=VECTOR('',#201,2.870499814952E-1);

+#203=CARTESIAN_POINT('',(6.5E-2,-1.E-1,-2.25E-1));

+#204=LINE('',#203,#202);

+#205=DIRECTION('',(0.E0,0.E0,1.E0));

+#206=VECTOR('',#205,2.870499814952E-1);

+#207=CARTESIAN_POINT('',(-6.5E-2,-1.E-1,-2.25E-1));

+#208=LINE('',#207,#206);

+#209=DIRECTION('',(-5.E-1,0.E0,-8.660254037845E-1));

+#210=VECTOR('',#209,2.484680888483E-2);

+#211=CARTESIAN_POINT('',(-5.257659555759E-2,-1.E-1,8.356794919243E-2));

+#212=LINE('',#211,#210);

+#213=DIRECTION('',(-8.660254037845E-1,0.E0,-5.E-1));

+#214=VECTOR('',#213,2.8E-3);

+#215=CARTESIAN_POINT('',(-5.015172442699E-2,-1.E-1,8.496794919243E-2));

+#216=LINE('',#215,#214);

+#217=DIRECTION('',(5.E-1,0.E0,-8.660254037845E-1));

+#218=VECTOR('',#217,2.E-3);

+#219=CARTESIAN_POINT('',(-5.115172442699E-2,-1.E-1,8.67E-2));

+#220=LINE('',#219,#218);

+#221=DIRECTION('',(8.660254037844E-1,0.E0,5.E-1));

+#222=VECTOR('',#221,2.8E-3);

+#223=CARTESIAN_POINT('',(-5.357659555759E-2,-1.E-1,8.53E-2));

+#224=LINE('',#223,#222);

+#225=DIRECTION('',(1.E0,0.E0,0.E0));

+#226=VECTOR('',#225,1.142340444241E-2);

+#227=CARTESIAN_POINT('',(-6.5E-2,-1.E-1,8.53E-2));

+#228=LINE('',#227,#226);

+#229=DIRECTION('',(0.E0,0.E0,1.E0));

+#230=VECTOR('',#229,1.397E-1);

+#231=CARTESIAN_POINT('',(-6.5E-2,-1.E-1,8.53E-2));

+#232=LINE('',#231,#230);

+#233=CARTESIAN_POINT('',(1.359067439440E-1,-1.707146793688E-1,4.63E-2));

+#234=CARTESIAN_POINT('',(1.347818470453E-1,-1.716427314769E-1,

+4.435162141820E-2));

+#235=CARTESIAN_POINT('',(1.325826344524E-1,-1.732269599096E-1,

+4.054247347054E-2));

+#236=CARTESIAN_POINT('',(1.294826259671E-1,-1.747302252402E-1,

+3.517310127021E-2));

+#237=CARTESIAN_POINT('',(1.264158758803E-1,-1.754149394403E-1,

+2.986133430575E-2));

+#238=CARTESIAN_POINT('',(1.233600650301E-1,-1.752360062162E-1,

+2.456851465476E-2));

+#239=CARTESIAN_POINT('',(1.202923466739E-1,-1.742123050750E-1,

+1.925507059854E-2));

+#240=CARTESIAN_POINT('',(1.171345716667E-1,-1.723650607587E-1,

+1.378564384723E-2));

+#241=CARTESIAN_POINT('',(1.137627364566E-1,-1.696464238197E-1,

+7.945453948624E-3));

+#242=CARTESIAN_POINT('',(1.100782418638E-1,-1.659770814270E-1,

+1.563722113738E-3));

+#243=CARTESIAN_POINT('',(1.073972705934E-1,-1.628925036232E-1,

+-3.079856340275E-3));

+#244=CARTESIAN_POINT('',(1.06E-1,-1.611975091251E-1,-5.5E-3));

+#246=CARTESIAN_POINT('',(1.574234044424E-1,-1.445586341714E-1,

+8.356794919243E-2));

+#247=CARTESIAN_POINT('',(1.556510049068E-1,-1.471059134075E-1,

+8.049806314542E-2));

+#248=CARTESIAN_POINT('',(1.522538698027E-1,-1.518856038577E-1,

+7.461405254494E-2));

+#249=CARTESIAN_POINT('',(1.476039632571E-1,-1.580684021107E-1,

+6.656017815753E-2));

+#250=CARTESIAN_POINT('',(1.433530093473E-1,-1.632839965971E-1,

+5.919731000509E-2));

+#251=CARTESIAN_POINT('',(1.394853686290E-1,-1.675180097546E-1,

+5.249835977563E-2));

+#252=CARTESIAN_POINT('',(1.370686310072E-1,-1.697561099750E-1,

+4.831244742601E-2));

+#253=CARTESIAN_POINT('',(1.359067439440E-1,-1.707146793688E-1,4.63E-2));

+#255=DIRECTION('',(0.E0,-1.E0,0.E0));

+#256=VECTOR('',#255,6.119750912510E-2);

+#257=CARTESIAN_POINT('',(1.06E-1,-1.E-1,-5.5E-3));

+#258=LINE('',#257,#256);

+#259=CARTESIAN_POINT('',(1.06E-1,-1.611975091251E-1,-5.5E-3));

+#260=CARTESIAN_POINT('',(1.06E-1,-1.608256025983E-1,-5.962053370720E-3));

+#261=CARTESIAN_POINT('',(1.06E-1,-1.600776027218E-1,-6.890766483237E-3));

+#262=CARTESIAN_POINT('',(1.06E-1,-1.593212552633E-1,-7.828713292664E-3));

+#263=CARTESIAN_POINT('',(1.06E-1,-1.589410006771E-1,-8.3E-3));

+#265=DIRECTION('',(0.E0,-1.E0,0.E0));

+#266=VECTOR('',#265,5.894100067710E-2);

+#267=CARTESIAN_POINT('',(1.06E-1,-1.E-1,-8.3E-3));

+#268=LINE('',#267,#266);

+#269=CARTESIAN_POINT('',(1.06E-1,-1.589410006771E-1,-8.3E-3));

+#270=CARTESIAN_POINT('',(1.056659451396E-1,-1.589985187898E-1,-8.3E-3));

+#271=CARTESIAN_POINT('',(1.049985592412E-1,-1.591086984013E-1,-8.3E-3));

+#272=CARTESIAN_POINT('',(1.043326136922E-1,-1.592091252124E-1,-8.3E-3));

+#273=CARTESIAN_POINT('',(1.04E-1,-1.592568924846E-1,-8.3E-3));

+#275=DIRECTION('',(0.E0,-1.E0,0.E0));

+#276=VECTOR('',#275,5.925689248462E-2);

+#277=CARTESIAN_POINT('',(1.04E-1,-1.E-1,-8.3E-3));

+#278=LINE('',#277,#276);

+#279=CARTESIAN_POINT('',(1.04E-1,-1.592568924846E-1,-8.3E-3));

+#280=CARTESIAN_POINT('',(1.04E-1,-1.596394088286E-1,-7.829161520218E-3));

+#281=CARTESIAN_POINT('',(1.04E-1,-1.604007484507E-1,-6.891661373974E-3));

+#282=CARTESIAN_POINT('',(1.04E-1,-1.611546836768E-1,-5.962500002914E-3));

+#283=CARTESIAN_POINT('',(1.04E-1,-1.615297952414E-1,-5.5E-3));

+#285=DIRECTION('',(0.E0,-1.E0,0.E0));

+#286=VECTOR('',#285,6.152979524142E-2);

+#287=CARTESIAN_POINT('',(1.04E-1,-1.E-1,-5.5E-3));

+#288=LINE('',#287,#286);

+#289=CARTESIAN_POINT('',(1.04E-1,-1.615297952414E-1,-5.5E-3));

+#290=CARTESIAN_POINT('',(1.026677871810E-1,-1.636025904772E-1,

+-3.192539710925E-3));

+#291=CARTESIAN_POINT('',(1.001103913130E-1,-1.675448457360E-1,

+1.236999867532E-3));

+#292=CARTESIAN_POINT('',(9.659307349503E-2,-1.728280968712E-1,

+7.329173034530E-3));

+#293=CARTESIAN_POINT('',(9.337629943372E-2,-1.774769255690E-1,

+1.290078914519E-2));

+#294=CARTESIAN_POINT('',(9.043799421210E-2,-1.814798916580E-1,

+1.799008307719E-2));

+#295=CARTESIAN_POINT('',(8.774875873874E-2,-1.848176417052E-1,

+2.264797555057E-2));

+#296=CARTESIAN_POINT('',(8.529914553242E-2,-1.874264761523E-1,

+2.689083008281E-2));

+#297=CARTESIAN_POINT('',(8.306147490191E-2,-1.892749685909E-1,

+3.076658930545E-2));

+#298=CARTESIAN_POINT('',(8.084973385689E-2,-1.904151960862E-1,

+3.459743716861E-2));

+#299=CARTESIAN_POINT('',(7.856031871802E-2,-1.906603971436E-1,

+3.856282050876E-2));

+#300=CARTESIAN_POINT('',(7.631311750265E-2,-1.899215158794E-1,

+4.245508718861E-2));

+#301=CARTESIAN_POINT('',(7.483504001672E-2,-1.889022348772E-1,

+4.501519249176E-2));

+#302=CARTESIAN_POINT('',(7.409325605598E-2,-1.882902550866E-1,4.63E-2));

+#304=DIRECTION('',(0.E0,-1.E0,0.E0));

+#305=VECTOR('',#304,5.881666482353E-2);

+#306=CARTESIAN_POINT('',(5.257659555759E-2,-1.E-1,8.356794919243E-2));

+#307=LINE('',#306,#305);

+#308=CARTESIAN_POINT('',(7.409325605598E-2,-1.882902550866E-1,4.63E-2));

+#309=CARTESIAN_POINT('',(7.331203565857E-2,-1.876457398093E-1,

+4.765311342023E-2));

+#310=CARTESIAN_POINT('',(7.171639888257E-2,-1.861029442667E-1,

+5.041683738668E-2));

+#311=CARTESIAN_POINT('',(6.926336705934E-2,-1.832450047646E-1,

+5.466561313710E-2));

+#312=CARTESIAN_POINT('',(6.652871483494E-2,-1.796792193425E-1,

+5.940216973080E-2));

+#313=CARTESIAN_POINT('',(6.352762475034E-2,-1.754649235327E-1,

+6.460021023541E-2));

+#314=CARTESIAN_POINT('',(6.022345713244E-2,-1.705986395660E-1,

+7.032319642634E-2));

+#315=CARTESIAN_POINT('',(5.659934298402E-2,-1.650870607740E-1,

+7.660034626383E-2));

+#316=CARTESIAN_POINT('',(5.395542322199E-2,-1.609771813768E-1,

+8.117974962281E-2));

+#317=CARTESIAN_POINT('',(5.257659555759E-2,-1.588166648235E-1,

+8.356794919243E-2));

+#319=DIRECTION('',(7.071067811865E-1,7.071067811865E-1,0.E0));

+#320=VECTOR('',#319,2.121320343560E-2);

+#321=CARTESIAN_POINT('',(8.E-2,-1.15E-1,8.53E-2));

+#322=LINE('',#321,#320);

+#323=DIRECTION('',(0.E0,0.E0,1.E0));

+#324=VECTOR('',#323,1.397E-1);

+#325=CARTESIAN_POINT('',(8.E-2,-1.15E-1,8.53E-2));

+#326=LINE('',#325,#324);

+#327=DIRECTION('',(4.472135954999E-1,4.472135954999E-1,-7.745966692415E-1));

+#328=VECTOR('',#327,3.354101966250E-2);

+#329=CARTESIAN_POINT('',(8.E-2,-1.15E-1,3.606921938166E-2));

+#330=LINE('',#329,#328);

+#331=DIRECTION('',(0.E0,0.E0,1.E0));

+#332=VECTOR('',#331,2.610692193817E-1);

+#333=CARTESIAN_POINT('',(8.E-2,-1.15E-1,-2.25E-1));

+#334=LINE('',#333,#332);

+#335=CARTESIAN_POINT('',(5.357659555759E-2,-1.584529252708E-1,8.53E-2));

+#336=CARTESIAN_POINT('',(5.663244194723E-2,-1.602747598675E-1,8.53E-2));

+#337=CARTESIAN_POINT('',(6.248790584942E-2,-1.635693235480E-1,8.53E-2));

+#338=CARTESIAN_POINT('',(7.050492033487E-2,-1.674170456356E-1,8.53E-2));

+#339=CARTESIAN_POINT('',(7.781939434949E-2,-1.701918575467E-1,8.53E-2));

+#340=CARTESIAN_POINT('',(8.468278420756E-2,-1.720001485712E-1,8.53E-2));

+#341=CARTESIAN_POINT('',(9.141161088772E-2,-1.728930552188E-1,8.53E-2));

+#342=CARTESIAN_POINT('',(9.812246616123E-2,-1.728326515409E-1,8.53E-2));

+#343=CARTESIAN_POINT('',(1.048466874162E-1,-1.718260528183E-1,8.53E-2));

+#344=CARTESIAN_POINT('',(1.117314134277E-1,-1.699090466749E-1,8.53E-2));

+#345=CARTESIAN_POINT('',(1.190967014330E-1,-1.670202176413E-1,8.53E-2));

+#346=CARTESIAN_POINT('',(1.271778953659E-1,-1.630564628607E-1,8.53E-2));

+#347=CARTESIAN_POINT('',(1.360335903179E-1,-1.580014158263E-1,8.53E-2));

+#348=CARTESIAN_POINT('',(1.457051050551E-1,-1.518781217069E-1,8.53E-2));

+#349=CARTESIAN_POINT('',(1.527502643574E-1,-1.470854943992E-1,8.53E-2));

+#350=CARTESIAN_POINT('',(1.564234044424E-1,-1.445195163487E-1,8.53E-2));

+#352=DIRECTION('',(0.E0,-1.E0,0.E0));

+#353=VECTOR('',#352,4.451951634874E-2);

+#354=CARTESIAN_POINT('',(1.564234044424E-1,-1.E-1,8.53E-2));

+#355=LINE('',#354,#353);

+#356=DIRECTION('',(-7.713691830612E-1,-6.363879189799E-1,0.E0));

+#357=VECTOR('',#356,3.737601997139E-2);

+#358=CARTESIAN_POINT('',(1.359067439440E-1,-1.707146793688E-1,4.63E-2));

+#359=LINE('',#358,#357);

+#360=DIRECTION('',(7.713691830612E-1,-6.363879189799E-1,0.E0));

+#361=VECTOR('',#360,9.758311976582E-3);

+#362=CARTESIAN_POINT('',(7.409325605598E-2,-1.882902550866E-1,4.63E-2));

+#363=LINE('',#362,#361);

+#364=CARTESIAN_POINT('',(9.434827557301E-2,-1.790729432769E-1,4.63E-2));

+#365=DIRECTION('',(0.E0,0.E0,-1.E0));

+#366=DIRECTION('',(6.363879189799E-1,-7.713691830612E-1,0.E0));

+#367=AXIS2_PLACEMENT_3D('',#364,#365,#366);

+#369=CARTESIAN_POINT('',(9.434827557301E-2,-1.790729432769E-1,4.63E-2));

+#370=DIRECTION('',(0.E0,0.E0,1.E0));

+#371=DIRECTION('',(-6.363879189799E-1,-7.713691830612E-1,0.E0));

+#372=AXIS2_PLACEMENT_3D('',#369,#370,#371);

+#374=CARTESIAN_POINT('',(9.434827557301E-2,-1.945003269382E-1,4.63E-2));

+#375=DIRECTION('',(0.E0,-1.E0,0.E0));

+#376=DIRECTION('',(1.E0,0.E0,0.E0));

+#377=AXIS2_PLACEMENT_3D('',#374,#375,#376);

+#379=CARTESIAN_POINT('',(9.434827557301E-2,-1.945003269382E-1,4.63E-2));

+#380=DIRECTION('',(0.E0,1.E0,0.E0));

+#381=DIRECTION('',(1.E0,0.E0,0.E0));

+#382=AXIS2_PLACEMENT_3D('',#379,#380,#381);

+#384=CARTESIAN_POINT('',(5.257659555759E-2,-1.588166648235E-1,

+8.356794919243E-2));

+#385=CARTESIAN_POINT('',(5.217581296567E-2,-1.584428483702E-1,

+8.379934112976E-2));

+#386=CARTESIAN_POINT('',(5.137089021924E-2,-1.576916919872E-1,

+8.426406349409E-2));

+#387=CARTESIAN_POINT('',(5.055923711301E-2,-1.569335104496E-1,

+8.473267163346E-2));

+#388=CARTESIAN_POINT('',(5.015172442699E-2,-1.565526684416E-1,

+8.496794919243E-2));

+#390=DIRECTION('',(0.E0,-1.E0,0.E0));

+#391=VECTOR('',#390,5.655266844162E-2);

+#392=CARTESIAN_POINT('',(5.015172442699E-2,-1.E-1,8.496794919243E-2));

+#393=LINE('',#392,#391);

+#394=CARTESIAN_POINT('',(5.015172442699E-2,-1.565526684416E-1,

+8.496794919243E-2));

+#395=CARTESIAN_POINT('',(5.031806493439E-2,-1.564994378828E-1,

+8.525605940260E-2));

+#396=CARTESIAN_POINT('',(5.065107110442E-2,-1.563883832289E-1,

+8.583284300832E-2));

+#397=CARTESIAN_POINT('',(5.098473039170E-2,-1.562681801997E-1,

+8.641075784631E-2));

+#398=CARTESIAN_POINT('',(5.115172442699E-2,-1.562058027820E-1,8.67E-2));

+#400=DIRECTION('',(0.E0,-1.E0,0.E0));

+#401=VECTOR('',#400,5.620580278197E-2);

+#402=CARTESIAN_POINT('',(5.115172442699E-2,-1.E-1,8.67E-2));

+#403=LINE('',#402,#401);

+#404=CARTESIAN_POINT('',(5.115172442699E-2,-1.562058027820E-1,8.67E-2));

+#405=CARTESIAN_POINT('',(5.155953767326E-2,-1.565842164206E-1,

+8.646454891248E-2));

+#406=CARTESIAN_POINT('',(5.237149011554E-2,-1.573371590786E-1,

+8.599576795137E-2));

+#407=CARTESIAN_POINT('',(5.317611227978E-2,-1.580823069249E-1,

+8.553121912824E-2));

+#408=CARTESIAN_POINT('',(5.357659555759E-2,-1.584529252708E-1,8.53E-2));

+#410=DIRECTION('',(0.E0,-1.E0,0.E0));

+#411=VECTOR('',#410,5.845292527080E-2);

+#412=CARTESIAN_POINT('',(5.357659555759E-2,-1.E-1,8.53E-2));

+#413=LINE('',#412,#411);

+#414=CARTESIAN_POINT('',(1.564234044424E-1,-1.445195163487E-1,8.53E-2));

+#415=CARTESIAN_POINT('',(1.568263784217E-1,-1.441358947675E-1,

+8.553265713540E-2));

+#416=CARTESIAN_POINT('',(1.576334974556E-1,-1.433675265797E-1,

+8.599864752688E-2));

+#417=CARTESIAN_POINT('',(1.584429590450E-1,-1.425969089083E-1,

+8.646599039342E-2));

+#418=CARTESIAN_POINT('',(1.588482755730E-1,-1.422110378340E-1,8.67E-2));

+#420=DIRECTION('',(0.E0,-1.E0,0.E0));

+#421=VECTOR('',#420,4.221103783395E-2);

+#422=CARTESIAN_POINT('',(1.588482755730E-1,-1.E-1,8.67E-2));

+#423=LINE('',#422,#421);

+#424=CARTESIAN_POINT('',(1.588482755730E-1,-1.422110378340E-1,8.67E-2));

+#425=CARTESIAN_POINT('',(1.590149631034E-1,-1.422209303221E-1,

+8.641128872844E-2));

+#426=CARTESIAN_POINT('',(1.593483172721E-1,-1.422371043354E-1,

+8.583390237121E-2));

+#427=CARTESIAN_POINT('',(1.596816297564E-1,-1.422460507655E-1,

+8.525658821359E-2));

+#428=CARTESIAN_POINT('',(1.598482755730E-1,-1.422487166033E-1,

+8.496794919243E-2));

+#430=DIRECTION('',(0.E0,-1.E0,0.E0));

+#431=VECTOR('',#430,4.224871660332E-2);

+#432=CARTESIAN_POINT('',(1.598482755730E-1,-1.E-1,8.496794919243E-2));

+#433=LINE('',#432,#431);

+#434=CARTESIAN_POINT('',(1.598482755730E-1,-1.422487166033E-1,

+8.496794919243E-2));

+#435=CARTESIAN_POINT('',(1.594435182579E-1,-1.426342866654E-1,

+8.473426244761E-2));

+#436=CARTESIAN_POINT('',(1.586346157645E-1,-1.434048430029E-1,

+8.426724237526E-2));

+#437=CARTESIAN_POINT('',(1.578269375145E-1,-1.441742317253E-1,

+8.380092912024E-2));

+#438=CARTESIAN_POINT('',(1.574234044424E-1,-1.445586341714E-1,

+8.356794919243E-2));

+#440=DIRECTION('',(0.E0,-1.E0,0.E0));

+#441=VECTOR('',#440,4.455863417144E-2);

+#442=CARTESIAN_POINT('',(1.574234044424E-1,-1.E-1,8.356794919243E-2));

+#443=LINE('',#442,#441);

+#444=DIRECTION('',(7.071067811866E-1,-7.071067811865E-1,0.E0));

+#445=VECTOR('',#444,2.121320343560E-2);

+#446=CARTESIAN_POINT('',(6.5E-2,-1.E-1,8.53E-2));

+#447=LINE('',#446,#445);

+#448=DIRECTION('',(4.472135954999E-1,-4.472135954999E-1,-7.745966692415E-1));

+#449=VECTOR('',#448,3.354101966250E-2);

+#450=CARTESIAN_POINT('',(6.5E-2,-1.E-1,6.204998149519E-2));

+#451=LINE('',#450,#449);

+#452=DIRECTION('',(1.E0,0.E0,0.E0));

+#453=VECTOR('',#452,2.5E-1);

+#454=CARTESIAN_POINT('',(-1.25E-1,-5.5E-2,2.25E-1));

+#455=LINE('',#454,#453);

+#456=DIRECTION('',(1.E0,0.E0,0.E0));

+#457=VECTOR('',#456,4.E-2);

+#458=CARTESIAN_POINT('',(-6.5E-2,-1.E-1,2.25E-1));

+#459=LINE('',#458,#457);

+#460=DIRECTION('',(7.071067811865E-1,7.071067811865E-1,0.E0));

+#461=VECTOR('',#460,2.121320343560E-2);

+#462=CARTESIAN_POINT('',(-8.E-2,-1.15E-1,2.25E-1));

+#463=LINE('',#462,#461);

+#464=DIRECTION('',(7.071067811866E-1,-7.071067811865E-1,0.E0));

+#465=VECTOR('',#464,2.121320343560E-2);

+#466=CARTESIAN_POINT('',(-9.5E-2,-1.E-1,2.25E-1));

+#467=LINE('',#466,#465);

+#468=DIRECTION('',(1.E0,0.E0,0.E0));

+#469=VECTOR('',#468,7.E-2);

+#470=CARTESIAN_POINT('',(-1.65E-1,-1.E-1,2.25E-1));

+#471=LINE('',#470,#469);

+#472=DIRECTION('',(0.E0,-1.E0,0.E0));

+#473=VECTOR('',#472,5.E-2);

+#474=CARTESIAN_POINT('',(-1.65E-1,-5.E-2,2.25E-1));

+#475=LINE('',#474,#473);

+#476=DIRECTION('',(-1.E0,0.E0,0.E0));

+#477=VECTOR('',#476,2.5E-2);

+#478=CARTESIAN_POINT('',(-1.4E-1,-5.E-2,2.25E-1));

+#479=LINE('',#478,#477);

+#480=DIRECTION('',(0.E0,-1.E0,0.E0));

+#481=VECTOR('',#480,2.1E-1);

+#482=CARTESIAN_POINT('',(-1.4E-1,1.6E-1,2.25E-1));

+#483=LINE('',#482,#481);

+#484=DIRECTION('',(-1.E0,0.E0,0.E0));

+#485=VECTOR('',#484,2.8E-1);

+#486=CARTESIAN_POINT('',(1.4E-1,1.6E-1,2.25E-1));

+#487=LINE('',#486,#485);

+#488=DIRECTION('',(0.E0,1.E0,0.E0));

+#489=VECTOR('',#488,2.1E-1);

+#490=CARTESIAN_POINT('',(1.4E-1,-5.E-2,2.25E-1));

+#491=LINE('',#490,#489);

+#492=DIRECTION('',(-1.E0,0.E0,0.E0));

+#493=VECTOR('',#492,2.5E-2);

+#494=CARTESIAN_POINT('',(1.65E-1,-5.E-2,2.25E-1));

+#495=LINE('',#494,#493);

+#496=DIRECTION('',(0.E0,1.E0,0.E0));

+#497=VECTOR('',#496,5.E-2);

+#498=CARTESIAN_POINT('',(1.65E-1,-1.E-1,2.25E-1));

+#499=LINE('',#498,#497);

+#500=DIRECTION('',(1.E0,0.E0,0.E0));

+#501=VECTOR('',#500,7.E-2);

+#502=CARTESIAN_POINT('',(9.5E-2,-1.E-1,2.25E-1));

+#503=LINE('',#502,#501);

+#504=DIRECTION('',(7.071067811865E-1,7.071067811865E-1,0.E0));

+#505=VECTOR('',#504,2.121320343560E-2);

+#506=CARTESIAN_POINT('',(8.E-2,-1.15E-1,2.25E-1));

+#507=LINE('',#506,#505);

+#508=DIRECTION('',(7.071067811865E-1,-7.071067811865E-1,0.E0));

+#509=VECTOR('',#508,2.121320343560E-2);

+#510=CARTESIAN_POINT('',(6.5E-2,-1.E-1,2.25E-1));

+#511=LINE('',#510,#509);

+#512=DIRECTION('',(1.E0,0.E0,0.E0));

+#513=VECTOR('',#512,4.E-2);

+#514=CARTESIAN_POINT('',(2.5E-2,-1.E-1,2.25E-1));

+#515=LINE('',#514,#513);

+#516=DIRECTION('',(0.E0,1.E0,0.E0));

+#517=VECTOR('',#516,4.E-2);

+#518=CARTESIAN_POINT('',(2.5E-2,-1.E-1,2.25E-1));

+#519=LINE('',#518,#517);

+#520=DIRECTION('',(-1.E0,0.E0,0.E0));

+#521=VECTOR('',#520,5.E-2);

+#522=CARTESIAN_POINT('',(2.5E-2,-6.E-2,2.25E-1));

+#523=LINE('',#522,#521);

+#524=DIRECTION('',(0.E0,-1.E0,0.E0));

+#525=VECTOR('',#524,4.E-2);

+#526=CARTESIAN_POINT('',(-2.5E-2,-6.E-2,2.25E-1));

+#527=LINE('',#526,#525);

+#528=DIRECTION('',(0.E0,1.E0,0.E0));

+#529=VECTOR('',#528,1.9E-1);

+#530=CARTESIAN_POINT('',(1.25E-1,-5.5E-2,2.25E-1));

+#531=LINE('',#530,#529);

+#532=DIRECTION('',(-7.071067811865E-1,0.E0,-7.071067811865E-1));

+#533=VECTOR('',#532,2.828427124746E-2);

+#534=CARTESIAN_POINT('',(1.25E-1,-5.5E-2,2.25E-1));

+#535=LINE('',#534,#533);

+#536=DIRECTION('',(0.E0,1.E0,0.E0));

+#537=VECTOR('',#536,1.7E-1);

+#538=CARTESIAN_POINT('',(1.05E-1,-5.5E-2,2.05E-1));

+#539=LINE('',#538,#537);

+#540=DIRECTION('',(-5.773502691896E-1,-5.773502691896E-1,-5.773502691896E-1));

+#541=VECTOR('',#540,3.464101615138E-2);

+#542=CARTESIAN_POINT('',(1.25E-1,1.35E-1,2.25E-1));

+#543=LINE('',#542,#541);

+#544=DIRECTION('',(0.E0,0.E0,-1.E0));

+#545=VECTOR('',#544,1.8E-1);

+#546=CARTESIAN_POINT('',(-1.05E-1,6.5E-2,5.5E-2));

+#547=LINE('',#546,#545);

+#548=DIRECTION('',(0.E0,0.E0,1.E0));

+#549=VECTOR('',#548,3.3E-1);

+#550=CARTESIAN_POINT('',(-1.05E-1,-5.5E-2,-1.25E-1));

+#551=LINE('',#550,#549);

+#552=DIRECTION('',(0.E0,0.E0,-1.E0));

+#553=VECTOR('',#552,1.3E-1);

+#554=CARTESIAN_POINT('',(-1.05E-1,1.15E-1,2.05E-1));

+#555=LINE('',#554,#553);

+#556=DIRECTION('',(0.E0,-1.E0,0.E0));

+#557=VECTOR('',#556,1.5E-2);

+#558=CARTESIAN_POINT('',(-6.E-2,1.E-1,7.5E-2));

+#559=LINE('',#558,#557);

+#560=DIRECTION('',(0.E0,-1.E0,0.E0));

+#561=VECTOR('',#560,1.5E-2);

+#562=CARTESIAN_POINT('',(-1.05E-1,1.E-1,7.5E-2));

+#563=LINE('',#562,#561);

+#564=DIRECTION('',(0.E0,-1.E0,0.E0));

+#565=VECTOR('',#564,1.5E-2);

+#566=CARTESIAN_POINT('',(6.E-2,1.E-1,7.5E-2));

+#567=LINE('',#566,#565);

+#568=DIRECTION('',(0.E0,1.E0,0.E0));

+#569=VECTOR('',#568,1.5E-2);

+#570=CARTESIAN_POINT('',(1.05E-1,8.5E-2,7.5E-2));

+#571=LINE('',#570,#569);

+#572=DIRECTION('',(1.E0,0.E0,0.E0));

+#573=VECTOR('',#572,1.2E-1);

+#574=CARTESIAN_POINT('',(-6.E-2,1.E-1,-1.25E-1));

+#575=LINE('',#574,#573);

+#576=DIRECTION('',(0.E0,0.E0,-1.E0));

+#577=VECTOR('',#576,2.E-1);

+#578=CARTESIAN_POINT('',(-6.E-2,1.E-1,7.5E-2));

+#579=LINE('',#578,#577);

+#580=DIRECTION('',(1.E0,0.E0,0.E0));

+#581=VECTOR('',#580,4.5E-2);

+#582=CARTESIAN_POINT('',(-1.05E-1,1.E-1,7.5E-2));

+#583=LINE('',#582,#581);

+#584=DIRECTION('',(-1.E0,0.E0,0.E0));

+#585=VECTOR('',#584,3.5E-2);

+#586=CARTESIAN_POINT('',(-1.05E-1,1.E-1,7.5E-2));

+#587=LINE('',#586,#585);

+#588=DIRECTION('',(-1.E0,0.E0,0.E0));

+#589=VECTOR('',#588,3.5E-2);

+#590=CARTESIAN_POINT('',(1.4E-1,1.E-1,7.5E-2));

+#591=LINE('',#590,#589);

+#592=DIRECTION('',(1.E0,0.E0,0.E0));

+#593=VECTOR('',#592,4.5E-2);

+#594=CARTESIAN_POINT('',(6.E-2,1.E-1,7.5E-2));

+#595=LINE('',#594,#593);

+#596=DIRECTION('',(0.E0,0.E0,-1.E0));

+#597=VECTOR('',#596,2.E-1);

+#598=CARTESIAN_POINT('',(6.E-2,1.E-1,7.5E-2));

+#599=LINE('',#598,#597);

+#600=DIRECTION('',(0.E0,1.E0,0.E0));

+#601=VECTOR('',#600,3.5E-2);

+#602=CARTESIAN_POINT('',(6.E-2,6.5E-2,-1.25E-1));

+#603=LINE('',#602,#601);

+#604=DIRECTION('',(-1.E0,0.E0,0.E0));

+#605=VECTOR('',#604,4.5E-2);

+#606=CARTESIAN_POINT('',(1.05E-1,6.5E-2,-1.25E-1));

+#607=LINE('',#606,#605);

+#608=DIRECTION('',(0.E0,1.E0,0.E0));

+#609=VECTOR('',#608,1.2E-1);

+#610=CARTESIAN_POINT('',(1.05E-1,-5.5E-2,-1.25E-1));

+#611=LINE('',#610,#609);

+#612=DIRECTION('',(1.E0,0.E0,0.E0));

+#613=VECTOR('',#612,2.1E-1);

+#614=CARTESIAN_POINT('',(-1.05E-1,-5.5E-2,-1.25E-1));

+#615=LINE('',#614,#613);

+#616=DIRECTION('',(0.E0,-1.E0,0.E0));

+#617=VECTOR('',#616,1.2E-1);

+#618=CARTESIAN_POINT('',(-1.05E-1,6.5E-2,-1.25E-1));

+#619=LINE('',#618,#617);

+#620=DIRECTION('',(-1.E0,0.E0,0.E0));

+#621=VECTOR('',#620,4.5E-2);

+#622=CARTESIAN_POINT('',(-6.E-2,6.5E-2,-1.25E-1));

+#623=LINE('',#622,#621);

+#624=DIRECTION('',(0.E0,-1.E0,0.E0));

+#625=VECTOR('',#624,3.5E-2);

+#626=CARTESIAN_POINT('',(-6.E-2,1.E-1,-1.25E-1));

+#627=LINE('',#626,#625);

+#628=DIRECTION('',(0.E0,1.E0,0.E0));

+#629=VECTOR('',#628,2.5E-2);

+#630=CARTESIAN_POINT('',(3.75E-2,-1.25E-2,-1.25E-1));

+#631=LINE('',#630,#629);

+#632=DIRECTION('',(1.E0,0.E0,0.E0));

+#633=VECTOR('',#632,2.5E-2);

+#634=CARTESIAN_POINT('',(3.75E-2,-1.25E-2,-1.25E-1));

+#635=LINE('',#634,#633);

+#636=DIRECTION('',(0.E0,1.E0,0.E0));

+#637=VECTOR('',#636,2.5E-2);

+#638=CARTESIAN_POINT('',(6.25E-2,-1.25E-2,-1.25E-1));

+#639=LINE('',#638,#637);

+#640=DIRECTION('',(1.E0,0.E0,0.E0));

+#641=VECTOR('',#640,2.5E-2);

+#642=CARTESIAN_POINT('',(3.75E-2,1.25E-2,-1.25E-1));

+#643=LINE('',#642,#641);

+#644=DIRECTION('',(0.E0,1.E0,0.E0));

+#645=VECTOR('',#644,2.5E-2);

+#646=CARTESIAN_POINT('',(-6.25E-2,-1.25E-2,-1.25E-1));

+#647=LINE('',#646,#645);

+#648=DIRECTION('',(1.E0,0.E0,0.E0));

+#649=VECTOR('',#648,2.5E-2);

+#650=CARTESIAN_POINT('',(-6.25E-2,-1.25E-2,-1.25E-1));

+#651=LINE('',#650,#649);

+#652=DIRECTION('',(0.E0,1.E0,0.E0));

+#653=VECTOR('',#652,2.5E-2);

+#654=CARTESIAN_POINT('',(-3.75E-2,-1.25E-2,-1.25E-1));

+#655=LINE('',#654,#653);

+#656=DIRECTION('',(1.E0,0.E0,0.E0));

+#657=VECTOR('',#656,2.5E-2);

+#658=CARTESIAN_POINT('',(-6.25E-2,1.25E-2,-1.25E-1));

+#659=LINE('',#658,#657);

+#660=DIRECTION('',(0.E0,0.E0,-1.E0));

+#661=VECTOR('',#660,1.8E-1);

+#662=CARTESIAN_POINT('',(6.E-2,6.5E-2,5.5E-2));

+#663=LINE('',#662,#661);

+#664=DIRECTION('',(1.E0,0.E0,0.E0));

+#665=VECTOR('',#664,4.5E-2);

+#666=CARTESIAN_POINT('',(6.E-2,8.5E-2,7.5E-2));

+#667=LINE('',#666,#665);

+#668=DIRECTION('',(0.E0,-7.071067811865E-1,-7.071067811865E-1));

+#669=VECTOR('',#668,2.828427124746E-2);

+#670=CARTESIAN_POINT('',(1.05E-1,8.5E-2,7.5E-2));

+#671=LINE('',#670,#669);

+#672=DIRECTION('',(1.E0,0.E0,0.E0));

+#673=VECTOR('',#672,4.5E-2);

+#674=CARTESIAN_POINT('',(6.E-2,6.5E-2,5.5E-2));

+#675=LINE('',#674,#673);

+#676=DIRECTION('',(0.E0,-7.071067811865E-1,-7.071067811865E-1));

+#677=VECTOR('',#676,2.828427124746E-2);

+#678=CARTESIAN_POINT('',(6.E-2,8.5E-2,7.5E-2));

+#679=LINE('',#678,#677);

+#680=DIRECTION('',(0.E0,0.E0,-1.E0));

+#681=VECTOR('',#680,3.3E-1);

+#682=CARTESIAN_POINT('',(1.05E-1,-5.5E-2,2.05E-1));

+#683=LINE('',#682,#681);

+#684=DIRECTION('',(0.E0,0.E0,-1.E0));

+#685=VECTOR('',#684,1.8E-1);

+#686=CARTESIAN_POINT('',(1.05E-1,6.5E-2,5.5E-2));

+#687=LINE('',#686,#685);

+#688=DIRECTION('',(0.E0,1.E0,0.E0));

+#689=VECTOR('',#688,4.5E-2);

+#690=CARTESIAN_POINT('',(1.E-1,1.15E-1,7.5E-2));

+#691=LINE('',#690,#689);

+#692=DIRECTION('',(-1.E0,0.E0,0.E0));

+#693=VECTOR('',#692,5.E-3);

+#694=CARTESIAN_POINT('',(1.05E-1,1.15E-1,7.5E-2));

+#695=LINE('',#694,#693);

+#696=DIRECTION('',(0.E0,1.E0,0.E0));

+#697=VECTOR('',#696,1.5E-2);

+#698=CARTESIAN_POINT('',(1.05E-1,1.E-1,7.5E-2));

+#699=LINE('',#698,#697);

+#700=DIRECTION('',(0.E0,1.E0,0.E0));

+#701=VECTOR('',#700,4.5E-2);

+#702=CARTESIAN_POINT('',(-1.E-1,1.15E-1,7.5E-2));

+#703=LINE('',#702,#701);

+#704=DIRECTION('',(0.E0,-1.E0,0.E0));

+#705=VECTOR('',#704,1.5E-2);

+#706=CARTESIAN_POINT('',(-1.05E-1,1.15E-1,7.5E-2));

+#707=LINE('',#706,#705);

+#708=DIRECTION('',(-1.E0,0.E0,0.E0));

+#709=VECTOR('',#708,5.E-3);

+#710=CARTESIAN_POINT('',(-1.E-1,1.15E-1,7.5E-2));

+#711=LINE('',#710,#709);

+#712=DIRECTION('',(0.E0,0.E0,1.E0));

+#713=VECTOR('',#712,3.7E-2);

+#714=CARTESIAN_POINT('',(1.E-1,1.6E-1,7.5E-2));

+#715=LINE('',#714,#713);

+#716=DIRECTION('',(-1.E0,0.E0,0.E0));

+#717=VECTOR('',#716,4.E-2);

+#718=CARTESIAN_POINT('',(1.4E-1,1.6E-1,7.5E-2));

+#719=LINE('',#718,#717);

+#720=DIRECTION('',(0.E0,0.E0,1.E0));

+#721=VECTOR('',#720,1.5E-1);

+#722=CARTESIAN_POINT('',(1.4E-1,1.6E-1,7.5E-2));

+#723=LINE('',#722,#721);

+#724=DIRECTION('',(-1.E0,0.E0,0.E0));

+#725=VECTOR('',#724,4.E-2);

+#726=CARTESIAN_POINT('',(-1.E-1,1.6E-1,7.5E-2));

+#727=LINE('',#726,#725);

+#728=DIRECTION('',(0.E0,0.E0,1.E0));

+#729=VECTOR('',#728,3.7E-2);

+#730=CARTESIAN_POINT('',(-1.E-1,1.6E-1,7.5E-2));

+#731=LINE('',#730,#729);

+#732=CARTESIAN_POINT('',(-4.7E-2,1.6E-1,1.12E-1));

+#733=DIRECTION('',(0.E0,-1.E0,0.E0));

+#734=DIRECTION('',(0.E0,0.E0,1.E0));

+#735=AXIS2_PLACEMENT_3D('',#732,#733,#734);

+#737=DIRECTION('',(-1.E0,0.E0,0.E0));

+#738=VECTOR('',#737,9.4E-2);

+#739=CARTESIAN_POINT('',(4.7E-2,1.6E-1,1.65E-1));

+#740=LINE('',#739,#738);

+#741=CARTESIAN_POINT('',(4.7E-2,1.6E-1,1.12E-1));

+#742=DIRECTION('',(0.E0,-1.E0,0.E0));

+#743=DIRECTION('',(1.E0,0.E0,0.E0));

+#744=AXIS2_PLACEMENT_3D('',#741,#742,#743);

+#746=DIRECTION('',(0.E0,1.E0,0.E0));

+#747=VECTOR('',#746,6.E-2);

+#748=CARTESIAN_POINT('',(1.4E-1,1.E-1,7.5E-2));

+#749=LINE('',#748,#747);

+#750=DIRECTION('',(0.E0,0.E0,-1.E0));

+#751=VECTOR('',#750,3.E-1);

+#752=CARTESIAN_POINT('',(1.4E-1,1.E-1,7.5E-2));

+#753=LINE('',#752,#751);

+#754=DIRECTION('',(0.E0,0.E0,1.E0));

+#755=VECTOR('',#754,4.5E-1);

+#756=CARTESIAN_POINT('',(1.4E-1,-5.E-2,-2.25E-1));

+#757=LINE('',#756,#755);

+#758=DIRECTION('',(0.E0,0.E0,1.E0));

+#759=VECTOR('',#758,4.5E-1);

+#760=CARTESIAN_POINT('',(1.65E-1,-5.E-2,-2.25E-1));

+#761=LINE('',#760,#759);

+#762=DIRECTION('',(0.E0,0.E0,1.E0));

+#763=VECTOR('',#762,4.5E-1);

+#764=CARTESIAN_POINT('',(1.65E-1,-1.E-1,-2.25E-1));

+#765=LINE('',#764,#763);

+#766=DIRECTION('',(0.E0,0.E0,1.E0));

+#767=VECTOR('',#766,1.5E-1);

+#768=CARTESIAN_POINT('',(-1.4E-1,1.6E-1,7.5E-2));

+#769=LINE('',#768,#767);

+#770=DIRECTION('',(0.E0,0.E0,-1.E0));

+#771=VECTOR('',#770,3.E-1);

+#772=CARTESIAN_POINT('',(-1.4E-1,1.E-1,7.5E-2));

+#773=LINE('',#772,#771);

+#774=DIRECTION('',(0.E0,1.E0,0.E0));

+#775=VECTOR('',#774,6.E-2);

+#776=CARTESIAN_POINT('',(-1.4E-1,1.E-1,7.5E-2));

+#777=LINE('',#776,#775);

+#778=DIRECTION('',(0.E0,0.E0,1.E0));

+#779=VECTOR('',#778,4.5E-1);

+#780=CARTESIAN_POINT('',(-1.4E-1,-5.E-2,-2.25E-1));

+#781=LINE('',#780,#779);

+#782=DIRECTION('',(0.E0,0.E0,1.E0));

+#783=VECTOR('',#782,4.5E-1);

+#784=CARTESIAN_POINT('',(-1.65E-1,-5.E-2,-2.25E-1));

+#785=LINE('',#784,#783);

+#786=DIRECTION('',(0.E0,1.E0,0.E0));

+#787=VECTOR('',#786,4.5E-2);

+#788=CARTESIAN_POINT('',(-1.E-1,1.15E-1,1.12E-1));

+#789=LINE('',#788,#787);

+#790=DIRECTION('',(0.E0,0.E0,1.E0));

+#791=VECTOR('',#790,3.7E-2);

+#792=CARTESIAN_POINT('',(1.E-1,1.15E-1,7.5E-2));

+#793=LINE('',#792,#791);

+#794=CARTESIAN_POINT('',(4.7E-2,1.15E-1,1.12E-1));

+#795=DIRECTION('',(0.E0,-1.E0,0.E0));

+#796=DIRECTION('',(1.E0,0.E0,0.E0));

+#797=AXIS2_PLACEMENT_3D('',#794,#795,#796);

+#799=DIRECTION('',(-1.E0,0.E0,0.E0));

+#800=VECTOR('',#799,9.4E-2);

+#801=CARTESIAN_POINT('',(4.7E-2,1.15E-1,1.65E-1));

+#802=LINE('',#801,#800);

+#803=CARTESIAN_POINT('',(-4.7E-2,1.15E-1,1.12E-1));

+#804=DIRECTION('',(0.E0,-1.E0,0.E0));

+#805=DIRECTION('',(0.E0,0.E0,1.E0));

+#806=AXIS2_PLACEMENT_3D('',#803,#804,#805);

+#808=DIRECTION('',(0.E0,0.E0,1.E0));

+#809=VECTOR('',#808,3.7E-2);

+#810=CARTESIAN_POINT('',(-1.E-1,1.15E-1,7.5E-2));

+#811=LINE('',#810,#809);

+#812=DIRECTION('',(0.E0,0.E0,-1.E0));

+#813=VECTOR('',#812,1.3E-1);

+#814=CARTESIAN_POINT('',(1.05E-1,1.15E-1,2.05E-1));

+#815=LINE('',#814,#813);

+#816=DIRECTION('',(0.E0,1.E0,0.E0));

+#817=VECTOR('',#816,4.5E-2);

+#818=CARTESIAN_POINT('',(1.E-1,1.15E-1,1.12E-1));

+#819=LINE('',#818,#817);

+#820=DIRECTION('',(0.E0,1.E0,0.E0));

+#821=VECTOR('',#820,4.5E-2);

+#822=CARTESIAN_POINT('',(4.7E-2,1.15E-1,1.65E-1));

+#823=LINE('',#822,#821);

+#824=DIRECTION('',(0.E0,1.E0,0.E0));

+#825=VECTOR('',#824,4.5E-2);

+#826=CARTESIAN_POINT('',(-4.7E-2,1.15E-1,1.65E-1));

+#827=LINE('',#826,#825);

+#828=DIRECTION('',(-1.E0,0.E0,0.E0));

+#829=VECTOR('',#828,2.5E-1);

+#830=CARTESIAN_POINT('',(1.25E-1,1.35E-1,2.25E-1));

+#831=LINE('',#830,#829);

+#832=DIRECTION('',(-1.E0,0.E0,0.E0));

+#833=VECTOR('',#832,2.1E-1);

+#834=CARTESIAN_POINT('',(1.05E-1,1.15E-1,2.05E-1));

+#835=LINE('',#834,#833);

+#836=DIRECTION('',(5.773502691896E-1,-5.773502691896E-1,-5.773502691896E-1));

+#837=VECTOR('',#836,3.464101615138E-2);

+#838=CARTESIAN_POINT('',(-1.25E-1,1.35E-1,2.25E-1));

+#839=LINE('',#838,#837);

+#840=DIRECTION('',(0.E0,-1.E0,0.E0));

+#841=VECTOR('',#840,1.9E-1);

+#842=CARTESIAN_POINT('',(-1.25E-1,1.35E-1,2.25E-1));

+#843=LINE('',#842,#841);

+#844=DIRECTION('',(0.E0,-1.E0,0.E0));

+#845=VECTOR('',#844,1.7E-1);

+#846=CARTESIAN_POINT('',(-1.05E-1,1.15E-1,2.05E-1));

+#847=LINE('',#846,#845);

+#848=DIRECTION('',(7.071067811865E-1,0.E0,-7.071067811865E-1));

+#849=VECTOR('',#848,2.828427124746E-2);

+#850=CARTESIAN_POINT('',(-1.25E-1,-5.5E-2,2.25E-1));

+#851=LINE('',#850,#849);

+#852=DIRECTION('',(0.E0,0.E0,-1.E0));

+#853=VECTOR('',#852,1.8E-1);

+#854=CARTESIAN_POINT('',(-6.E-2,6.5E-2,5.5E-2));

+#855=LINE('',#854,#853);

+#856=DIRECTION('',(-1.E0,0.E0,0.E0));

+#857=VECTOR('',#856,4.5E-2);

+#858=CARTESIAN_POINT('',(-6.E-2,8.5E-2,7.5E-2));

+#859=LINE('',#858,#857);

+#860=DIRECTION('',(0.E0,-7.071067811865E-1,-7.071067811865E-1));

+#861=VECTOR('',#860,2.828427124746E-2);

+#862=CARTESIAN_POINT('',(-6.E-2,8.5E-2,7.5E-2));

+#863=LINE('',#862,#861);

+#864=DIRECTION('',(-1.E0,0.E0,0.E0));

+#865=VECTOR('',#864,4.5E-2);

+#866=CARTESIAN_POINT('',(-6.E-2,6.5E-2,5.5E-2));

+#867=LINE('',#866,#865);

+#868=DIRECTION('',(0.E0,-7.071067811865E-1,-7.071067811865E-1));

+#869=VECTOR('',#868,2.828427124746E-2);

+#870=CARTESIAN_POINT('',(-1.05E-1,8.5E-2,7.5E-2));

+#871=LINE('',#870,#869);

+#872=DIRECTION('',(0.E0,0.E0,-1.E0));

+#873=VECTOR('',#872,2.4E-1);

+#874=CARTESIAN_POINT('',(3.75E-2,-1.25E-2,1.15E-1));

+#875=LINE('',#874,#873);

+#876=DIRECTION('',(0.E0,0.E0,1.E0));

+#877=VECTOR('',#876,2.4E-1);

+#878=CARTESIAN_POINT('',(3.75E-2,1.25E-2,-1.25E-1));

+#879=LINE('',#878,#877);

+#880=DIRECTION('',(0.E0,-1.E0,0.E0));

+#881=VECTOR('',#880,2.5E-2);

+#882=CARTESIAN_POINT('',(3.75E-2,1.25E-2,1.15E-1));

+#883=LINE('',#882,#881);

+#884=DIRECTION('',(0.E0,-1.E0,0.E0));

+#885=VECTOR('',#884,1.85E-1);

+#886=CARTESIAN_POINT('',(3.75E-2,-6.E-2,-2.7E-1));

+#887=LINE('',#886,#885);

+#888=DIRECTION('',(0.E0,0.E0,-1.E0));

+#889=VECTOR('',#888,2.5E-2);

+#890=CARTESIAN_POINT('',(3.75E-2,-2.45E-1,-2.7E-1));

+#891=LINE('',#890,#889);

+#892=DIRECTION('',(0.E0,1.E0,0.E0));

+#893=VECTOR('',#892,1.85E-1);

+#894=CARTESIAN_POINT('',(3.75E-2,-2.45E-1,-2.95E-1));

+#895=LINE('',#894,#893);

+#896=DIRECTION('',(1.E0,0.E0,0.E0));

+#897=VECTOR('',#896,2.5E-2);

+#898=CARTESIAN_POINT('',(3.75E-2,-1.25E-2,1.15E-1));

+#899=LINE('',#898,#897);

+#900=DIRECTION('',(1.E0,0.E0,0.E0));

+#901=VECTOR('',#900,2.5E-2);

+#902=CARTESIAN_POINT('',(3.75E-2,1.25E-2,1.15E-1));

+#903=LINE('',#902,#901);

+#904=DIRECTION('',(0.E0,0.E0,-1.E0));

+#905=VECTOR('',#904,2.4E-1);

+#906=CARTESIAN_POINT('',(6.25E-2,-1.25E-2,1.15E-1));

+#907=LINE('',#906,#905);

+#908=DIRECTION('',(0.E0,-1.E0,0.E0));

+#909=VECTOR('',#908,2.5E-2);

+#910=CARTESIAN_POINT('',(6.25E-2,1.25E-2,1.15E-1));

+#911=LINE('',#910,#909);

+#912=DIRECTION('',(0.E0,0.E0,1.E0));

+#913=VECTOR('',#912,2.4E-1);

+#914=CARTESIAN_POINT('',(6.25E-2,1.25E-2,-1.25E-1));

+#915=LINE('',#914,#913);

+#916=DIRECTION('',(0.E0,-1.E0,0.E0));

+#917=VECTOR('',#916,1.85E-1);

+#918=CARTESIAN_POINT('',(6.25E-2,-6.E-2,-2.7E-1));

+#919=LINE('',#918,#917);

+#920=DIRECTION('',(0.E0,1.E0,0.E0));

+#921=VECTOR('',#920,1.85E-1);

+#922=CARTESIAN_POINT('',(6.25E-2,-2.45E-1,-2.95E-1));

+#923=LINE('',#922,#921);

+#924=DIRECTION('',(0.E0,0.E0,-1.E0));

+#925=VECTOR('',#924,2.5E-2);

+#926=CARTESIAN_POINT('',(6.25E-2,-2.45E-1,-2.7E-1));

+#927=LINE('',#926,#925);

+#928=DIRECTION('',(1.E0,0.E0,0.E0));

+#929=VECTOR('',#928,2.5E-2);

+#930=CARTESIAN_POINT('',(3.75E-2,-3.75E-2,-2.7E-1));

+#931=LINE('',#930,#929);

+#932=CARTESIAN_POINT('',(3.75E-2,-3.75E-2,-2.45E-1));

+#933=DIRECTION('',(1.E0,0.E0,0.E0));

+#934=DIRECTION('',(0.E0,0.E0,-1.E0));

+#935=AXIS2_PLACEMENT_3D('',#932,#933,#934);

+#937=DIRECTION('',(0.E0,1.E0,0.E0));

+#938=VECTOR('',#937,2.25E-2);

+#939=CARTESIAN_POINT('',(3.75E-2,-6.E-2,-2.7E-1));

+#940=LINE('',#939,#938);

+#941=DIRECTION('',(0.E0,1.E0,0.E0));

+#942=VECTOR('',#941,2.25E-2);

+#943=CARTESIAN_POINT('',(3.75E-2,-6.E-2,-2.95E-1));

+#944=LINE('',#943,#942);

+#945=CARTESIAN_POINT('',(3.75E-2,-3.75E-2,-2.45E-1));

+#946=DIRECTION('',(1.E0,0.E0,0.E0));

+#947=DIRECTION('',(0.E0,0.E0,-1.E0));

+#948=AXIS2_PLACEMENT_3D('',#945,#946,#947);

+#950=DIRECTION('',(0.E0,0.E0,-1.E0));

+#951=VECTOR('',#950,4.1E-2);

+#952=CARTESIAN_POINT('',(3.75E-2,3.E-2,-2.69E-1));

+#953=LINE('',#952,#951);

+#954=DIRECTION('',(1.E0,0.E0,0.E0));

+#955=VECTOR('',#954,2.5E-2);

+#956=CARTESIAN_POINT('',(3.75E-2,-3.05E-2,-2.69E-1));

+#957=LINE('',#956,#955);

+#958=DIRECTION('',(0.E0,1.E0,0.E0));

+#959=VECTOR('',#958,2.95E-2);

+#960=CARTESIAN_POINT('',(3.75E-2,-6.E-2,-2.69E-1));

+#961=LINE('',#960,#959);

+#962=DIRECTION('',(1.E0,0.E0,0.E0));

+#963=VECTOR('',#962,2.5E-2);

+#964=CARTESIAN_POINT('',(3.75E-2,-6.E-2,-2.69E-1));

+#965=LINE('',#964,#963);

+#966=DIRECTION('',(0.E0,1.E0,0.E0));

+#967=VECTOR('',#966,2.95E-2);

+#968=CARTESIAN_POINT('',(6.25E-2,-6.E-2,-2.69E-1));

+#969=LINE('',#968,#967);

+#970=DIRECTION('',(1.E0,0.E0,0.E0));

+#971=VECTOR('',#970,2.5E-2);

+#972=CARTESIAN_POINT('',(-6.25E-2,-3.05E-2,-2.69E-1));

+#973=LINE('',#972,#971);

+#974=DIRECTION('',(0.E0,1.E0,0.E0));

+#975=VECTOR('',#974,2.95E-2);

+#976=CARTESIAN_POINT('',(-6.25E-2,-6.E-2,-2.69E-1));

+#977=LINE('',#976,#975);

+#978=DIRECTION('',(1.E0,0.E0,0.E0));

+#979=VECTOR('',#978,2.5E-2);

+#980=CARTESIAN_POINT('',(-6.25E-2,-6.E-2,-2.69E-1));

+#981=LINE('',#980,#979);

+#982=DIRECTION('',(0.E0,1.E0,0.E0));

+#983=VECTOR('',#982,2.95E-2);

+#984=CARTESIAN_POINT('',(-3.75E-2,-6.E-2,-2.69E-1));

+#985=LINE('',#984,#983);

+#986=DIRECTION('',(1.E0,0.E0,0.E0));

+#987=VECTOR('',#986,2.5E-2);

+#988=CARTESIAN_POINT('',(-6.25E-2,6.363424398923E-3,-2.69E-1));

+#989=LINE('',#988,#987);

+#990=DIRECTION('',(0.E0,1.E0,0.E0));

+#991=VECTOR('',#990,2.363657560108E-2);

+#992=CARTESIAN_POINT('',(-3.75E-2,6.363424398923E-3,-2.69E-1));

+#993=LINE('',#992,#991);

+#994=DIRECTION('',(1.E0,0.E0,0.E0));

+#995=VECTOR('',#994,7.5E-2);

+#996=CARTESIAN_POINT('',(-3.75E-2,3.E-2,-2.69E-1));

+#997=LINE('',#996,#995);

+#998=DIRECTION('',(0.E0,1.E0,0.E0));

+#999=VECTOR('',#998,2.363657560108E-2);

+#1000=CARTESIAN_POINT('',(3.75E-2,6.363424398923E-3,-2.69E-1));

+#1001=LINE('',#1000,#999);

+#1002=DIRECTION('',(1.E0,0.E0,0.E0));

+#1003=VECTOR('',#1002,2.5E-2);

+#1004=CARTESIAN_POINT('',(3.75E-2,6.363424398923E-3,-2.69E-1));

+#1005=LINE('',#1004,#1003);

+#1006=DIRECTION('',(0.E0,1.E0,0.E0));

+#1007=VECTOR('',#1006,2.363657560108E-2);

+#1008=CARTESIAN_POINT('',(6.25E-2,6.363424398923E-3,-2.69E-1));

+#1009=LINE('',#1008,#1007);

+#1010=DIRECTION('',(-1.E0,0.E0,0.E0));

+#1011=VECTOR('',#1010,3.75E-2);

+#1012=CARTESIAN_POINT('',(1.E-1,3.E-2,-2.69E-1));

+#1013=LINE('',#1012,#1011);

+#1014=DIRECTION('',(1.E0,0.E0,0.E0));

+#1015=VECTOR('',#1014,3.75E-2);

+#1016=CARTESIAN_POINT('',(-1.E-1,3.E-2,-2.69E-1));

+#1017=LINE('',#1016,#1015);

+#1018=DIRECTION('',(0.E0,1.E0,0.E0));

+#1019=VECTOR('',#1018,2.363657560108E-2);

+#1020=CARTESIAN_POINT('',(-6.25E-2,6.363424398923E-3,-2.69E-1));

+#1021=LINE('',#1020,#1019);

+#1022=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1023=VECTOR('',#1022,2.5E-2);

+#1024=CARTESIAN_POINT('',(-6.25E-2,-6.E-2,-2.7E-1));

+#1025=LINE('',#1024,#1023);

+#1026=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1027=VECTOR('',#1026,1.5E-2);

+#1028=CARTESIAN_POINT('',(-6.25E-2,-6.E-2,-2.95E-1));

+#1029=LINE('',#1028,#1027);

+#1030=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1031=VECTOR('',#1030,8.5E-2);

+#1032=CARTESIAN_POINT('',(1.E-1,-6.E-2,-2.25E-1));

+#1033=LINE('',#1032,#1031);

+#1034=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1035=VECTOR('',#1034,2.5E-2);

+#1036=CARTESIAN_POINT('',(6.25E-2,-6.E-2,-2.7E-1));

+#1037=LINE('',#1036,#1035);

+#1038=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1039=VECTOR('',#1038,9.999999999999E-4);

+#1040=CARTESIAN_POINT('',(3.75E-2,-6.E-2,-2.69E-1));

+#1041=LINE('',#1040,#1039);

+#1042=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1043=VECTOR('',#1042,2.5E-2);

+#1044=CARTESIAN_POINT('',(3.75E-2,-6.E-2,-2.7E-1));

+#1045=LINE('',#1044,#1043);

+#1046=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1047=VECTOR('',#1046,1.5E-2);

+#1048=CARTESIAN_POINT('',(3.75E-2,-6.E-2,-2.95E-1));

+#1049=LINE('',#1048,#1047);

+#1050=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1051=VECTOR('',#1050,2.5E-2);

+#1052=CARTESIAN_POINT('',(-3.75E-2,-6.E-2,-2.7E-1));

+#1053=LINE('',#1052,#1051);

+#1054=DIRECTION('',(-1.387778780782E-14,0.E0,-1.E0));

+#1055=VECTOR('',#1054,9.999999999999E-4);

+#1056=CARTESIAN_POINT('',(-6.25E-2,-6.E-2,-2.69E-1));

+#1057=LINE('',#1056,#1055);

+#1058=DIRECTION('',(0.E0,0.E0,1.E0));

+#1059=VECTOR('',#1058,2.4E-1);

+#1060=CARTESIAN_POINT('',(-6.25E-2,1.25E-2,-1.25E-1));

+#1061=LINE('',#1060,#1059);

+#1062=DIRECTION('',(0.E0,-1.E0,0.E0));

+#1063=VECTOR('',#1062,2.5E-2);

+#1064=CARTESIAN_POINT('',(-6.25E-2,1.25E-2,1.15E-1));

+#1065=LINE('',#1064,#1063);

+#1066=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1067=VECTOR('',#1066,2.4E-1);

+#1068=CARTESIAN_POINT('',(-6.25E-2,-1.25E-2,1.15E-1));

+#1069=LINE('',#1068,#1067);

+#1070=DIRECTION('',(0.E0,-1.E0,0.E0));

+#1071=VECTOR('',#1070,1.85E-1);

+#1072=CARTESIAN_POINT('',(-6.25E-2,-6.E-2,-2.7E-1));

+#1073=LINE('',#1072,#1071);

+#1074=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1075=VECTOR('',#1074,2.5E-2);

+#1076=CARTESIAN_POINT('',(-6.25E-2,-2.45E-1,-2.7E-1));

+#1077=LINE('',#1076,#1075);

+#1078=DIRECTION('',(0.E0,1.E0,0.E0));

+#1079=VECTOR('',#1078,1.85E-1);

+#1080=CARTESIAN_POINT('',(-6.25E-2,-2.45E-1,-2.95E-1));

+#1081=LINE('',#1080,#1079);

+#1082=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1083=VECTOR('',#1082,2.4E-1);

+#1084=CARTESIAN_POINT('',(-3.75E-2,-1.25E-2,1.15E-1));

+#1085=LINE('',#1084,#1083);

+#1086=DIRECTION('',(0.E0,-1.E0,0.E0));

+#1087=VECTOR('',#1086,2.5E-2);

+#1088=CARTESIAN_POINT('',(-3.75E-2,1.25E-2,1.15E-1));

+#1089=LINE('',#1088,#1087);

+#1090=DIRECTION('',(0.E0,0.E0,1.E0));

+#1091=VECTOR('',#1090,2.4E-1);

+#1092=CARTESIAN_POINT('',(-3.75E-2,1.25E-2,-1.25E-1));

+#1093=LINE('',#1092,#1091);

+#1094=DIRECTION('',(0.E0,1.E0,0.E0));

+#1095=VECTOR('',#1094,1.85E-1);

+#1096=CARTESIAN_POINT('',(-3.75E-2,-2.45E-1,-2.95E-1));

+#1097=LINE('',#1096,#1095);

+#1098=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1099=VECTOR('',#1098,2.5E-2);

+#1100=CARTESIAN_POINT('',(-3.75E-2,-2.45E-1,-2.7E-1));

+#1101=LINE('',#1100,#1099);

+#1102=DIRECTION('',(0.E0,-1.E0,0.E0));

+#1103=VECTOR('',#1102,1.85E-1);

+#1104=CARTESIAN_POINT('',(-3.75E-2,-6.E-2,-2.7E-1));

+#1105=LINE('',#1104,#1103);

+#1106=DIRECTION('',(1.E0,0.E0,0.E0));

+#1107=VECTOR('',#1106,2.5E-2);

+#1108=CARTESIAN_POINT('',(-6.25E-2,-1.25E-2,1.15E-1));

+#1109=LINE('',#1108,#1107);

+#1110=DIRECTION('',(1.E0,0.E0,0.E0));

+#1111=VECTOR('',#1110,2.5E-2);

+#1112=CARTESIAN_POINT('',(-6.25E-2,1.25E-2,1.15E-1));

+#1113=LINE('',#1112,#1111);

+#1114=DIRECTION('',(1.E0,0.E0,0.E0));

+#1115=VECTOR('',#1114,2.5E-2);

+#1116=CARTESIAN_POINT('',(-6.25E-2,-2.45E-1,-2.95E-1));

+#1117=LINE('',#1116,#1115);

+#1118=CARTESIAN_POINT('',(-6.25E-2,-3.75E-2,-2.45E-1));

+#1119=DIRECTION('',(1.E0,0.E0,0.E0));

+#1120=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1121=AXIS2_PLACEMENT_3D('',#1118,#1119,#1120);

+#1123=DIRECTION('',(0.E0,1.E0,0.E0));

+#1124=VECTOR('',#1123,2.25E-2);

+#1125=CARTESIAN_POINT('',(-6.25E-2,-6.E-2,-2.7E-1));

+#1126=LINE('',#1125,#1124);

+#1127=DIRECTION('',(0.E0,1.E0,0.E0));

+#1128=VECTOR('',#1127,2.25E-2);

+#1129=CARTESIAN_POINT('',(-6.25E-2,-6.E-2,-2.95E-1));

+#1130=LINE('',#1129,#1128);

+#1131=CARTESIAN_POINT('',(-6.25E-2,-3.75E-2,-2.45E-1));

+#1132=DIRECTION('',(1.E0,0.E0,0.E0));

+#1133=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1134=AXIS2_PLACEMENT_3D('',#1131,#1132,#1133);

+#1136=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1137=VECTOR('',#1136,4.1E-2);

+#1138=CARTESIAN_POINT('',(-6.25E-2,3.E-2,-2.69E-1));

+#1139=LINE('',#1138,#1137);

+#1140=CARTESIAN_POINT('',(-3.75E-2,-3.75E-2,-2.45E-1));

+#1141=DIRECTION('',(1.E0,0.E0,0.E0));

+#1142=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1143=AXIS2_PLACEMENT_3D('',#1140,#1141,#1142);

+#1145=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1146=VECTOR('',#1145,9.999999999999E-4);

+#1147=CARTESIAN_POINT('',(-3.75E-2,-6.E-2,-2.69E-1));

+#1148=LINE('',#1147,#1146);

+#1149=DIRECTION('',(0.E0,1.E0,0.E0));

+#1150=VECTOR('',#1149,2.25E-2);

+#1151=CARTESIAN_POINT('',(-3.75E-2,-6.E-2,-2.7E-1));

+#1152=LINE('',#1151,#1150);

+#1153=DIRECTION('',(0.E0,1.E0,0.E0));

+#1154=VECTOR('',#1153,2.25E-2);

+#1155=CARTESIAN_POINT('',(-3.75E-2,-6.E-2,-2.95E-1));

+#1156=LINE('',#1155,#1154);

+#1157=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1158=VECTOR('',#1157,1.5E-2);

+#1159=CARTESIAN_POINT('',(-3.75E-2,-6.E-2,-2.95E-1));

+#1160=LINE('',#1159,#1158);

+#1161=CARTESIAN_POINT('',(-3.75E-2,-3.75E-2,-2.45E-1));

+#1162=DIRECTION('',(1.E0,0.E0,0.E0));

+#1163=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1164=AXIS2_PLACEMENT_3D('',#1161,#1162,#1163);

+#1166=DIRECTION('',(1.E0,0.E0,0.E0));

+#1167=VECTOR('',#1166,2.5E-2);

+#1168=CARTESIAN_POINT('',(-6.25E-2,-3.75E-2,-2.7E-1));

+#1169=LINE('',#1168,#1167);

+#1170=DIRECTION('',(1.E0,0.E0,0.E0));

+#1171=VECTOR('',#1170,2.5E-2);

+#1172=CARTESIAN_POINT('',(-6.25E-2,-2.45E-1,-2.7E-1));

+#1173=LINE('',#1172,#1171);

+#1174=DIRECTION('',(0.E0,-1.E0,0.E0));

+#1175=VECTOR('',#1174,9.E-2);

+#1176=CARTESIAN_POINT('',(1.E-1,3.E-2,-3.1E-1));

+#1177=LINE('',#1176,#1175);

+#1178=DIRECTION('',(1.E0,0.E0,0.E0));

+#1179=VECTOR('',#1178,3.75E-2);

+#1180=CARTESIAN_POINT('',(6.25E-2,3.E-2,-3.1E-1));

+#1181=LINE('',#1180,#1179);

+#1182=DIRECTION('',(0.E0,1.E0,0.E0));

+#1183=VECTOR('',#1182,9.E-2);

+#1184=CARTESIAN_POINT('',(6.25E-2,-6.E-2,-3.1E-1));

+#1185=LINE('',#1184,#1183);

+#1186=DIRECTION('',(-1.E0,0.E0,0.E0));

+#1187=VECTOR('',#1186,3.75E-2);

+#1188=CARTESIAN_POINT('',(1.E-1,-6.E-2,-3.1E-1));

+#1189=LINE('',#1188,#1187);

+#1190=DIRECTION('',(1.E0,0.E0,0.E0));

+#1191=VECTOR('',#1190,3.75E-2);

+#1192=CARTESIAN_POINT('',(-1.E-1,3.E-2,-3.1E-1));

+#1193=LINE('',#1192,#1191);

+#1194=DIRECTION('',(0.E0,1.E0,0.E0));

+#1195=VECTOR('',#1194,9.E-2);

+#1196=CARTESIAN_POINT('',(-1.E-1,-6.E-2,-3.1E-1));

+#1197=LINE('',#1196,#1195);

+#1198=DIRECTION('',(-1.E0,0.E0,0.E0));

+#1199=VECTOR('',#1198,3.75E-2);

+#1200=CARTESIAN_POINT('',(-6.25E-2,-6.E-2,-3.1E-1));

+#1201=LINE('',#1200,#1199);

+#1202=DIRECTION('',(0.E0,-1.E0,0.E0));

+#1203=VECTOR('',#1202,9.E-2);

+#1204=CARTESIAN_POINT('',(-6.25E-2,3.E-2,-3.1E-1));

+#1205=LINE('',#1204,#1203);

+#1206=DIRECTION('',(0.E0,-1.E0,0.E0));

+#1207=VECTOR('',#1206,9.E-2);

+#1208=CARTESIAN_POINT('',(3.75E-2,3.E-2,-3.1E-1));

+#1209=LINE('',#1208,#1207);

+#1210=DIRECTION('',(1.E0,0.E0,0.E0));

+#1211=VECTOR('',#1210,7.5E-2);

+#1212=CARTESIAN_POINT('',(-3.75E-2,3.E-2,-3.1E-1));

+#1213=LINE('',#1212,#1211);

+#1214=DIRECTION('',(0.E0,1.E0,0.E0));

+#1215=VECTOR('',#1214,9.E-2);

+#1216=CARTESIAN_POINT('',(-3.75E-2,-6.E-2,-3.1E-1));

+#1217=LINE('',#1216,#1215);

+#1218=DIRECTION('',(-1.E0,0.E0,0.E0));

+#1219=VECTOR('',#1218,7.5E-2);

+#1220=CARTESIAN_POINT('',(3.75E-2,-6.E-2,-3.1E-1));

+#1221=LINE('',#1220,#1219);

+#1222=DIRECTION('',(0.E0,1.E0,0.E0));

+#1223=VECTOR('',#1222,3.E-2);

+#1224=CARTESIAN_POINT('',(1.E-1,3.E-2,-2.69E-1));

+#1225=LINE('',#1224,#1223);

+#1226=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1227=VECTOR('',#1226,4.1E-2);

+#1228=CARTESIAN_POINT('',(1.E-1,3.E-2,-2.69E-1));

+#1229=LINE('',#1228,#1227);

+#1230=DIRECTION('',(0.E0,0.E0,1.E0));

+#1231=VECTOR('',#1230,4.4E-2);

+#1232=CARTESIAN_POINT('',(1.E-1,6.E-2,-2.69E-1));

+#1233=LINE('',#1232,#1231);

+#1234=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1235=VECTOR('',#1234,4.1E-2);

+#1236=CARTESIAN_POINT('',(6.25E-2,3.E-2,-2.69E-1));

+#1237=LINE('',#1236,#1235);

+#1238=CARTESIAN_POINT('',(6.25E-2,-3.75E-2,-2.45E-1));

+#1239=DIRECTION('',(1.E0,0.E0,0.E0));

+#1240=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1241=AXIS2_PLACEMENT_3D('',#1238,#1239,#1240);

+#1243=DIRECTION('',(1.387778780782E-14,0.E0,-1.E0));

+#1244=VECTOR('',#1243,9.999999999999E-4);

+#1245=CARTESIAN_POINT('',(6.25E-2,-6.E-2,-2.69E-1));

+#1246=LINE('',#1245,#1244);

+#1247=DIRECTION('',(0.E0,1.E0,0.E0));

+#1248=VECTOR('',#1247,2.25E-2);

+#1249=CARTESIAN_POINT('',(6.25E-2,-6.E-2,-2.7E-1));

+#1250=LINE('',#1249,#1248);

+#1251=DIRECTION('',(0.E0,1.E0,0.E0));

+#1252=VECTOR('',#1251,2.25E-2);

+#1253=CARTESIAN_POINT('',(6.25E-2,-6.E-2,-2.95E-1));

+#1254=LINE('',#1253,#1252);

+#1255=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1256=VECTOR('',#1255,1.5E-2);

+#1257=CARTESIAN_POINT('',(6.25E-2,-6.E-2,-2.95E-1));

+#1258=LINE('',#1257,#1256);

+#1259=CARTESIAN_POINT('',(6.25E-2,-3.75E-2,-2.45E-1));

+#1260=DIRECTION('',(1.E0,0.E0,0.E0));

+#1261=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1262=AXIS2_PLACEMENT_3D('',#1259,#1260,#1261);

+#1264=DIRECTION('',(1.E0,0.E0,0.E0));

+#1265=VECTOR('',#1264,2.5E-2);

+#1266=CARTESIAN_POINT('',(3.75E-2,-2.45E-1,-2.95E-1));

+#1267=LINE('',#1266,#1265);

+#1268=DIRECTION('',(1.E0,0.E0,0.E0));

+#1269=VECTOR('',#1268,2.5E-2);

+#1270=CARTESIAN_POINT('',(3.75E-2,-3.75E-2,-2.95E-1));

+#1271=LINE('',#1270,#1269);

+#1272=DIRECTION('',(1.E0,0.E0,0.E0));

+#1273=VECTOR('',#1272,2.5E-2);

+#1274=CARTESIAN_POINT('',(3.75E-2,-2.45E-1,-2.7E-1));

+#1275=LINE('',#1274,#1273);

+#1276=DIRECTION('',(1.E0,0.E0,0.E0));

+#1277=VECTOR('',#1276,2.E-1);

+#1278=CARTESIAN_POINT('',(-1.E-1,6.E-2,-2.69E-1));

+#1279=LINE('',#1278,#1277);

+#1280=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1281=VECTOR('',#1280,8.5E-2);

+#1282=CARTESIAN_POINT('',(-1.E-1,-6.E-2,-2.25E-1));

+#1283=LINE('',#1282,#1281);

+#1284=DIRECTION('',(0.E0,1.E0,0.E0));

+#1285=VECTOR('',#1284,3.E-2);

+#1286=CARTESIAN_POINT('',(-1.E-1,3.E-2,-2.69E-1));

+#1287=LINE('',#1286,#1285);

+#1288=DIRECTION('',(0.E0,0.E0,1.E0));

+#1289=VECTOR('',#1288,4.4E-2);

+#1290=CARTESIAN_POINT('',(-1.E-1,6.E-2,-2.69E-1));

+#1291=LINE('',#1290,#1289);

+#1292=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1293=VECTOR('',#1292,4.1E-2);

+#1294=CARTESIAN_POINT('',(-1.E-1,3.E-2,-2.69E-1));

+#1295=LINE('',#1294,#1293);

+#1296=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1297=VECTOR('',#1296,4.1E-2);

+#1298=CARTESIAN_POINT('',(-3.75E-2,3.E-2,-2.69E-1));

+#1299=LINE('',#1298,#1297);

+#1300=DIRECTION('',(1.E0,0.E0,0.E0));

+#1301=VECTOR('',#1300,2.5E-2);

+#1302=CARTESIAN_POINT('',(-6.25E-2,-3.75E-2,-2.95E-1));

+#1303=LINE('',#1302,#1301);

+#1304=DIRECTION('',(7.071067811865E-1,7.071067811865E-1,0.E0));

+#1305=VECTOR('',#1304,2.121320343560E-2);

+#1306=CARTESIAN_POINT('',(-8.E-2,-1.15E-1,8.53E-2));

+#1307=LINE('',#1306,#1305);

+#1308=DIRECTION('',(0.E0,0.E0,1.E0));

+#1309=VECTOR('',#1308,1.397E-1);

+#1310=CARTESIAN_POINT('',(-8.E-2,-1.15E-1,8.53E-2));

+#1311=LINE('',#1310,#1309);

+#1312=DIRECTION('',(4.472135954999E-1,4.472135954999E-1,7.745966692415E-1));

+#1313=VECTOR('',#1312,3.354101966250E-2);

+#1314=CARTESIAN_POINT('',(-8.E-2,-1.15E-1,3.606921938166E-2));

+#1315=LINE('',#1314,#1313);

+#1316=DIRECTION('',(0.E0,0.E0,1.E0));

+#1317=VECTOR('',#1316,2.610692193817E-1);

+#1318=CARTESIAN_POINT('',(-8.E-2,-1.15E-1,-2.25E-1));

+#1319=LINE('',#1318,#1317);

+#1320=CARTESIAN_POINT('',(-5.357659555759E-2,-1.584529252708E-1,8.53E-2));

+#1321=CARTESIAN_POINT('',(-5.663251262146E-2,-1.602748020020E-1,8.53E-2));

+#1322=CARTESIAN_POINT('',(-6.248823265869E-2,-1.635695268730E-1,8.53E-2));

+#1323=CARTESIAN_POINT('',(-7.050677154246E-2,-1.674178932629E-1,8.53E-2));

+#1324=CARTESIAN_POINT('',(-7.782186226657E-2,-1.701926684698E-1,8.53E-2));

+#1325=CARTESIAN_POINT('',(-8.468531890115E-2,-1.720006646524E-1,8.53E-2));

+#1326=CARTESIAN_POINT('',(-9.141439480696E-2,-1.728932407002E-1,8.53E-2));

+#1327=CARTESIAN_POINT('',(-9.812537784739E-2,-1.728324213069E-1,8.53E-2));

+#1328=CARTESIAN_POINT('',(-1.048497820165E-1,-1.718253942191E-1,8.53E-2));

+#1329=CARTESIAN_POINT('',(-1.117350648462E-1,-1.699078390694E-1,8.53E-2));

+#1330=CARTESIAN_POINT('',(-1.191013480360E-1,-1.670181714848E-1,8.53E-2));

+#1331=CARTESIAN_POINT('',(-1.271830023765E-1,-1.630537253032E-1,8.53E-2));

+#1332=CARTESIAN_POINT('',(-1.360381346460E-1,-1.579986388164E-1,8.53E-2));

+#1333=CARTESIAN_POINT('',(-1.457081013489E-1,-1.518761100516E-1,8.53E-2));

+#1334=CARTESIAN_POINT('',(-1.527513889228E-1,-1.470847088016E-1,8.53E-2));

+#1335=CARTESIAN_POINT('',(-1.564234044424E-1,-1.445195163487E-1,8.53E-2));

+#1337=DIRECTION('',(0.E0,-1.E0,0.E0));

+#1338=VECTOR('',#1337,4.451951634874E-2);

+#1339=CARTESIAN_POINT('',(-1.564234044424E-1,-1.E-1,8.53E-2));

+#1340=LINE('',#1339,#1338);

+#1341=DIRECTION('',(-7.713691830612E-1,-6.363879189799E-1,0.E0));

+#1342=VECTOR('',#1341,9.758311976582E-3);

+#1343=CARTESIAN_POINT('',(-7.409325605598E-2,-1.882902550866E-1,4.63E-2));

+#1344=LINE('',#1343,#1342);

+#1345=DIRECTION('',(7.713691830612E-1,-6.363879189799E-1,0.E0));

+#1346=VECTOR('',#1345,3.737601997139E-2);

+#1347=CARTESIAN_POINT('',(-1.359067439440E-1,-1.707146793688E-1,4.63E-2));

+#1348=LINE('',#1347,#1346);

+#1349=CARTESIAN_POINT('',(-1.359067439440E-1,-1.707146793688E-1,4.63E-2));

+#1350=CARTESIAN_POINT('',(-1.347818304692E-1,-1.716427451524E-1,

+4.435159270743E-2));

+#1351=CARTESIAN_POINT('',(-1.325825676173E-1,-1.732270113341E-1,

+4.054235770879E-2));

+#1352=CARTESIAN_POINT('',(-1.294823806596E-1,-1.747303299587E-1,

+3.517267638502E-2));

+#1353=CARTESIAN_POINT('',(-1.264154611381E-1,-1.754149892381E-1,

+2.986061595117E-2));

+#1354=CARTESIAN_POINT('',(-1.233594852548E-1,-1.752359071234E-1,

+2.456751045462E-2));

+#1355=CARTESIAN_POINT('',(-1.202915661583E-1,-1.742119607851E-1,

+1.925371870584E-2));

+#1356=CARTESIAN_POINT('',(-1.171335690736E-1,-1.723643667652E-1,

+1.378390730511E-2));

+#1357=CARTESIAN_POINT('',(-1.137616720891E-1,-1.696454501204E-1,

+7.943610410063E-3));

+#1358=CARTESIAN_POINT('',(-1.100774138201E-1,-1.659761594804E-1,

+1.562287899877E-3));

+#1359=CARTESIAN_POINT('',(-1.073969492721E-1,-1.628921138363E-1,

+-3.080412885144E-3));

+#1360=CARTESIAN_POINT('',(-1.06E-1,-1.611975091251E-1,-5.5E-3));

+#1362=DIRECTION('',(0.E0,-1.E0,0.E0));

+#1363=VECTOR('',#1362,6.119750912510E-2);

+#1364=CARTESIAN_POINT('',(-1.06E-1,-1.E-1,-5.5E-3));

+#1365=LINE('',#1364,#1363);

+#1366=CARTESIAN_POINT('',(-1.574234044424E-1,-1.445586341714E-1,

+8.356794919243E-2));

+#1367=CARTESIAN_POINT('',(-1.556513464535E-1,-1.471054225392E-1,

+8.049865472165E-2));

+#1368=CARTESIAN_POINT('',(-1.522547386123E-1,-1.518843967519E-1,

+7.461555736733E-2));

+#1369=CARTESIAN_POINT('',(-1.476050986136E-1,-1.580669398403E-1,

+6.656214465263E-2));

+#1370=CARTESIAN_POINT('',(-1.433539961350E-1,-1.632828598805E-1,

+5.919901917161E-2));

+#1371=CARTESIAN_POINT('',(-1.394858289585E-1,-1.675175689925E-1,

+5.249915708967E-2));

+#1372=CARTESIAN_POINT('',(-1.370687942147E-1,-1.697559753270E-1,

+4.831273010978E-2));

+#1373=CARTESIAN_POINT('',(-1.359067439440E-1,-1.707146793688E-1,4.63E-2));

+#1375=CARTESIAN_POINT('',(-9.434827557301E-2,-1.945003269382E-1,4.63E-2));

+#1376=DIRECTION('',(0.E0,-1.E0,0.E0));

+#1377=DIRECTION('',(-1.E0,0.E0,0.E0));

+#1378=AXIS2_PLACEMENT_3D('',#1375,#1376,#1377);

+#1380=CARTESIAN_POINT('',(-9.434827557301E-2,-1.790729432769E-1,4.63E-2));

+#1381=DIRECTION('',(0.E0,0.E0,1.E0));

+#1382=DIRECTION('',(-6.363879189799E-1,-7.713691830612E-1,0.E0));

+#1383=AXIS2_PLACEMENT_3D('',#1380,#1381,#1382);

+#1385=CARTESIAN_POINT('',(-9.434827557301E-2,-1.945003269382E-1,4.63E-2));

+#1386=DIRECTION('',(0.E0,1.E0,0.E0));

+#1387=DIRECTION('',(-1.E0,0.E0,0.E0));

+#1388=AXIS2_PLACEMENT_3D('',#1385,#1386,#1387);

+#1390=CARTESIAN_POINT('',(-9.434827557301E-2,-1.790729432769E-1,4.63E-2));

+#1391=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1392=DIRECTION('',(6.363879189799E-1,-7.713691830612E-1,0.E0));

+#1393=AXIS2_PLACEMENT_3D('',#1390,#1391,#1392);

+#1395=CARTESIAN_POINT('',(-1.04E-1,-1.615297952414E-1,-5.5E-3));

+#1396=CARTESIAN_POINT('',(-1.026682576530E-1,-1.636018584679E-1,

+-3.193354592429E-3));

+#1397=CARTESIAN_POINT('',(-1.001116839393E-1,-1.675428607596E-1,

+1.234760972986E-3));

+#1398=CARTESIAN_POINT('',(-9.659523240660E-2,-1.728248917968E-1,

+7.325433690009E-3));

+#1399=CARTESIAN_POINT('',(-9.337899903542E-2,-1.774731058704E-1,

+1.289611329789E-2));

+#1400=CARTESIAN_POINT('',(-9.044094627590E-2,-1.814760124802E-1,

+1.798496995270E-2));

+#1401=CARTESIAN_POINT('',(-8.775172573942E-2,-1.848141821279E-1,

+2.264283655464E-2));

+#1402=CARTESIAN_POINT('',(-8.530186951023E-2,-1.874238866681E-1,

+2.688611201484E-2));

+#1403=CARTESIAN_POINT('',(-8.306372284748E-2,-1.892734762783E-1,

+3.076269574951E-2));

+#1404=CARTESIAN_POINT('',(-8.085149192496E-2,-1.904147026451E-1,

+3.459439210539E-2));

+#1405=CARTESIAN_POINT('',(-7.856145149216E-2,-1.906606296865E-1,

+3.856085848638E-2));

+#1406=CARTESIAN_POINT('',(-7.631357120374E-2,-1.899218247534E-1,

+4.245430135527E-2));

+#1407=CARTESIAN_POINT('',(-7.483518463656E-2,-1.889023541901E-1,

+4.501494200285E-2));

+#1408=CARTESIAN_POINT('',(-7.409325605598E-2,-1.882902550866E-1,4.63E-2));

+#1410=CARTESIAN_POINT('',(-7.409325605598E-2,-1.882902550866E-1,4.63E-2));

+#1411=CARTESIAN_POINT('',(-7.331187686197E-2,-1.876456088004E-1,

+4.765338846400E-2));

+#1412=CARTESIAN_POINT('',(-7.171593453487E-2,-1.861024731040E-1,

+5.041764166049E-2));

+#1413=CARTESIAN_POINT('',(-6.926242411520E-2,-1.832438523859E-1,

+5.466724636425E-2));

+#1414=CARTESIAN_POINT('',(-6.652736541744E-2,-1.796773890294E-1,

+5.940450699046E-2));

+#1415=CARTESIAN_POINT('',(-6.352615439082E-2,-1.754627992800E-1,

+6.460275697280E-2));

+#1416=CARTESIAN_POINT('',(-6.022212646643E-2,-1.705966377575E-1,

+7.032550120747E-2));

+#1417=CARTESIAN_POINT('',(-5.659847526199E-2,-1.650857169535E-1,

+7.660184920247E-2));

+#1418=CARTESIAN_POINT('',(-5.395509643280E-2,-1.609766693234E-1,

+8.118031563828E-2));

+#1419=CARTESIAN_POINT('',(-5.257659555759E-2,-1.588166648235E-1,

+8.356794919243E-2));

+#1421=DIRECTION('',(0.E0,-1.E0,0.E0));

+#1422=VECTOR('',#1421,5.881666482353E-2);

+#1423=CARTESIAN_POINT('',(-5.257659555759E-2,-1.E-1,8.356794919243E-2));

+#1424=LINE('',#1423,#1422);

+#1425=CARTESIAN_POINT('',(-5.257659555759E-2,-1.588166648235E-1,

+8.356794919243E-2));

+#1426=CARTESIAN_POINT('',(-5.217581120736E-2,-1.584428467302E-1,

+8.379934214492E-2));

+#1427=CARTESIAN_POINT('',(-5.137088670405E-2,-1.576916887051E-1,

+8.426406552358E-2));

+#1428=CARTESIAN_POINT('',(-5.055923535607E-2,-1.569335088076E-1,

+8.473267264783E-2));

+#1429=CARTESIAN_POINT('',(-5.015172442699E-2,-1.565526684416E-1,

+8.496794919243E-2));

+#1431=DIRECTION('',(0.E0,-1.E0,0.E0));

+#1432=VECTOR('',#1431,5.655266844162E-2);

+#1433=CARTESIAN_POINT('',(-5.015172442699E-2,-1.E-1,8.496794919243E-2));

+#1434=LINE('',#1433,#1432);

+#1435=CARTESIAN_POINT('',(-5.015172442699E-2,-1.565526684416E-1,

+8.496794919243E-2));

+#1436=CARTESIAN_POINT('',(-5.031806470115E-2,-1.564994379575E-1,

+8.525605899863E-2));

+#1437=CARTESIAN_POINT('',(-5.065107063889E-2,-1.563883833904E-1,

+8.583284220201E-2));

+#1438=CARTESIAN_POINT('',(-5.098473015951E-2,-1.562681802864E-1,

+8.641075744415E-2));

+#1439=CARTESIAN_POINT('',(-5.115172442699E-2,-1.562058027820E-1,8.67E-2));

+#1441=DIRECTION('',(0.E0,-1.E0,0.E0));

+#1442=VECTOR('',#1441,5.620580278197E-2);

+#1443=CARTESIAN_POINT('',(-5.115172442699E-2,-1.E-1,8.67E-2));

+#1444=LINE('',#1443,#1442);

+#1445=CARTESIAN_POINT('',(-5.115172442699E-2,-1.562058027820E-1,8.67E-2));

+#1446=CARTESIAN_POINT('',(-5.155953580995E-2,-1.565842146916E-1,

+8.646454998827E-2));

+#1447=CARTESIAN_POINT('',(-5.237148638706E-2,-1.573371556234E-1,

+8.599577010401E-2));

+#1448=CARTESIAN_POINT('',(-5.317611041452E-2,-1.580823051987E-1,

+8.553122020516E-2));

+#1449=CARTESIAN_POINT('',(-5.357659555759E-2,-1.584529252708E-1,8.53E-2));

+#1451=DIRECTION('',(0.E0,-1.E0,0.E0));

+#1452=VECTOR('',#1451,5.845292527080E-2);

+#1453=CARTESIAN_POINT('',(-5.357659555759E-2,-1.E-1,8.53E-2));

+#1454=LINE('',#1453,#1452);

+#1455=DIRECTION('',(7.071067811866E-1,-7.071067811865E-1,0.E0));

+#1456=VECTOR('',#1455,2.121320343560E-2);

+#1457=CARTESIAN_POINT('',(-9.5E-2,-1.E-1,8.53E-2));

+#1458=LINE('',#1457,#1456);

+#1459=DIRECTION('',(-4.472135954999E-1,4.472135954999E-1,-7.745966692415E-1));

+#1460=VECTOR('',#1459,3.354101966250E-2);

+#1461=CARTESIAN_POINT('',(-8.E-2,-1.15E-1,3.606921938166E-2));

+#1462=LINE('',#1461,#1460);

+#1463=CARTESIAN_POINT('',(-1.04E-1,-1.592568924846E-1,-8.3E-3));

+#1464=CARTESIAN_POINT('',(-1.04E-1,-1.596394080684E-1,-7.829162455956E-3));

+#1465=CARTESIAN_POINT('',(-1.04E-1,-1.604007469310E-1,-6.891663246043E-3));

+#1466=CARTESIAN_POINT('',(-1.04E-1,-1.611546829174E-1,-5.962500939277E-3));

+#1467=CARTESIAN_POINT('',(-1.04E-1,-1.615297952414E-1,-5.5E-3));

+#1469=DIRECTION('',(0.E0,-1.E0,0.E0));

+#1470=VECTOR('',#1469,6.152979524142E-2);

+#1471=CARTESIAN_POINT('',(-1.04E-1,-1.E-1,-5.5E-3));

+#1472=LINE('',#1471,#1470);

+#1473=CARTESIAN_POINT('',(-1.06E-1,-1.589410006771E-1,-8.3E-3));

+#1474=CARTESIAN_POINT('',(-1.056659442691E-1,-1.589985189396E-1,-8.3E-3));

+#1475=CARTESIAN_POINT('',(-1.049985574959E-1,-1.591086986771E-1,-8.3E-3));

+#1476=CARTESIAN_POINT('',(-1.043326128179E-1,-1.592091253379E-1,-8.3E-3));

+#1477=CARTESIAN_POINT('',(-1.04E-1,-1.592568924846E-1,-8.3E-3));

+#1479=DIRECTION('',(0.E0,-1.E0,0.E0));

+#1480=VECTOR('',#1479,5.925689248462E-2);

+#1481=CARTESIAN_POINT('',(-1.04E-1,-1.E-1,-8.3E-3));

+#1482=LINE('',#1481,#1480);

+#1483=CARTESIAN_POINT('',(-1.06E-1,-1.611975091251E-1,-5.5E-3));

+#1484=CARTESIAN_POINT('',(-1.06E-1,-1.608256017647E-1,-5.962054406320E-3));

+#1485=CARTESIAN_POINT('',(-1.06E-1,-1.600776010535E-1,-6.890768553405E-3));

+#1486=CARTESIAN_POINT('',(-1.06E-1,-1.593212544286E-1,-7.828714327281E-3));

+#1487=CARTESIAN_POINT('',(-1.06E-1,-1.589410006771E-1,-8.3E-3));

+#1489=DIRECTION('',(0.E0,-1.E0,0.E0));

+#1490=VECTOR('',#1489,5.894100067710E-2);

+#1491=CARTESIAN_POINT('',(-1.06E-1,-1.E-1,-8.3E-3));

+#1492=LINE('',#1491,#1490);

+#1493=CARTESIAN_POINT('',(-1.598482755730E-1,-1.422487166033E-1,

+8.496794919243E-2));

+#1494=CARTESIAN_POINT('',(-1.594435190594E-1,-1.426342859019E-1,

+8.473426291033E-2));

+#1495=CARTESIAN_POINT('',(-1.586346173674E-1,-1.434048414760E-1,

+8.426724330069E-2));

+#1496=CARTESIAN_POINT('',(-1.578269383160E-1,-1.441742309618E-1,

+8.380092958296E-2));

+#1497=CARTESIAN_POINT('',(-1.574234044424E-1,-1.445586341714E-1,

+8.356794919243E-2));

+#1499=DIRECTION('',(0.E0,-1.E0,0.E0));

+#1500=VECTOR('',#1499,4.455863417144E-2);

+#1501=CARTESIAN_POINT('',(-1.574234044424E-1,-1.E-1,8.356794919243E-2));

+#1502=LINE('',#1501,#1500);

+#1503=CARTESIAN_POINT('',(-1.588482755730E-1,-1.422110378340E-1,8.67E-2));

+#1504=CARTESIAN_POINT('',(-1.590149631225E-1,-1.422209303233E-1,

+8.641128869537E-2));

+#1505=CARTESIAN_POINT('',(-1.593483173103E-1,-1.422371043369E-1,

+8.583390230505E-2));

+#1506=CARTESIAN_POINT('',(-1.596816297755E-1,-1.422460507658E-1,

+8.525658818051E-2));

+#1507=CARTESIAN_POINT('',(-1.598482755730E-1,-1.422487166033E-1,

+8.496794919243E-2));

+#1509=DIRECTION('',(0.E0,-1.E0,0.E0));

+#1510=VECTOR('',#1509,4.224871660332E-2);

+#1511=CARTESIAN_POINT('',(-1.598482755730E-1,-1.E-1,8.496794919243E-2));

+#1512=LINE('',#1511,#1510);

+#1513=CARTESIAN_POINT('',(-1.564234044424E-1,-1.445195163487E-1,8.53E-2));

+#1514=CARTESIAN_POINT('',(-1.568263799912E-1,-1.441358932733E-1,

+8.553265804156E-2));

+#1515=CARTESIAN_POINT('',(-1.576335005946E-1,-1.433675235913E-1,

+8.599864933919E-2));

+#1516=CARTESIAN_POINT('',(-1.584429606144E-1,-1.425969074141E-1,

+8.646599129957E-2));

+#1517=CARTESIAN_POINT('',(-1.588482755730E-1,-1.422110378340E-1,8.67E-2));

+#1519=DIRECTION('',(0.E0,-1.E0,0.E0));

+#1520=VECTOR('',#1519,4.221103783395E-2);

+#1521=CARTESIAN_POINT('',(-1.588482755730E-1,-1.E-1,8.67E-2));

+#1522=LINE('',#1521,#1520);

+#1523=DIRECTION('',(0.E0,-1.E0,0.E0));

+#1524=VECTOR('',#1523,4.E-2);

+#1525=CARTESIAN_POINT('',(-2.5E-2,-6.E-2,-1.25E-1));

+#1526=LINE('',#1525,#1524);

+#1527=DIRECTION('',(-1.E0,0.E0,0.E0));

+#1528=VECTOR('',#1527,5.E-2);

+#1529=CARTESIAN_POINT('',(2.5E-2,-6.E-2,-1.25E-1));

+#1530=LINE('',#1529,#1528);

+#1531=DIRECTION('',(0.E0,1.E0,0.E0));

+#1532=VECTOR('',#1531,4.E-2);

+#1533=CARTESIAN_POINT('',(2.5E-2,-1.E-1,-1.25E-1));

+#1534=LINE('',#1533,#1532);

+#1535=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1536=VECTOR('',#1535,3.5E-1);

+#1537=CARTESIAN_POINT('',(-2.5E-2,-6.E-2,2.25E-1));

+#1538=LINE('',#1537,#1536);

+#1539=DIRECTION('',(0.E0,0.E0,-1.E0));

+#1540=VECTOR('',#1539,3.5E-1);

+#1541=CARTESIAN_POINT('',(2.5E-2,-6.E-2,2.25E-1));

+#1542=LINE('',#1541,#1540);

+#1543=CARTESIAN_POINT('',(3.75E-2,1.25E-2,1.15E-1));

+#1544=CARTESIAN_POINT('',(3.75E-2,-1.25E-2,1.15E-1));

+#1545=VERTEX_POINT('',#1543);

+#1546=VERTEX_POINT('',#1544);

+#1547=CARTESIAN_POINT('',(3.75E-2,-2.45E-1,-2.7E-1));

+#1548=CARTESIAN_POINT('',(3.75E-2,-2.45E-1,-2.95E-1));

+#1549=VERTEX_POINT('',#1547);

+#1550=VERTEX_POINT('',#1548);

+#1551=CARTESIAN_POINT('',(6.25E-2,1.25E-2,1.15E-1));

+#1552=CARTESIAN_POINT('',(6.25E-2,-1.25E-2,1.15E-1));

+#1553=VERTEX_POINT('',#1551);

+#1554=VERTEX_POINT('',#1552);

+#1555=CARTESIAN_POINT('',(6.25E-2,-2.45E-1,-2.7E-1));

+#1556=CARTESIAN_POINT('',(6.25E-2,-2.45E-1,-2.95E-1));

+#1557=VERTEX_POINT('',#1555);

+#1558=VERTEX_POINT('',#1556);

+#1559=CARTESIAN_POINT('',(3.75E-2,-1.25E-2,-1.25E-1));

+#1560=VERTEX_POINT('',#1559);

+#1561=CARTESIAN_POINT('',(3.75E-2,1.25E-2,-1.25E-1));

+#1562=VERTEX_POINT('',#1561);

+#1563=CARTESIAN_POINT('',(6.25E-2,-1.25E-2,-1.25E-1));

+#1564=VERTEX_POINT('',#1563);

+#1565=CARTESIAN_POINT('',(6.25E-2,1.25E-2,-1.25E-1));

+#1566=VERTEX_POINT('',#1565);

+#1567=CARTESIAN_POINT('',(3.75E-2,-6.E-2,-2.7E-1));

+#1568=VERTEX_POINT('',#1567);

+#1569=CARTESIAN_POINT('',(6.25E-2,-6.E-2,-2.7E-1));

+#1570=VERTEX_POINT('',#1569);

+#1571=CARTESIAN_POINT('',(6.25E-2,-6.E-2,-2.95E-1));

+#1572=VERTEX_POINT('',#1571);

+#1573=CARTESIAN_POINT('',(3.75E-2,-3.75E-2,-2.95E-1));

+#1574=CARTESIAN_POINT('',(6.25E-2,-3.75E-2,-2.95E-1));

+#1575=VERTEX_POINT('',#1573);

+#1576=VERTEX_POINT('',#1574);

+#1577=CARTESIAN_POINT('',(3.75E-2,-6.E-2,-2.95E-1));

+#1578=VERTEX_POINT('',#1577);

+#1579=CARTESIAN_POINT('',(3.75E-2,-3.75E-2,-2.7E-1));

+#1580=CARTESIAN_POINT('',(6.25E-2,-3.75E-2,-2.7E-1));

+#1581=VERTEX_POINT('',#1579);

+#1582=VERTEX_POINT('',#1580);

+#1583=CARTESIAN_POINT('',(-1.4E-1,1.6E-1,2.25E-1));

+#1584=CARTESIAN_POINT('',(-1.4E-1,-5.E-2,2.25E-1));

+#1585=VERTEX_POINT('',#1583);

+#1586=VERTEX_POINT('',#1584);

+#1587=CARTESIAN_POINT('',(1.4E-1,-5.E-2,2.25E-1));

+#1588=CARTESIAN_POINT('',(1.4E-1,1.6E-1,2.25E-1));

+#1589=VERTEX_POINT('',#1587);

+#1590=VERTEX_POINT('',#1588);

+#1591=CARTESIAN_POINT('',(-1.4E-1,1.6E-1,7.5E-2));

+#1592=VERTEX_POINT('',#1591);

+#1593=CARTESIAN_POINT('',(1.4E-1,1.6E-1,7.5E-2));

+#1594=VERTEX_POINT('',#1593);

+#1595=CARTESIAN_POINT('',(1.4E-1,1.E-1,-2.25E-1));

+#1596=CARTESIAN_POINT('',(-1.4E-1,1.E-1,-2.25E-1));

+#1597=VERTEX_POINT('',#1595);

+#1598=VERTEX_POINT('',#1596);

+#1599=CARTESIAN_POINT('',(-1.4E-1,1.E-1,7.5E-2));

+#1600=VERTEX_POINT('',#1599);

+#1601=CARTESIAN_POINT('',(1.4E-1,1.E-1,7.5E-2));

+#1602=VERTEX_POINT('',#1601);

+#1603=CARTESIAN_POINT('',(-1.4E-1,-5.E-2,-2.25E-1));

+#1604=VERTEX_POINT('',#1603);

+#1605=CARTESIAN_POINT('',(1.4E-1,-5.E-2,-2.25E-1));

+#1606=VERTEX_POINT('',#1605);

+#1607=CARTESIAN_POINT('',(1.05E-1,1.E-1,7.5E-2));

+#1608=CARTESIAN_POINT('',(1.05E-1,1.15E-1,7.5E-2));

+#1609=VERTEX_POINT('',#1607);

+#1610=VERTEX_POINT('',#1608);

+#1611=CARTESIAN_POINT('',(-1.05E-1,1.E-1,7.5E-2));

+#1612=VERTEX_POINT('',#1611);

+#1613=CARTESIAN_POINT('',(1.05E-1,6.5E-2,-1.25E-1));

+#1614=CARTESIAN_POINT('',(6.E-2,6.5E-2,-1.25E-1));

+#1615=VERTEX_POINT('',#1613);

+#1616=VERTEX_POINT('',#1614);

+#1617=CARTESIAN_POINT('',(-1.05E-1,-5.5E-2,-1.25E-1));

+#1618=CARTESIAN_POINT('',(1.05E-1,-5.5E-2,-1.25E-1));

+#1619=VERTEX_POINT('',#1617);

+#1620=VERTEX_POINT('',#1618);

+#1621=CARTESIAN_POINT('',(-6.E-2,6.5E-2,-1.25E-1));

+#1622=CARTESIAN_POINT('',(-1.05E-1,6.5E-2,-1.25E-1));

+#1623=VERTEX_POINT('',#1621);

+#1624=VERTEX_POINT('',#1622);

+#1625=CARTESIAN_POINT('',(-6.E-2,1.E-1,-1.25E-1));

+#1626=CARTESIAN_POINT('',(6.E-2,1.E-1,-1.25E-1));

+#1627=VERTEX_POINT('',#1625);

+#1628=VERTEX_POINT('',#1626);

+#1629=CARTESIAN_POINT('',(-6.E-2,1.E-1,7.5E-2));

+#1630=VERTEX_POINT('',#1629);

+#1631=CARTESIAN_POINT('',(6.E-2,1.E-1,7.5E-2));

+#1632=VERTEX_POINT('',#1631);

+#1633=CARTESIAN_POINT('',(1.E-1,1.15E-1,7.5E-2));

+#1634=CARTESIAN_POINT('',(1.E-1,1.6E-1,7.5E-2));

+#1635=VERTEX_POINT('',#1633);

+#1636=VERTEX_POINT('',#1634);

+#1637=CARTESIAN_POINT('',(-1.E-1,1.15E-1,7.5E-2));

+#1638=CARTESIAN_POINT('',(-1.E-1,1.6E-1,7.5E-2));

+#1639=VERTEX_POINT('',#1637);

+#1640=VERTEX_POINT('',#1638);

+#1641=CARTESIAN_POINT('',(1.E-1,1.15E-1,1.12E-1));

+#1642=CARTESIAN_POINT('',(1.E-1,1.6E-1,1.12E-1));

+#1643=VERTEX_POINT('',#1641);

+#1644=VERTEX_POINT('',#1642);

+#1645=CARTESIAN_POINT('',(4.7E-2,1.15E-1,1.65E-1));

+#1646=CARTESIAN_POINT('',(4.7E-2,1.6E-1,1.65E-1));

+#1647=VERTEX_POINT('',#1645);

+#1648=VERTEX_POINT('',#1646);

+#1649=CARTESIAN_POINT('',(-4.7E-2,1.15E-1,1.65E-1));

+#1650=CARTESIAN_POINT('',(-4.7E-2,1.6E-1,1.65E-1));

+#1651=VERTEX_POINT('',#1649);

+#1652=VERTEX_POINT('',#1650);

+#1653=CARTESIAN_POINT('',(-1.E-1,1.15E-1,1.12E-1));

+#1654=CARTESIAN_POINT('',(-1.E-1,1.6E-1,1.12E-1));

+#1655=VERTEX_POINT('',#1653);

+#1656=VERTEX_POINT('',#1654);

+#1657=CARTESIAN_POINT('',(-1.25E-1,1.35E-1,2.25E-1));

+#1658=CARTESIAN_POINT('',(-1.25E-1,-5.5E-2,2.25E-1));

+#1659=VERTEX_POINT('',#1657);

+#1660=VERTEX_POINT('',#1658);

+#1661=CARTESIAN_POINT('',(-1.05E-1,1.15E-1,2.05E-1));

+#1662=CARTESIAN_POINT('',(-1.05E-1,-5.5E-2,2.05E-1));

+#1663=VERTEX_POINT('',#1661);

+#1664=VERTEX_POINT('',#1662);

+#1665=CARTESIAN_POINT('',(1.25E-1,-5.5E-2,2.25E-1));

+#1666=CARTESIAN_POINT('',(1.25E-1,1.35E-1,2.25E-1));

+#1667=VERTEX_POINT('',#1665);

+#1668=VERTEX_POINT('',#1666);

+#1669=CARTESIAN_POINT('',(1.05E-1,-5.5E-2,2.05E-1));

+#1670=CARTESIAN_POINT('',(1.05E-1,1.15E-1,2.05E-1));

+#1671=VERTEX_POINT('',#1669);

+#1672=VERTEX_POINT('',#1670);

+#1673=CARTESIAN_POINT('',(6.E-2,8.5E-2,7.5E-2));

+#1674=CARTESIAN_POINT('',(1.05E-1,8.5E-2,7.5E-2));

+#1675=VERTEX_POINT('',#1673);

+#1676=VERTEX_POINT('',#1674);

+#1677=CARTESIAN_POINT('',(6.E-2,6.5E-2,5.5E-2));

+#1678=CARTESIAN_POINT('',(1.05E-1,6.5E-2,5.5E-2));

+#1679=VERTEX_POINT('',#1677);

+#1680=VERTEX_POINT('',#1678);

+#1681=CARTESIAN_POINT('',(-6.E-2,8.5E-2,7.5E-2));

+#1682=CARTESIAN_POINT('',(-1.05E-1,8.5E-2,7.5E-2));

+#1683=VERTEX_POINT('',#1681);

+#1684=VERTEX_POINT('',#1682);

+#1685=CARTESIAN_POINT('',(-6.E-2,6.5E-2,5.5E-2));

+#1686=CARTESIAN_POINT('',(-1.05E-1,6.5E-2,5.5E-2));

+#1687=VERTEX_POINT('',#1685);

+#1688=VERTEX_POINT('',#1686);

+#1689=CARTESIAN_POINT('',(-1.65E-1,-5.E-2,-2.25E-1));

+#1690=VERTEX_POINT('',#1689);

+#1691=CARTESIAN_POINT('',(-1.65E-1,-1.E-1,-2.25E-1));

+#1692=VERTEX_POINT('',#1691);

+#1693=CARTESIAN_POINT('',(1.65E-1,-1.E-1,-2.25E-1));

+#1694=CARTESIAN_POINT('',(1.65E-1,-5.E-2,-2.25E-1));

+#1695=VERTEX_POINT('',#1693);

+#1696=VERTEX_POINT('',#1694);

+#1697=CARTESIAN_POINT('',(-1.65E-1,-5.E-2,2.25E-1));

+#1698=VERTEX_POINT('',#1697);

+#1699=CARTESIAN_POINT('',(-1.65E-1,-1.E-1,2.25E-1));

+#1700=VERTEX_POINT('',#1699);

+#1701=CARTESIAN_POINT('',(1.65E-1,-1.E-1,2.25E-1));

+#1702=CARTESIAN_POINT('',(1.65E-1,-5.E-2,2.25E-1));

+#1703=VERTEX_POINT('',#1701);

+#1704=VERTEX_POINT('',#1702);

+#1705=CARTESIAN_POINT('',(-1.E-1,6.E-2,-2.69E-1));

+#1706=CARTESIAN_POINT('',(1.E-1,6.E-2,-2.69E-1));

+#1707=VERTEX_POINT('',#1705);

+#1708=VERTEX_POINT('',#1706);

+#1709=CARTESIAN_POINT('',(-1.E-1,-6.E-2,-2.25E-1));

+#1710=CARTESIAN_POINT('',(-1.E-1,6.E-2,-2.25E-1));

+#1711=VERTEX_POINT('',#1709);

+#1712=VERTEX_POINT('',#1710);

+#1713=CARTESIAN_POINT('',(1.E-1,-6.E-2,-2.25E-1));

+#1714=CARTESIAN_POINT('',(1.E-1,6.E-2,-2.25E-1));

+#1715=VERTEX_POINT('',#1713);

+#1716=VERTEX_POINT('',#1714);

+#1717=CARTESIAN_POINT('',(3.75E-2,-3.05E-2,-2.69E-1));

+#1718=CARTESIAN_POINT('',(6.25E-2,-3.05E-2,-2.69E-1));

+#1719=VERTEX_POINT('',#1717);

+#1720=VERTEX_POINT('',#1718);

+#1721=CARTESIAN_POINT('',(3.75E-2,6.363424398923E-3,-2.69E-1));

+#1722=CARTESIAN_POINT('',(6.25E-2,6.363424398923E-3,-2.69E-1));

+#1723=VERTEX_POINT('',#1721);

+#1724=VERTEX_POINT('',#1722);

+#1725=CARTESIAN_POINT('',(-1.05E-1,1.15E-1,7.5E-2));

+#1726=VERTEX_POINT('',#1725);

+#1727=CARTESIAN_POINT('',(-9.5E-2,-1.E-1,-2.25E-1));

+#1728=VERTEX_POINT('',#1727);

+#1729=CARTESIAN_POINT('',(-6.5E-2,-1.E-1,-2.25E-1));

+#1730=CARTESIAN_POINT('',(6.5E-2,-1.E-1,-2.25E-1));

+#1731=VERTEX_POINT('',#1729);

+#1732=VERTEX_POINT('',#1730);

+#1733=CARTESIAN_POINT('',(9.5E-2,-1.E-1,-2.25E-1));

+#1734=VERTEX_POINT('',#1733);

+#1735=CARTESIAN_POINT('',(-9.5E-2,-1.E-1,2.25E-1));

+#1736=VERTEX_POINT('',#1735);

+#1737=CARTESIAN_POINT('',(9.5E-2,-1.E-1,2.25E-1));

+#1738=VERTEX_POINT('',#1737);

+#1739=CARTESIAN_POINT('',(8.E-2,-1.15E-1,-2.25E-1));

+#1740=VERTEX_POINT('',#1739);

+#1741=CARTESIAN_POINT('',(-8.E-2,-1.15E-1,-2.25E-1));

+#1742=VERTEX_POINT('',#1741);

+#1743=CARTESIAN_POINT('',(6.5E-2,-1.E-1,2.25E-1));

+#1744=CARTESIAN_POINT('',(8.E-2,-1.15E-1,2.25E-1));

+#1745=VERTEX_POINT('',#1743);

+#1746=VERTEX_POINT('',#1744);

+#1747=CARTESIAN_POINT('',(-8.E-2,-1.15E-1,2.25E-1));

+#1748=VERTEX_POINT('',#1747);

+#1749=CARTESIAN_POINT('',(-6.5E-2,-1.E-1,2.25E-1));

+#1750=VERTEX_POINT('',#1749);

+#1751=CARTESIAN_POINT('',(2.5E-2,-6.E-2,-1.25E-1));

+#1752=CARTESIAN_POINT('',(-2.5E-2,-6.E-2,-1.25E-1));

+#1753=VERTEX_POINT('',#1751);

+#1754=VERTEX_POINT('',#1752);

+#1755=CARTESIAN_POINT('',(-2.5E-2,-1.E-1,2.25E-1));

+#1756=VERTEX_POINT('',#1755);

+#1757=CARTESIAN_POINT('',(-2.5E-2,-1.E-1,-1.25E-1));

+#1758=VERTEX_POINT('',#1757);

+#1759=CARTESIAN_POINT('',(-2.5E-2,-6.E-2,2.25E-1));

+#1760=VERTEX_POINT('',#1759);

+#1761=CARTESIAN_POINT('',(2.5E-2,-1.E-1,-1.25E-1));

+#1762=VERTEX_POINT('',#1761);

+#1763=CARTESIAN_POINT('',(1.06E-1,-1.E-1,-5.5E-3));

+#1764=CARTESIAN_POINT('',(1.574234044424E-1,-1.E-1,8.356794919243E-2));

+#1765=VERTEX_POINT('',#1763);

+#1766=VERTEX_POINT('',#1764);

+#1767=CARTESIAN_POINT('',(1.598482755730E-1,-1.E-1,8.496794919243E-2));

+#1768=VERTEX_POINT('',#1767);

+#1769=CARTESIAN_POINT('',(1.588482755730E-1,-1.E-1,8.67E-2));

+#1770=VERTEX_POINT('',#1769);

+#1771=CARTESIAN_POINT('',(1.564234044424E-1,-1.E-1,8.53E-2));

+#1772=VERTEX_POINT('',#1771);

+#1773=CARTESIAN_POINT('',(9.5E-2,-1.E-1,8.53E-2));

+#1774=VERTEX_POINT('',#1773);

+#1775=CARTESIAN_POINT('',(6.5E-2,-1.E-1,8.53E-2));

+#1776=CARTESIAN_POINT('',(5.357659555759E-2,-1.E-1,8.53E-2));

+#1777=VERTEX_POINT('',#1775);

+#1778=VERTEX_POINT('',#1776);

+#1779=CARTESIAN_POINT('',(5.115172442699E-2,-1.E-1,8.67E-2));

+#1780=VERTEX_POINT('',#1779);

+#1781=CARTESIAN_POINT('',(5.015172442699E-2,-1.E-1,8.496794919243E-2));

+#1782=VERTEX_POINT('',#1781);

+#1783=CARTESIAN_POINT('',(5.257659555759E-2,-1.E-1,8.356794919243E-2));

+#1784=VERTEX_POINT('',#1783);

+#1785=CARTESIAN_POINT('',(6.5E-2,-1.E-1,6.204998149519E-2));

+#1786=VERTEX_POINT('',#1785);

+#1787=CARTESIAN_POINT('',(1.04E-1,-1.E-1,-8.3E-3));

+#1788=CARTESIAN_POINT('',(1.06E-1,-1.E-1,-8.3E-3));

+#1789=VERTEX_POINT('',#1787);

+#1790=VERTEX_POINT('',#1788);

+#1791=CARTESIAN_POINT('',(8.E-2,-1.15E-1,8.53E-2));

+#1792=VERTEX_POINT('',#1791);

+#1793=CARTESIAN_POINT('',(8.E-2,-1.15E-1,3.606921938166E-2));

+#1794=VERTEX_POINT('',#1793);

+#1795=CARTESIAN_POINT('',(9.5E-2,-1.E-1,1.008845726812E-2));

+#1796=VERTEX_POINT('',#1795);

+#1797=CARTESIAN_POINT('',(1.070760339526E-1,-1.945003269382E-1,4.63E-2));

+#1798=CARTESIAN_POINT('',(9.434827557301E-2,-1.990729432769E-1,4.63E-2));

+#1799=VERTEX_POINT('',#1797);

+#1800=VERTEX_POINT('',#1798);

+#1801=CARTESIAN_POINT('',(8.162051719341E-2,-1.945003269382E-1,4.63E-2));

+#1802=VERTEX_POINT('',#1801);

+#1803=VERTEX_POINT('',#233);

+#1804=VERTEX_POINT('',#244);

+#1805=VERTEX_POINT('',#246);

+#1806=VERTEX_POINT('',#434);

+#1807=VERTEX_POINT('',#424);

+#1808=VERTEX_POINT('',#414);

+#1809=VERTEX_POINT('',#335);

+#1810=VERTEX_POINT('',#404);

+#1811=VERTEX_POINT('',#394);

+#1812=VERTEX_POINT('',#384);

+#1813=VERTEX_POINT('',#289);

+#1814=VERTEX_POINT('',#302);

+#1815=VERTEX_POINT('',#279);

+#1816=VERTEX_POINT('',#269);

+#1817=CARTESIAN_POINT('',(1.04E-1,-1.E-1,-5.5E-3));

+#1818=VERTEX_POINT('',#1817);

+#1819=CARTESIAN_POINT('',(-1.06E-1,-1.E-1,-5.5E-3));

+#1820=CARTESIAN_POINT('',(-1.574234044424E-1,-1.E-1,8.356794919243E-2));

+#1821=VERTEX_POINT('',#1819);

+#1822=VERTEX_POINT('',#1820);

+#1823=CARTESIAN_POINT('',(-1.598482755730E-1,-1.E-1,8.496794919243E-2));

+#1824=VERTEX_POINT('',#1823);

+#1825=CARTESIAN_POINT('',(-1.588482755730E-1,-1.E-1,8.67E-2));

+#1826=VERTEX_POINT('',#1825);

+#1827=CARTESIAN_POINT('',(-1.564234044424E-1,-1.E-1,8.53E-2));

+#1828=VERTEX_POINT('',#1827);

+#1829=CARTESIAN_POINT('',(-9.5E-2,-1.E-1,8.53E-2));

+#1830=VERTEX_POINT('',#1829);

+#1831=CARTESIAN_POINT('',(-6.5E-2,-1.E-1,8.53E-2));

+#1832=CARTESIAN_POINT('',(-5.357659555759E-2,-1.E-1,8.53E-2));

+#1833=VERTEX_POINT('',#1831);

+#1834=VERTEX_POINT('',#1832);

+#1835=CARTESIAN_POINT('',(-5.115172442699E-2,-1.E-1,8.67E-2));

+#1836=VERTEX_POINT('',#1835);

+#1837=CARTESIAN_POINT('',(-5.015172442699E-2,-1.E-1,8.496794919243E-2));

+#1838=VERTEX_POINT('',#1837);

+#1839=CARTESIAN_POINT('',(-5.257659555759E-2,-1.E-1,8.356794919243E-2));

+#1840=VERTEX_POINT('',#1839);

+#1841=CARTESIAN_POINT('',(-6.5E-2,-1.E-1,6.204998149519E-2));

+#1842=VERTEX_POINT('',#1841);

+#1843=CARTESIAN_POINT('',(-1.04E-1,-1.E-1,-8.3E-3));

+#1844=CARTESIAN_POINT('',(-1.06E-1,-1.E-1,-8.3E-3));

+#1845=VERTEX_POINT('',#1843);

+#1846=VERTEX_POINT('',#1844);

+#1847=CARTESIAN_POINT('',(-8.E-2,-1.15E-1,8.53E-2));

+#1848=VERTEX_POINT('',#1847);

+#1849=CARTESIAN_POINT('',(-8.E-2,-1.15E-1,3.606921938166E-2));

+#1850=CARTESIAN_POINT('',(-9.5E-2,-1.E-1,1.008845726812E-2));

+#1851=VERTEX_POINT('',#1849);

+#1852=VERTEX_POINT('',#1850);

+#1853=CARTESIAN_POINT('',(-1.070760339526E-1,-1.945003269382E-1,4.63E-2));

+#1854=CARTESIAN_POINT('',(-9.434827557301E-2,-1.990729432769E-1,4.63E-2));

+#1855=VERTEX_POINT('',#1853);

+#1856=VERTEX_POINT('',#1854);

+#1857=CARTESIAN_POINT('',(-8.162051719341E-2,-1.945003269382E-1,4.63E-2));

+#1858=VERTEX_POINT('',#1857);

+#1859=VERTEX_POINT('',#1349);

+#1860=VERTEX_POINT('',#1360);

+#1861=VERTEX_POINT('',#1366);

+#1862=VERTEX_POINT('',#1493);

+#1863=VERTEX_POINT('',#1503);

+#1864=VERTEX_POINT('',#1513);

+#1865=VERTEX_POINT('',#1320);

+#1866=VERTEX_POINT('',#1445);

+#1867=VERTEX_POINT('',#1435);

+#1868=VERTEX_POINT('',#1425);

+#1869=VERTEX_POINT('',#1395);

+#1870=VERTEX_POINT('',#1408);

+#1871=VERTEX_POINT('',#1463);

+#1872=VERTEX_POINT('',#1473);

+#1873=CARTESIAN_POINT('',(-1.04E-1,-1.E-1,-5.5E-3));

+#1874=VERTEX_POINT('',#1873);

+#1875=CARTESIAN_POINT('',(1.E-1,3.E-2,-3.1E-1));

+#1876=CARTESIAN_POINT('',(1.E-1,-6.E-2,-3.1E-1));

+#1877=VERTEX_POINT('',#1875);

+#1878=VERTEX_POINT('',#1876);

+#1879=CARTESIAN_POINT('',(6.25E-2,-6.E-2,-3.1E-1));

+#1880=VERTEX_POINT('',#1879);

+#1881=CARTESIAN_POINT('',(6.25E-2,3.E-2,-3.1E-1));

+#1882=VERTEX_POINT('',#1881);

+#1883=CARTESIAN_POINT('',(-1.E-1,3.E-2,-3.1E-1));

+#1884=CARTESIAN_POINT('',(-6.25E-2,3.E-2,-3.1E-1));

+#1885=VERTEX_POINT('',#1883);

+#1886=VERTEX_POINT('',#1884);

+#1887=CARTESIAN_POINT('',(-6.25E-2,-6.E-2,-3.1E-1));

+#1888=VERTEX_POINT('',#1887);

+#1889=CARTESIAN_POINT('',(-1.E-1,-6.E-2,-3.1E-1));

+#1890=VERTEX_POINT('',#1889);

+#1891=CARTESIAN_POINT('',(3.75E-2,3.E-2,-3.1E-1));

+#1892=CARTESIAN_POINT('',(3.75E-2,-6.E-2,-3.1E-1));

+#1893=VERTEX_POINT('',#1891);

+#1894=VERTEX_POINT('',#1892);

+#1895=CARTESIAN_POINT('',(-3.75E-2,-6.E-2,-3.1E-1));

+#1896=VERTEX_POINT('',#1895);

+#1897=CARTESIAN_POINT('',(-3.75E-2,3.E-2,-3.1E-1));

+#1898=VERTEX_POINT('',#1897);

+#1899=CARTESIAN_POINT('',(1.E-1,3.E-2,-2.69E-1));

+#1900=CARTESIAN_POINT('',(6.25E-2,3.E-2,-2.69E-1));

+#1901=VERTEX_POINT('',#1899);

+#1902=VERTEX_POINT('',#1900);

+#1903=CARTESIAN_POINT('',(-1.E-1,3.E-2,-2.69E-1));

+#1904=CARTESIAN_POINT('',(-6.25E-2,3.E-2,-2.69E-1));

+#1905=VERTEX_POINT('',#1903);

+#1906=VERTEX_POINT('',#1904);

+#1907=CARTESIAN_POINT('',(-3.75E-2,3.E-2,-2.69E-1));

+#1908=CARTESIAN_POINT('',(3.75E-2,3.E-2,-2.69E-1));

+#1909=VERTEX_POINT('',#1907);

+#1910=VERTEX_POINT('',#1908);

+#1911=CARTESIAN_POINT('',(-6.25E-2,-6.E-2,-2.69E-1));

+#1912=CARTESIAN_POINT('',(-3.75E-2,-6.E-2,-2.69E-1));

+#1913=VERTEX_POINT('',#1911);

+#1914=VERTEX_POINT('',#1912);

+#1915=CARTESIAN_POINT('',(3.75E-2,-6.E-2,-2.69E-1));

+#1916=CARTESIAN_POINT('',(6.25E-2,-6.E-2,-2.69E-1));

+#1917=VERTEX_POINT('',#1915);

+#1918=VERTEX_POINT('',#1916);

+#1919=CARTESIAN_POINT('',(2.5E-2,-1.E-1,2.25E-1));

+#1920=CARTESIAN_POINT('',(2.5E-2,-6.E-2,2.25E-1));

+#1921=VERTEX_POINT('',#1919);

+#1922=VERTEX_POINT('',#1920);

+#1923=CARTESIAN_POINT('',(-6.25E-2,1.25E-2,1.15E-1));

+#1924=CARTESIAN_POINT('',(-6.25E-2,-1.25E-2,1.15E-1));

+#1925=VERTEX_POINT('',#1923);

+#1926=VERTEX_POINT('',#1924);

+#1927=CARTESIAN_POINT('',(-6.25E-2,-2.45E-1,-2.7E-1));

+#1928=CARTESIAN_POINT('',(-6.25E-2,-2.45E-1,-2.95E-1));

+#1929=VERTEX_POINT('',#1927);

+#1930=VERTEX_POINT('',#1928);

+#1931=CARTESIAN_POINT('',(-3.75E-2,1.25E-2,1.15E-1));

+#1932=CARTESIAN_POINT('',(-3.75E-2,-1.25E-2,1.15E-1));

+#1933=VERTEX_POINT('',#1931);

+#1934=VERTEX_POINT('',#1932);

+#1935=CARTESIAN_POINT('',(-3.75E-2,-2.45E-1,-2.7E-1));

+#1936=CARTESIAN_POINT('',(-3.75E-2,-2.45E-1,-2.95E-1));

+#1937=VERTEX_POINT('',#1935);

+#1938=VERTEX_POINT('',#1936);

+#1939=CARTESIAN_POINT('',(-6.25E-2,-1.25E-2,-1.25E-1));

+#1940=CARTESIAN_POINT('',(-6.25E-2,1.25E-2,-1.25E-1));

+#1941=VERTEX_POINT('',#1939);

+#1942=VERTEX_POINT('',#1940);

+#1943=CARTESIAN_POINT('',(-3.75E-2,-1.25E-2,-1.25E-1));

+#1944=CARTESIAN_POINT('',(-3.75E-2,1.25E-2,-1.25E-1));

+#1945=VERTEX_POINT('',#1943);

+#1946=VERTEX_POINT('',#1944);

+#1947=CARTESIAN_POINT('',(-6.25E-2,-6.E-2,-2.7E-1));

+#1948=CARTESIAN_POINT('',(-6.25E-2,-6.E-2,-2.95E-1));

+#1949=VERTEX_POINT('',#1947);

+#1950=VERTEX_POINT('',#1948);

+#1951=CARTESIAN_POINT('',(-3.75E-2,-6.E-2,-2.7E-1));

+#1952=CARTESIAN_POINT('',(-3.75E-2,-6.E-2,-2.95E-1));

+#1953=VERTEX_POINT('',#1951);

+#1954=VERTEX_POINT('',#1952);

+#1955=CARTESIAN_POINT('',(-6.25E-2,-3.05E-2,-2.69E-1));

+#1956=CARTESIAN_POINT('',(-3.75E-2,-3.05E-2,-2.69E-1));

+#1957=VERTEX_POINT('',#1955);

+#1958=VERTEX_POINT('',#1956);

+#1959=CARTESIAN_POINT('',(-6.25E-2,6.363424398923E-3,-2.69E-1));

+#1960=CARTESIAN_POINT('',(-3.75E-2,6.363424398923E-3,-2.69E-1));

+#1961=VERTEX_POINT('',#1959);

+#1962=VERTEX_POINT('',#1960);

+#1963=CARTESIAN_POINT('',(-6.25E-2,-3.75E-2,-2.7E-1));

+#1964=VERTEX_POINT('',#1963);

+#1965=CARTESIAN_POINT('',(-6.25E-2,-3.75E-2,-2.95E-1));

+#1966=VERTEX_POINT('',#1965);

+#1967=CARTESIAN_POINT('',(-3.75E-2,-3.75E-2,-2.7E-1));

+#1968=VERTEX_POINT('',#1967);

+#1969=CARTESIAN_POINT('',(-3.75E-2,-3.75E-2,-2.95E-1));

+#1970=VERTEX_POINT('',#1969);

+#1971=CARTESIAN_POINT('',(0.E0,0.E0,-2.25E-1));

+#1972=DIRECTION('',(0.E0,0.E0,1.E0));

+#1973=DIRECTION('',(1.E0,0.E0,0.E0));

+#1974=AXIS2_PLACEMENT_3D('',#1971,#1972,#1973);

+#1975=PLANE('',#1974);

+#1977=ORIENTED_EDGE('',*,*,#1976,.T.);

+#1979=ORIENTED_EDGE('',*,*,#1978,.T.);

+#1981=ORIENTED_EDGE('',*,*,#1980,.T.);

+#1983=ORIENTED_EDGE('',*,*,#1982,.T.);

+#1985=ORIENTED_EDGE('',*,*,#1984,.T.);

+#1987=ORIENTED_EDGE('',*,*,#1986,.T.);

+#1989=ORIENTED_EDGE('',*,*,#1988,.T.);

+#1991=ORIENTED_EDGE('',*,*,#1990,.T.);

+#1993=ORIENTED_EDGE('',*,*,#1992,.T.);

+#1995=ORIENTED_EDGE('',*,*,#1994,.T.);

+#1997=ORIENTED_EDGE('',*,*,#1996,.T.);

+#1999=ORIENTED_EDGE('',*,*,#1998,.T.);

+#2001=ORIENTED_EDGE('',*,*,#2000,.T.);

+#2003=ORIENTED_EDGE('',*,*,#2002,.T.);

+#2004=EDGE_LOOP('',(#1977,#1979,#1981,#1983,#1985,#1987,#1989,#1991,#1993,#1995,

+#1997,#1999,#2001,#2003));

+#2005=FACE_OUTER_BOUND('',#2004,.F.);

+#2007=ORIENTED_EDGE('',*,*,#2006,.T.);

+#2009=ORIENTED_EDGE('',*,*,#2008,.T.);

+#2011=ORIENTED_EDGE('',*,*,#2010,.F.);

+#2013=ORIENTED_EDGE('',*,*,#2012,.F.);

+#2014=EDGE_LOOP('',(#2007,#2009,#2011,#2013));

+#2015=FACE_BOUND('',#2014,.F.);

+#2016=ADVANCED_FACE('',(#2005,#2015),#1975,.F.);

+#2017=CARTESIAN_POINT('',(-1.65E-1,-1.E-1,-2.25E-1));

+#2018=DIRECTION('',(0.E0,-1.E0,0.E0));

+#2019=DIRECTION('',(1.E0,0.E0,0.E0));

+#2020=AXIS2_PLACEMENT_3D('',#2017,#2018,#2019);

+#2021=PLANE('',#2020);

+#2023=ORIENTED_EDGE('',*,*,#2022,.T.);

+#2025=ORIENTED_EDGE('',*,*,#2024,.T.);

+#2027=ORIENTED_EDGE('',*,*,#2026,.T.);

+#2029=ORIENTED_EDGE('',*,*,#2028,.T.);

+#2031=ORIENTED_EDGE('',*,*,#2030,.T.);

+#2033=ORIENTED_EDGE('',*,*,#2032,.T.);

+#2035=ORIENTED_EDGE('',*,*,#2034,.T.);

+#2037=ORIENTED_EDGE('',*,*,#2036,.F.);

+#2038=ORIENTED_EDGE('',*,*,#1988,.F.);

+#2040=ORIENTED_EDGE('',*,*,#2039,.T.);

+#2042=ORIENTED_EDGE('',*,*,#2041,.T.);

+#2044=ORIENTED_EDGE('',*,*,#2043,.T.);

+#2046=ORIENTED_EDGE('',*,*,#2045,.T.);

+#2048=ORIENTED_EDGE('',*,*,#2047,.T.);

+#2049=EDGE_LOOP('',(#2023,#2025,#2027,#2029,#2031,#2033,#2035,#2037,#2038,#2040,

+#2042,#2044,#2046,#2048));

+#2050=FACE_OUTER_BOUND('',#2049,.F.);

+#2051=ADVANCED_FACE('',(#2050),#2021,.T.);

+#2052=CARTESIAN_POINT('',(-1.65E-1,-1.E-1,-2.25E-1));

+#2053=DIRECTION('',(0.E0,-1.E0,0.E0));

+#2054=DIRECTION('',(1.E0,0.E0,0.E0));

+#2055=AXIS2_PLACEMENT_3D('',#2052,#2053,#2054);

+#2056=PLANE('',#2055);

+#2058=ORIENTED_EDGE('',*,*,#2057,.F.);

+#2060=ORIENTED_EDGE('',*,*,#2059,.F.);

+#2062=ORIENTED_EDGE('',*,*,#2061,.F.);

+#2064=ORIENTED_EDGE('',*,*,#2063,.F.);

+#2066=ORIENTED_EDGE('',*,*,#2065,.F.);

+#2068=ORIENTED_EDGE('',*,*,#2067,.F.);

+#2069=ORIENTED_EDGE('',*,*,#1976,.F.);

+#2071=ORIENTED_EDGE('',*,*,#2070,.T.);

+#2073=ORIENTED_EDGE('',*,*,#2072,.T.);

+#2075=ORIENTED_EDGE('',*,*,#2074,.F.);

+#2077=ORIENTED_EDGE('',*,*,#2076,.F.);

+#2079=ORIENTED_EDGE('',*,*,#2078,.F.);

+#2081=ORIENTED_EDGE('',*,*,#2080,.F.);

+#2083=ORIENTED_EDGE('',*,*,#2082,.F.);

+#2084=EDGE_LOOP('',(#2058,#2060,#2062,#2064,#2066,#2068,#2069,#2071,#2073,#2075,

+#2077,#2079,#2081,#2083));

+#2085=FACE_OUTER_BOUND('',#2084,.F.);

+#2086=ADVANCED_FACE('',(#2085),#2056,.T.);

+#2087=CARTESIAN_POINT('',(-1.65E-1,-1.E-1,-2.25E-1));

+#2088=DIRECTION('',(0.E0,-1.E0,0.E0));

+#2089=DIRECTION('',(1.E0,0.E0,0.E0));

+#2090=AXIS2_PLACEMENT_3D('',#2087,#2088,#2089);

+#2091=PLANE('',#2090);

+#2093=ORIENTED_EDGE('',*,*,#2092,.T.);

+#2095=ORIENTED_EDGE('',*,*,#2094,.T.);

+#2097=ORIENTED_EDGE('',*,*,#2096,.T.);

+#2099=ORIENTED_EDGE('',*,*,#2098,.F.);

+#2101=ORIENTED_EDGE('',*,*,#2100,.T.);

+#2103=ORIENTED_EDGE('',*,*,#2102,.F.);

+#2105=ORIENTED_EDGE('',*,*,#2104,.T.);

+#2107=ORIENTED_EDGE('',*,*,#2106,.T.);

+#2109=ORIENTED_EDGE('',*,*,#2108,.T.);

+#2111=ORIENTED_EDGE('',*,*,#2110,.T.);

+#2113=ORIENTED_EDGE('',*,*,#2112,.T.);

+#2115=ORIENTED_EDGE('',*,*,#2114,.F.);

+#2116=ORIENTED_EDGE('',*,*,#1982,.F.);

+#2118=ORIENTED_EDGE('',*,*,#2117,.T.);

+#2120=ORIENTED_EDGE('',*,*,#2119,.F.);

+#2122=ORIENTED_EDGE('',*,*,#2121,.F.);

+#2124=ORIENTED_EDGE('',*,*,#2123,.F.);

+#2126=ORIENTED_EDGE('',*,*,#2125,.F.);

+#2128=ORIENTED_EDGE('',*,*,#2127,.F.);

+#2130=ORIENTED_EDGE('',*,*,#2129,.T.);

+#2131=EDGE_LOOP('',(#2093,#2095,#2097,#2099,#2101,#2103,#2105,#2107,#2109,#2111,

+#2113,#2115,#2116,#2118,#2120,#2122,#2124,#2126,#2128,#2130));

+#2132=FACE_OUTER_BOUND('',#2131,.F.);

+#2133=ADVANCED_FACE('',(#2132),#2091,.T.);

+#2134=CARTESIAN_POINT('',(1.06E-1,-1.E-1,-5.5E-3));

+#2135=DIRECTION('',(8.660254037845E-1,0.E0,-5.E-1));

+#2136=DIRECTION('',(5.E-1,0.E0,8.660254037845E-1));

+#2137=AXIS2_PLACEMENT_3D('',#2134,#2135,#2136);

+#2138=PLANE('',#2137);

+#2140=ORIENTED_EDGE('',*,*,#2139,.F.);

+#2142=ORIENTED_EDGE('',*,*,#2141,.F.);

+#2144=ORIENTED_EDGE('',*,*,#2143,.F.);

+#2145=ORIENTED_EDGE('',*,*,#2022,.F.);

+#2147=ORIENTED_EDGE('',*,*,#2146,.T.);

+#2148=EDGE_LOOP('',(#2140,#2142,#2144,#2145,#2147));

+#2149=FACE_OUTER_BOUND('',#2148,.F.);

+#2150=ADVANCED_FACE('',(#2149),#2138,.T.);

+#2151=CARTESIAN_POINT('',(9.434827557301E-2,-1.767206638076E-1,4.63E-2));

+#2152=DIRECTION('',(0.E0,1.E0,0.E0));

+#2153=DIRECTION('',(1.E0,0.E0,0.E0));

+#2154=AXIS2_PLACEMENT_3D('',#2151,#2152,#2153);

+#2155=CONICAL_SURFACE('',#2154,3.427858267256E-2,5.0477E1);

+#2156=ORIENTED_EDGE('',*,*,#2139,.T.);

+#2158=ORIENTED_EDGE('',*,*,#2157,.T.);

+#2160=ORIENTED_EDGE('',*,*,#2159,.T.);

+#2162=ORIENTED_EDGE('',*,*,#2161,.T.);

+#2164=ORIENTED_EDGE('',*,*,#2163,.T.);

+#2166=ORIENTED_EDGE('',*,*,#2165,.T.);

+#2168=ORIENTED_EDGE('',*,*,#2167,.F.);

+#2170=ORIENTED_EDGE('',*,*,#2169,.F.);

+#2171=EDGE_LOOP('',(#2156,#2158,#2160,#2162,#2164,#2166,#2168,#2170));

+#2172=FACE_OUTER_BOUND('',#2171,.F.);

+#2173=ADVANCED_FACE('',(#2172),#2155,.T.);

+#2174=CARTESIAN_POINT('',(1.06E-1,-1.E-1,-8.3E-3));

+#2175=DIRECTION('',(1.E0,0.E0,0.E0));

+#2176=DIRECTION('',(0.E0,0.E0,1.E0));

+#2177=AXIS2_PLACEMENT_3D('',#2174,#2175,#2176);

+#2178=PLANE('',#2177);

+#2179=ORIENTED_EDGE('',*,*,#2157,.F.);

+#2180=ORIENTED_EDGE('',*,*,#2146,.F.);

+#2181=ORIENTED_EDGE('',*,*,#2047,.F.);

+#2183=ORIENTED_EDGE('',*,*,#2182,.T.);

+#2184=EDGE_LOOP('',(#2179,#2180,#2181,#2183));

+#2185=FACE_OUTER_BOUND('',#2184,.F.);

+#2186=ADVANCED_FACE('',(#2185),#2178,.T.);

+#2187=CARTESIAN_POINT('',(1.04E-1,-1.E-1,-8.3E-3));

+#2188=DIRECTION('',(0.E0,0.E0,-1.E0));

+#2189=DIRECTION('',(1.E0,0.E0,0.E0));

+#2190=AXIS2_PLACEMENT_3D('',#2187,#2188,#2189);

+#2191=PLANE('',#2190);

+#2192=ORIENTED_EDGE('',*,*,#2159,.F.);

+#2193=ORIENTED_EDGE('',*,*,#2182,.F.);

+#2194=ORIENTED_EDGE('',*,*,#2045,.F.);

+#2196=ORIENTED_EDGE('',*,*,#2195,.T.);

+#2197=EDGE_LOOP('',(#2192,#2193,#2194,#2196));

+#2198=FACE_OUTER_BOUND('',#2197,.F.);

+#2199=ADVANCED_FACE('',(#2198),#2191,.T.);

+#2200=CARTESIAN_POINT('',(1.04E-1,-1.E-1,-5.5E-3));

+#2201=DIRECTION('',(-1.E0,0.E0,0.E0));

+#2202=DIRECTION('',(0.E0,0.E0,-1.E0));

+#2203=AXIS2_PLACEMENT_3D('',#2200,#2201,#2202);

+#2204=PLANE('',#2203);

+#2205=ORIENTED_EDGE('',*,*,#2161,.F.);

+#2206=ORIENTED_EDGE('',*,*,#2195,.F.);

+#2207=ORIENTED_EDGE('',*,*,#2043,.F.);

+#2209=ORIENTED_EDGE('',*,*,#2208,.T.);

+#2210=EDGE_LOOP('',(#2205,#2206,#2207,#2209));

+#2211=FACE_OUTER_BOUND('',#2210,.F.);

+#2212=ADVANCED_FACE('',(#2211),#2204,.T.);

+#2213=CARTESIAN_POINT('',(5.257659555759E-2,-1.E-1,8.356794919243E-2));

+#2214=DIRECTION('',(-8.660254037845E-1,0.E0,-5.E-1));

+#2215=DIRECTION('',(5.E-1,0.E0,-8.660254037845E-1));

+#2216=AXIS2_PLACEMENT_3D('',#2213,#2214,#2215);

+#2217=PLANE('',#2216);

+#2218=ORIENTED_EDGE('',*,*,#2163,.F.);

+#2219=ORIENTED_EDGE('',*,*,#2208,.F.);

+#2220=ORIENTED_EDGE('',*,*,#2041,.F.);

+#2222=ORIENTED_EDGE('',*,*,#2221,.F.);

+#2224=ORIENTED_EDGE('',*,*,#2223,.F.);

+#2225=ORIENTED_EDGE('',*,*,#2112,.F.);

+#2227=ORIENTED_EDGE('',*,*,#2226,.T.);

+#2229=ORIENTED_EDGE('',*,*,#2228,.F.);

+#2230=EDGE_LOOP('',(#2218,#2219,#2220,#2222,#2224,#2225,#2227,#2229));

+#2231=FACE_OUTER_BOUND('',#2230,.F.);

+#2232=ADVANCED_FACE('',(#2231),#2217,.T.);

+#2233=CARTESIAN_POINT('',(8.E-2,-1.15E-1,-2.25E-1));

+#2234=DIRECTION('',(7.071067811865E-1,-7.071067811865E-1,0.E0));

+#2235=DIRECTION('',(7.071067811865E-1,7.071067811865E-1,0.E0));

+#2236=AXIS2_PLACEMENT_3D('',#2233,#2234,#2235);

+#2237=PLANE('',#2236);

+#2239=ORIENTED_EDGE('',*,*,#2238,.F.);

+#2241=ORIENTED_EDGE('',*,*,#2240,.T.);

+#2243=ORIENTED_EDGE('',*,*,#2242,.T.);

+#2244=ORIENTED_EDGE('',*,*,#2032,.F.);

+#2245=EDGE_LOOP('',(#2239,#2241,#2243,#2244));

+#2246=FACE_OUTER_BOUND('',#2245,.F.);

+#2247=ADVANCED_FACE('',(#2246),#2237,.T.);

+#2248=CARTESIAN_POINT('',(8.E-2,-1.15E-1,-2.25E-1));

+#2249=DIRECTION('',(7.071067811865E-1,-7.071067811865E-1,0.E0));

+#2250=DIRECTION('',(7.071067811865E-1,7.071067811865E-1,0.E0));

+#2251=AXIS2_PLACEMENT_3D('',#2248,#2249,#2250);

+#2252=PLANE('',#2251);

+#2253=ORIENTED_EDGE('',*,*,#2221,.T.);

+#2254=ORIENTED_EDGE('',*,*,#2039,.F.);

+#2255=ORIENTED_EDGE('',*,*,#1986,.F.);

+#2257=ORIENTED_EDGE('',*,*,#2256,.T.);

+#2258=EDGE_LOOP('',(#2253,#2254,#2255,#2257));

+#2259=FACE_OUTER_BOUND('',#2258,.F.);

+#2260=ADVANCED_FACE('',(#2259),#2252,.T.);

+#2261=CARTESIAN_POINT('',(1.564234044424E-1,-1.E-1,8.53E-2));

+#2262=DIRECTION('',(0.E0,0.E0,1.E0));

+#2263=DIRECTION('',(-1.E0,0.E0,0.E0));

+#2264=AXIS2_PLACEMENT_3D('',#2261,#2262,#2263);

+#2265=PLANE('',#2264);

+#2267=ORIENTED_EDGE('',*,*,#2266,.F.);

+#2269=ORIENTED_EDGE('',*,*,#2268,.F.);

+#2270=ORIENTED_EDGE('',*,*,#2104,.F.);

+#2272=ORIENTED_EDGE('',*,*,#2271,.T.);

+#2273=ORIENTED_EDGE('',*,*,#2238,.T.);

+#2274=ORIENTED_EDGE('',*,*,#2030,.F.);

+#2276=ORIENTED_EDGE('',*,*,#2275,.T.);

+#2277=EDGE_LOOP('',(#2267,#2269,#2270,#2272,#2273,#2274,#2276));

+#2278=FACE_OUTER_BOUND('',#2277,.F.);

+#2279=ADVANCED_FACE('',(#2278),#2265,.T.);

+#2280=CARTESIAN_POINT('',(9.434827557301E-2,-1.683556823861E-1,4.63E-2));

+#2281=DIRECTION('',(0.E0,1.E0,0.E0));

+#2282=DIRECTION('',(1.E0,0.E0,0.E0));

+#2283=AXIS2_PLACEMENT_3D('',#2280,#2281,#2282);

+#2284=CONICAL_SURFACE('',#2283,4.441782116695E-2,5.0477E1);

+#2285=ORIENTED_EDGE('',*,*,#2141,.T.);

+#2286=ORIENTED_EDGE('',*,*,#2169,.T.);

+#2288=ORIENTED_EDGE('',*,*,#2287,.T.);

+#2289=ORIENTED_EDGE('',*,*,#2165,.F.);

+#2290=ORIENTED_EDGE('',*,*,#2228,.T.);

+#2292=ORIENTED_EDGE('',*,*,#2291,.T.);

+#2294=ORIENTED_EDGE('',*,*,#2293,.T.);

+#2296=ORIENTED_EDGE('',*,*,#2295,.T.);

+#2297=ORIENTED_EDGE('',*,*,#2266,.T.);

+#2299=ORIENTED_EDGE('',*,*,#2298,.T.);

+#2301=ORIENTED_EDGE('',*,*,#2300,.T.);

+#2303=ORIENTED_EDGE('',*,*,#2302,.T.);

+#2304=EDGE_LOOP('',(#2285,#2286,#2288,#2289,#2290,#2292,#2294,#2296,#2297,#2299,

+#2301,#2303));

+#2305=FACE_OUTER_BOUND('',#2304,.F.);

+#2306=ADVANCED_FACE('',(#2305),#2284,.T.);

+#2307=CARTESIAN_POINT('',(9.434827557301E-2,-1.790729432769E-1,4.63E-2));

+#2308=DIRECTION('',(0.E0,1.E0,0.E0));

+#2309=DIRECTION('',(1.E0,0.E0,0.E0));

+#2310=AXIS2_PLACEMENT_3D('',#2307,#2308,#2309);

+#2311=SPHERICAL_SURFACE('',#2310,2.E-2);

+#2313=ORIENTED_EDGE('',*,*,#2312,.T.);

+#2315=ORIENTED_EDGE('',*,*,#2314,.F.);

+#2316=ORIENTED_EDGE('',*,*,#2287,.F.);

+#2317=EDGE_LOOP('',(#2313,#2315,#2316));

+#2318=FACE_OUTER_BOUND('',#2317,.F.);

+#2319=ADVANCED_FACE('',(#2318),#2311,.T.);

+#2320=CARTESIAN_POINT('',(9.434827557301E-2,-1.790729432769E-1,4.63E-2));

+#2321=DIRECTION('',(0.E0,1.E0,0.E0));

+#2322=DIRECTION('',(1.E0,0.E0,0.E0));

+#2323=AXIS2_PLACEMENT_3D('',#2320,#2321,#2322);

+#2324=SPHERICAL_SURFACE('',#2323,2.E-2);

+#2325=ORIENTED_EDGE('',*,*,#2312,.F.);

+#2326=ORIENTED_EDGE('',*,*,#2167,.T.);

+#2327=ORIENTED_EDGE('',*,*,#2314,.T.);

+#2328=EDGE_LOOP('',(#2325,#2326,#2327));

+#2329=FACE_OUTER_BOUND('',#2328,.F.);

+#2330=ADVANCED_FACE('',(#2329),#2324,.T.);

+#2331=CARTESIAN_POINT('',(5.015172442699E-2,-1.E-1,8.496794919243E-2));

+#2332=DIRECTION('',(-5.E-1,0.E0,-8.660254037845E-1));

+#2333=DIRECTION('',(8.660254037845E-1,0.E0,-5.E-1));

+#2334=AXIS2_PLACEMENT_3D('',#2331,#2332,#2333);

+#2335=PLANE('',#2334);

+#2336=ORIENTED_EDGE('',*,*,#2291,.F.);

+#2337=ORIENTED_EDGE('',*,*,#2226,.F.);

+#2338=ORIENTED_EDGE('',*,*,#2110,.F.);

+#2340=ORIENTED_EDGE('',*,*,#2339,.T.);

+#2341=EDGE_LOOP('',(#2336,#2337,#2338,#2340));

+#2342=FACE_OUTER_BOUND('',#2341,.F.);

+#2343=ADVANCED_FACE('',(#2342),#2335,.T.);

+#2344=CARTESIAN_POINT('',(5.115172442699E-2,-1.E-1,8.67E-2));

+#2345=DIRECTION('',(-8.660254037845E-1,0.E0,5.E-1));

+#2346=DIRECTION('',(-5.E-1,0.E0,-8.660254037845E-1));

+#2347=AXIS2_PLACEMENT_3D('',#2344,#2345,#2346);

+#2348=PLANE('',#2347);

+#2349=ORIENTED_EDGE('',*,*,#2293,.F.);

+#2350=ORIENTED_EDGE('',*,*,#2339,.F.);

+#2351=ORIENTED_EDGE('',*,*,#2108,.F.);

+#2353=ORIENTED_EDGE('',*,*,#2352,.T.);

+#2354=EDGE_LOOP('',(#2349,#2350,#2351,#2353));

+#2355=FACE_OUTER_BOUND('',#2354,.F.);

+#2356=ADVANCED_FACE('',(#2355),#2348,.T.);

+#2357=CARTESIAN_POINT('',(5.357659555759E-2,-1.E-1,8.53E-2));

+#2358=DIRECTION('',(5.E-1,0.E0,8.660254037844E-1));

+#2359=DIRECTION('',(-8.660254037844E-1,0.E0,5.E-1));

+#2360=AXIS2_PLACEMENT_3D('',#2357,#2358,#2359);

+#2361=PLANE('',#2360);

+#2362=ORIENTED_EDGE('',*,*,#2295,.F.);

+#2363=ORIENTED_EDGE('',*,*,#2352,.F.);

+#2364=ORIENTED_EDGE('',*,*,#2106,.F.);

+#2365=ORIENTED_EDGE('',*,*,#2268,.T.);

+#2366=EDGE_LOOP('',(#2362,#2363,#2364,#2365));

+#2367=FACE_OUTER_BOUND('',#2366,.F.);

+#2368=ADVANCED_FACE('',(#2367),#2361,.T.);

+#2369=CARTESIAN_POINT('',(1.588482755730E-1,-1.E-1,8.67E-2));

+#2370=DIRECTION('',(-5.E-1,0.E0,8.660254037844E-1));

+#2371=DIRECTION('',(-8.660254037844E-1,0.E0,-5.E-1));

+#2372=AXIS2_PLACEMENT_3D('',#2369,#2370,#2371);

+#2373=PLANE('',#2372);

+#2374=ORIENTED_EDGE('',*,*,#2298,.F.);

+#2375=ORIENTED_EDGE('',*,*,#2275,.F.);

+#2376=ORIENTED_EDGE('',*,*,#2028,.F.);

+#2378=ORIENTED_EDGE('',*,*,#2377,.T.);

+#2379=EDGE_LOOP('',(#2374,#2375,#2376,#2378));

+#2380=FACE_OUTER_BOUND('',#2379,.F.);

+#2381=ADVANCED_FACE('',(#2380),#2373,.T.);

+#2382=CARTESIAN_POINT('',(1.598482755730E-1,-1.E-1,8.496794919243E-2));

+#2383=DIRECTION('',(8.660254037844E-1,0.E0,5.E-1));

+#2384=DIRECTION('',(-5.E-1,0.E0,8.660254037844E-1));

+#2385=AXIS2_PLACEMENT_3D('',#2382,#2383,#2384);

+#2386=PLANE('',#2385);

+#2387=ORIENTED_EDGE('',*,*,#2300,.F.);

+#2388=ORIENTED_EDGE('',*,*,#2377,.F.);

+#2389=ORIENTED_EDGE('',*,*,#2026,.F.);

+#2391=ORIENTED_EDGE('',*,*,#2390,.T.);

+#2392=EDGE_LOOP('',(#2387,#2388,#2389,#2391));

+#2393=FACE_OUTER_BOUND('',#2392,.F.);

+#2394=ADVANCED_FACE('',(#2393),#2386,.T.);

+#2395=CARTESIAN_POINT('',(1.574234044424E-1,-1.E-1,8.356794919243E-2));

+#2396=DIRECTION('',(5.E-1,0.E0,-8.660254037844E-1));

+#2397=DIRECTION('',(8.660254037844E-1,0.E0,5.E-1));

+#2398=AXIS2_PLACEMENT_3D('',#2395,#2396,#2397);

+#2399=PLANE('',#2398);

+#2400=ORIENTED_EDGE('',*,*,#2302,.F.);

+#2401=ORIENTED_EDGE('',*,*,#2390,.F.);

+#2402=ORIENTED_EDGE('',*,*,#2024,.F.);

+#2403=ORIENTED_EDGE('',*,*,#2143,.T.);

+#2404=EDGE_LOOP('',(#2400,#2401,#2402,#2403));

+#2405=FACE_OUTER_BOUND('',#2404,.F.);

+#2406=ADVANCED_FACE('',(#2405),#2399,.T.);

+#2407=CARTESIAN_POINT('',(6.5E-2,-1.E-1,-2.25E-1));

+#2408=DIRECTION('',(-7.071067811865E-1,-7.071067811865E-1,0.E0));

+#2409=DIRECTION('',(7.071067811865E-1,-7.071067811865E-1,0.E0));

+#2410=AXIS2_PLACEMENT_3D('',#2407,#2408,#2409);

+#2411=PLANE('',#2410);

+#2412=ORIENTED_EDGE('',*,*,#2271,.F.);

+#2413=ORIENTED_EDGE('',*,*,#2102,.T.);

+#2415=ORIENTED_EDGE('',*,*,#2414,.T.);

+#2416=ORIENTED_EDGE('',*,*,#2240,.F.);

+#2417=EDGE_LOOP('',(#2412,#2413,#2415,#2416));

+#2418=FACE_OUTER_BOUND('',#2417,.F.);

+#2419=ADVANCED_FACE('',(#2418),#2411,.T.);

+#2420=CARTESIAN_POINT('',(6.5E-2,-1.E-1,-2.25E-1));

+#2421=DIRECTION('',(-7.071067811865E-1,-7.071067811865E-1,0.E0));

+#2422=DIRECTION('',(7.071067811865E-1,-7.071067811865E-1,0.E0));

+#2423=AXIS2_PLACEMENT_3D('',#2420,#2421,#2422);

+#2424=PLANE('',#2423);

+#2425=ORIENTED_EDGE('',*,*,#2223,.T.);

+#2426=ORIENTED_EDGE('',*,*,#2256,.F.);

+#2427=ORIENTED_EDGE('',*,*,#1984,.F.);

+#2428=ORIENTED_EDGE('',*,*,#2114,.T.);

+#2429=EDGE_LOOP('',(#2425,#2426,#2427,#2428));

+#2430=FACE_OUTER_BOUND('',#2429,.F.);

+#2431=ADVANCED_FACE('',(#2430),#2424,.T.);

+#2432=CARTESIAN_POINT('',(0.E0,0.E0,2.25E-1));

+#2433=DIRECTION('',(0.E0,0.E0,1.E0));

+#2434=DIRECTION('',(1.E0,0.E0,0.E0));

+#2435=AXIS2_PLACEMENT_3D('',#2432,#2433,#2434);

+#2436=PLANE('',#2435);

+#2437=ORIENTED_EDGE('',*,*,#2092,.F.);

+#2439=ORIENTED_EDGE('',*,*,#2438,.F.);

+#2441=ORIENTED_EDGE('',*,*,#2440,.F.);

+#2442=ORIENTED_EDGE('',*,*,#2072,.F.);

+#2444=ORIENTED_EDGE('',*,*,#2443,.F.);

+#2446=ORIENTED_EDGE('',*,*,#2445,.F.);

+#2448=ORIENTED_EDGE('',*,*,#2447,.F.);

+#2450=ORIENTED_EDGE('',*,*,#2449,.F.);

+#2452=ORIENTED_EDGE('',*,*,#2451,.F.);

+#2454=ORIENTED_EDGE('',*,*,#2453,.F.);

+#2456=ORIENTED_EDGE('',*,*,#2455,.F.);

+#2457=ORIENTED_EDGE('',*,*,#2034,.F.);

+#2458=ORIENTED_EDGE('',*,*,#2242,.F.);

+#2459=ORIENTED_EDGE('',*,*,#2414,.F.);

+#2460=ORIENTED_EDGE('',*,*,#2100,.F.);

+#2462=ORIENTED_EDGE('',*,*,#2461,.T.);

+#2464=ORIENTED_EDGE('',*,*,#2463,.T.);

+#2466=ORIENTED_EDGE('',*,*,#2465,.T.);

+#2467=EDGE_LOOP('',(#2437,#2439,#2441,#2442,#2444,#2446,#2448,#2450,#2452,#2454,

+#2456,#2457,#2458,#2459,#2460,#2462,#2464,#2466));

+#2468=FACE_OUTER_BOUND('',#2467,.F.);

+#2470=ORIENTED_EDGE('',*,*,#2469,.T.);

+#2472=ORIENTED_EDGE('',*,*,#2471,.T.);

+#2474=ORIENTED_EDGE('',*,*,#2473,.T.);

+#2476=ORIENTED_EDGE('',*,*,#2475,.T.);

+#2477=EDGE_LOOP('',(#2470,#2472,#2474,#2476));

+#2478=FACE_BOUND('',#2477,.F.);

+#2479=ADVANCED_FACE('',(#2468,#2478),#2436,.T.);

+#2480=CARTESIAN_POINT('',(1.25E-1,-5.5E-2,2.25E-1));

+#2481=DIRECTION('',(-7.071067811865E-1,0.E0,7.071067811865E-1));

+#2482=DIRECTION('',(0.E0,1.E0,0.E0));

+#2483=AXIS2_PLACEMENT_3D('',#2480,#2481,#2482);

+#2484=PLANE('',#2483);

+#2485=ORIENTED_EDGE('',*,*,#2469,.F.);

+#2487=ORIENTED_EDGE('',*,*,#2486,.T.);

+#2489=ORIENTED_EDGE('',*,*,#2488,.T.);

+#2491=ORIENTED_EDGE('',*,*,#2490,.F.);

+#2492=EDGE_LOOP('',(#2485,#2487,#2489,#2491));

+#2493=FACE_OUTER_BOUND('',#2492,.F.);

+#2494=ADVANCED_FACE('',(#2493),#2484,.T.);

+#2495=CARTESIAN_POINT('',(-1.05E-1,-5.5E-2,2.25E-1));

+#2496=DIRECTION('',(0.E0,-1.E0,0.E0));

+#2497=DIRECTION('',(1.E0,0.E0,0.E0));

+#2498=AXIS2_PLACEMENT_3D('',#2495,#2496,#2497);

+#2499=PLANE('',#2498);

+#2501=ORIENTED_EDGE('',*,*,#2500,.F.);

+#2503=ORIENTED_EDGE('',*,*,#2502,.T.);

+#2505=ORIENTED_EDGE('',*,*,#2504,.F.);

+#2506=ORIENTED_EDGE('',*,*,#2486,.F.);

+#2507=ORIENTED_EDGE('',*,*,#2475,.F.);

+#2509=ORIENTED_EDGE('',*,*,#2508,.T.);

+#2510=EDGE_LOOP('',(#2501,#2503,#2505,#2506,#2507,#2509));

+#2511=FACE_OUTER_BOUND('',#2510,.F.);

+#2512=ADVANCED_FACE('',(#2511),#2499,.F.);

+#2513=CARTESIAN_POINT('',(-1.05E-1,1.15E-1,2.25E-1));

+#2514=DIRECTION('',(-1.E0,0.E0,0.E0));

+#2515=DIRECTION('',(0.E0,-1.E0,0.E0));

+#2516=AXIS2_PLACEMENT_3D('',#2513,#2514,#2515);

+#2517=PLANE('',#2516);

+#2519=ORIENTED_EDGE('',*,*,#2518,.T.);

+#2521=ORIENTED_EDGE('',*,*,#2520,.T.);

+#2523=ORIENTED_EDGE('',*,*,#2522,.T.);

+#2525=ORIENTED_EDGE('',*,*,#2524,.T.);

+#2526=ORIENTED_EDGE('',*,*,#2500,.T.);

+#2528=ORIENTED_EDGE('',*,*,#2527,.F.);

+#2530=ORIENTED_EDGE('',*,*,#2529,.T.);

+#2532=ORIENTED_EDGE('',*,*,#2531,.T.);

+#2533=EDGE_LOOP('',(#2519,#2521,#2523,#2525,#2526,#2528,#2530,#2532));

+#2534=FACE_OUTER_BOUND('',#2533,.F.);

+#2535=ADVANCED_FACE('',(#2534),#2517,.F.);

+#2536=CARTESIAN_POINT('',(1.98E-1,1.6E-1,7.5E-2));

+#2537=DIRECTION('',(0.E0,0.E0,1.E0));

+#2538=DIRECTION('',(0.E0,-1.E0,0.E0));

+#2539=AXIS2_PLACEMENT_3D('',#2536,#2537,#2538);

+#2540=PLANE('',#2539);

+#2542=ORIENTED_EDGE('',*,*,#2541,.T.);

+#2544=ORIENTED_EDGE('',*,*,#2543,.T.);

+#2545=ORIENTED_EDGE('',*,*,#2518,.F.);

+#2547=ORIENTED_EDGE('',*,*,#2546,.T.);

+#2548=EDGE_LOOP('',(#2542,#2544,#2545,#2547));

+#2549=FACE_OUTER_BOUND('',#2548,.F.);

+#2550=ADVANCED_FACE('',(#2549),#2540,.T.);

+#2551=CARTESIAN_POINT('',(1.98E-1,1.6E-1,7.5E-2));

+#2552=DIRECTION('',(0.E0,0.E0,1.E0));

+#2553=DIRECTION('',(0.E0,-1.E0,0.E0));

+#2554=AXIS2_PLACEMENT_3D('',#2551,#2552,#2553);

+#2555=PLANE('',#2554);

+#2557=ORIENTED_EDGE('',*,*,#2556,.F.);

+#2559=ORIENTED_EDGE('',*,*,#2558,.F.);

+#2561=ORIENTED_EDGE('',*,*,#2560,.T.);

+#2563=ORIENTED_EDGE('',*,*,#2562,.F.);

+#2564=EDGE_LOOP('',(#2557,#2559,#2561,#2563));

+#2565=FACE_OUTER_BOUND('',#2564,.F.);

+#2566=ADVANCED_FACE('',(#2565),#2555,.T.);

+#2567=CARTESIAN_POINT('',(-6.E-2,1.15E-1,2.25E-1));

+#2568=DIRECTION('',(-1.E0,0.E0,0.E0));

+#2569=DIRECTION('',(0.E0,-1.E0,0.E0));

+#2570=AXIS2_PLACEMENT_3D('',#2567,#2568,#2569);

+#2571=PLANE('',#2570);

+#2573=ORIENTED_EDGE('',*,*,#2572,.T.);

+#2575=ORIENTED_EDGE('',*,*,#2574,.T.);

+#2577=ORIENTED_EDGE('',*,*,#2576,.F.);

+#2579=ORIENTED_EDGE('',*,*,#2578,.F.);

+#2580=ORIENTED_EDGE('',*,*,#2541,.F.);

+#2581=EDGE_LOOP('',(#2573,#2575,#2577,#2579,#2580));

+#2582=FACE_OUTER_BOUND('',#2581,.F.);

+#2583=ADVANCED_FACE('',(#2582),#2571,.F.);

+#2584=CARTESIAN_POINT('',(1.98E-1,1.E-1,7.5E-2));

+#2585=DIRECTION('',(0.E0,-1.E0,0.E0));

+#2586=DIRECTION('',(0.E0,0.E0,-1.E0));

+#2587=AXIS2_PLACEMENT_3D('',#2584,#2585,#2586);

+#2588=PLANE('',#2587);

+#2590=ORIENTED_EDGE('',*,*,#2589,.F.);

+#2591=ORIENTED_EDGE('',*,*,#2572,.F.);

+#2592=ORIENTED_EDGE('',*,*,#2546,.F.);

+#2594=ORIENTED_EDGE('',*,*,#2593,.T.);

+#2596=ORIENTED_EDGE('',*,*,#2595,.T.);

+#2597=ORIENTED_EDGE('',*,*,#1996,.F.);

+#2599=ORIENTED_EDGE('',*,*,#2598,.F.);

+#2601=ORIENTED_EDGE('',*,*,#2600,.T.);

+#2602=ORIENTED_EDGE('',*,*,#2560,.F.);

+#2604=ORIENTED_EDGE('',*,*,#2603,.T.);

+#2605=EDGE_LOOP('',(#2590,#2591,#2592,#2594,#2596,#2597,#2599,#2601,#2602,

+#2604));

+#2606=FACE_OUTER_BOUND('',#2605,.F.);

+#2607=ADVANCED_FACE('',(#2606),#2588,.F.);

+#2608=CARTESIAN_POINT('',(0.E0,0.E0,-1.25E-1));

+#2609=DIRECTION('',(0.E0,0.E0,1.E0));

+#2610=DIRECTION('',(1.E0,0.E0,0.E0));

+#2611=AXIS2_PLACEMENT_3D('',#2608,#2609,#2610);

+#2612=PLANE('',#2611);

+#2613=ORIENTED_EDGE('',*,*,#2589,.T.);

+#2615=ORIENTED_EDGE('',*,*,#2614,.F.);

+#2617=ORIENTED_EDGE('',*,*,#2616,.F.);

+#2619=ORIENTED_EDGE('',*,*,#2618,.F.);

+#2620=ORIENTED_EDGE('',*,*,#2502,.F.);

+#2621=ORIENTED_EDGE('',*,*,#2524,.F.);

+#2623=ORIENTED_EDGE('',*,*,#2622,.F.);

+#2624=ORIENTED_EDGE('',*,*,#2574,.F.);

+#2625=EDGE_LOOP('',(#2613,#2615,#2617,#2619,#2620,#2621,#2623,#2624));

+#2626=FACE_OUTER_BOUND('',#2625,.F.);

+#2628=ORIENTED_EDGE('',*,*,#2627,.F.);

+#2630=ORIENTED_EDGE('',*,*,#2629,.T.);

+#2632=ORIENTED_EDGE('',*,*,#2631,.T.);

+#2634=ORIENTED_EDGE('',*,*,#2633,.F.);

+#2635=EDGE_LOOP('',(#2628,#2630,#2632,#2634));

+#2636=FACE_BOUND('',#2635,.F.);

+#2638=ORIENTED_EDGE('',*,*,#2637,.F.);

+#2640=ORIENTED_EDGE('',*,*,#2639,.T.);

+#2642=ORIENTED_EDGE('',*,*,#2641,.T.);

+#2644=ORIENTED_EDGE('',*,*,#2643,.F.);

+#2645=EDGE_LOOP('',(#2638,#2640,#2642,#2644));

+#2646=FACE_BOUND('',#2645,.F.);

+#2647=ADVANCED_FACE('',(#2626,#2636,#2646),#2612,.T.);

+#2648=CARTESIAN_POINT('',(6.E-2,6.5E-2,2.25E-1));

+#2649=DIRECTION('',(1.E0,0.E0,0.E0));

+#2650=DIRECTION('',(0.E0,1.E0,0.E0));

+#2651=AXIS2_PLACEMENT_3D('',#2648,#2649,#2650);

+#2652=PLANE('',#2651);

+#2653=ORIENTED_EDGE('',*,*,#2603,.F.);

+#2654=ORIENTED_EDGE('',*,*,#2558,.T.);

+#2656=ORIENTED_EDGE('',*,*,#2655,.T.);

+#2658=ORIENTED_EDGE('',*,*,#2657,.T.);

+#2659=ORIENTED_EDGE('',*,*,#2614,.T.);

+#2660=EDGE_LOOP('',(#2653,#2654,#2656,#2658,#2659));

+#2661=FACE_OUTER_BOUND('',#2660,.F.);

+#2662=ADVANCED_FACE('',(#2661),#2652,.F.);

+#2663=CARTESIAN_POINT('',(6.E-2,8.5E-2,7.5E-2));

+#2664=DIRECTION('',(0.E0,7.071067811865E-1,-7.071067811865E-1));

+#2665=DIRECTION('',(1.E0,0.E0,0.E0));

+#2666=AXIS2_PLACEMENT_3D('',#2663,#2664,#2665);

+#2667=PLANE('',#2666);

+#2668=ORIENTED_EDGE('',*,*,#2556,.T.);

+#2670=ORIENTED_EDGE('',*,*,#2669,.T.);

+#2672=ORIENTED_EDGE('',*,*,#2671,.F.);

+#2673=ORIENTED_EDGE('',*,*,#2655,.F.);

+#2674=EDGE_LOOP('',(#2668,#2670,#2672,#2673));

+#2675=FACE_OUTER_BOUND('',#2674,.F.);

+#2676=ADVANCED_FACE('',(#2675),#2667,.F.);

+#2677=CARTESIAN_POINT('',(1.05E-1,-5.5E-2,2.25E-1));

+#2678=DIRECTION('',(1.E0,0.E0,0.E0));

+#2679=DIRECTION('',(0.E0,1.E0,0.E0));

+#2680=AXIS2_PLACEMENT_3D('',#2677,#2678,#2679);

+#2681=PLANE('',#2680);

+#2682=ORIENTED_EDGE('',*,*,#2504,.T.);

+#2683=ORIENTED_EDGE('',*,*,#2618,.T.);

+#2685=ORIENTED_EDGE('',*,*,#2684,.F.);

+#2686=ORIENTED_EDGE('',*,*,#2669,.F.);

+#2687=ORIENTED_EDGE('',*,*,#2562,.T.);

+#2689=ORIENTED_EDGE('',*,*,#2688,.T.);

+#2691=ORIENTED_EDGE('',*,*,#2690,.F.);

+#2692=ORIENTED_EDGE('',*,*,#2488,.F.);

+#2693=EDGE_LOOP('',(#2682,#2683,#2685,#2686,#2687,#2689,#2691,#2692));

+#2694=FACE_OUTER_BOUND('',#2693,.F.);

+#2695=ADVANCED_FACE('',(#2694),#2681,.F.);

+#2696=CARTESIAN_POINT('',(1.05E-1,6.5E-2,2.25E-1));

+#2697=DIRECTION('',(0.E0,1.E0,0.E0));

+#2698=DIRECTION('',(-1.E0,0.E0,0.E0));

+#2699=AXIS2_PLACEMENT_3D('',#2696,#2697,#2698);

+#2700=PLANE('',#2699);

+#2701=ORIENTED_EDGE('',*,*,#2671,.T.);

+#2702=ORIENTED_EDGE('',*,*,#2684,.T.);

+#2703=ORIENTED_EDGE('',*,*,#2616,.T.);

+#2704=ORIENTED_EDGE('',*,*,#2657,.F.);

+#2705=EDGE_LOOP('',(#2701,#2702,#2703,#2704));

+#2706=FACE_OUTER_BOUND('',#2705,.F.);

+#2707=ADVANCED_FACE('',(#2706),#2700,.F.);

+#2708=CARTESIAN_POINT('',(1.98E-1,1.6E-1,7.5E-2));

+#2709=DIRECTION('',(0.E0,0.E0,1.E0));

+#2710=DIRECTION('',(0.E0,-1.E0,0.E0));

+#2711=AXIS2_PLACEMENT_3D('',#2708,#2709,#2710);

+#2712=PLANE('',#2711);

+#2714=ORIENTED_EDGE('',*,*,#2713,.F.);

+#2716=ORIENTED_EDGE('',*,*,#2715,.F.);

+#2717=ORIENTED_EDGE('',*,*,#2688,.F.);

+#2718=ORIENTED_EDGE('',*,*,#2600,.F.);

+#2720=ORIENTED_EDGE('',*,*,#2719,.T.);

+#2722=ORIENTED_EDGE('',*,*,#2721,.T.);

+#2723=EDGE_LOOP('',(#2714,#2716,#2717,#2718,#2720,#2722));

+#2724=FACE_OUTER_BOUND('',#2723,.F.);

+#2725=ADVANCED_FACE('',(#2724),#2712,.F.);

+#2726=CARTESIAN_POINT('',(1.98E-1,1.6E-1,7.5E-2));

+#2727=DIRECTION('',(0.E0,0.E0,1.E0));

+#2728=DIRECTION('',(0.E0,-1.E0,0.E0));

+#2729=AXIS2_PLACEMENT_3D('',#2726,#2727,#2728);

+#2730=PLANE('',#2729);

+#2732=ORIENTED_EDGE('',*,*,#2731,.T.);

+#2734=ORIENTED_EDGE('',*,*,#2733,.T.);

+#2736=ORIENTED_EDGE('',*,*,#2735,.F.);

+#2737=ORIENTED_EDGE('',*,*,#2593,.F.);

+#2738=ORIENTED_EDGE('',*,*,#2531,.F.);

+#2740=ORIENTED_EDGE('',*,*,#2739,.F.);

+#2741=EDGE_LOOP('',(#2732,#2734,#2736,#2737,#2738,#2740));

+#2742=FACE_OUTER_BOUND('',#2741,.F.);

+#2743=ADVANCED_FACE('',(#2742),#2730,.F.);

+#2744=CARTESIAN_POINT('',(1.E-1,1.6E-1,7.5E-2));

+#2745=DIRECTION('',(1.E0,0.E0,0.E0));

+#2746=DIRECTION('',(0.E0,0.E0,1.E0));

+#2747=AXIS2_PLACEMENT_3D('',#2744,#2745,#2746);

+#2748=PLANE('',#2747);

+#2749=ORIENTED_EDGE('',*,*,#2713,.T.);

+#2751=ORIENTED_EDGE('',*,*,#2750,.T.);

+#2753=ORIENTED_EDGE('',*,*,#2752,.F.);

+#2755=ORIENTED_EDGE('',*,*,#2754,.F.);

+#2756=EDGE_LOOP('',(#2749,#2751,#2753,#2755));

+#2757=FACE_OUTER_BOUND('',#2756,.F.);

+#2758=ADVANCED_FACE('',(#2757),#2748,.F.);

+#2759=CARTESIAN_POINT('',(1.4E-1,1.6E-1,-2.25E-1));

+#2760=DIRECTION('',(0.E0,1.E0,0.E0));

+#2761=DIRECTION('',(-1.E0,0.E0,0.E0));

+#2762=AXIS2_PLACEMENT_3D('',#2759,#2760,#2761);

+#2763=PLANE('',#2762);

+#2764=ORIENTED_EDGE('',*,*,#2750,.F.);

+#2765=ORIENTED_EDGE('',*,*,#2721,.F.);

+#2767=ORIENTED_EDGE('',*,*,#2766,.T.);

+#2768=ORIENTED_EDGE('',*,*,#2449,.T.);

+#2770=ORIENTED_EDGE('',*,*,#2769,.F.);

+#2771=ORIENTED_EDGE('',*,*,#2733,.F.);

+#2773=ORIENTED_EDGE('',*,*,#2772,.T.);

+#2775=ORIENTED_EDGE('',*,*,#2774,.F.);

+#2777=ORIENTED_EDGE('',*,*,#2776,.F.);

+#2779=ORIENTED_EDGE('',*,*,#2778,.F.);

+#2780=EDGE_LOOP('',(#2764,#2765,#2767,#2768,#2770,#2771,#2773,#2775,#2777,

+#2779));

+#2781=FACE_OUTER_BOUND('',#2780,.F.);

+#2782=ADVANCED_FACE('',(#2781),#2763,.T.);

+#2783=CARTESIAN_POINT('',(1.4E-1,-5.E-2,-2.25E-1));

+#2784=DIRECTION('',(1.E0,0.E0,0.E0));

+#2785=DIRECTION('',(0.E0,1.E0,0.E0));

+#2786=AXIS2_PLACEMENT_3D('',#2783,#2784,#2785);

+#2787=PLANE('',#2786);

+#2788=ORIENTED_EDGE('',*,*,#2766,.F.);

+#2789=ORIENTED_EDGE('',*,*,#2719,.F.);

+#2790=ORIENTED_EDGE('',*,*,#2598,.T.);

+#2791=ORIENTED_EDGE('',*,*,#1994,.F.);

+#2793=ORIENTED_EDGE('',*,*,#2792,.T.);

+#2794=ORIENTED_EDGE('',*,*,#2451,.T.);

+#2795=EDGE_LOOP('',(#2788,#2789,#2790,#2791,#2793,#2794));

+#2796=FACE_OUTER_BOUND('',#2795,.F.);

+#2797=ADVANCED_FACE('',(#2796),#2787,.T.);

+#2798=CARTESIAN_POINT('',(1.65E-1,-5.E-2,-2.25E-1));

+#2799=DIRECTION('',(0.E0,1.E0,0.E0));

+#2800=DIRECTION('',(-1.E0,0.E0,0.E0));

+#2801=AXIS2_PLACEMENT_3D('',#2798,#2799,#2800);

+#2802=PLANE('',#2801);

+#2803=ORIENTED_EDGE('',*,*,#1992,.F.);

+#2805=ORIENTED_EDGE('',*,*,#2804,.T.);

+#2806=ORIENTED_EDGE('',*,*,#2453,.T.);

+#2807=ORIENTED_EDGE('',*,*,#2792,.F.);

+#2808=EDGE_LOOP('',(#2803,#2805,#2806,#2807));

+#2809=FACE_OUTER_BOUND('',#2808,.F.);

+#2810=ADVANCED_FACE('',(#2809),#2802,.T.);

+#2811=CARTESIAN_POINT('',(1.65E-1,-1.E-1,-2.25E-1));

+#2812=DIRECTION('',(1.E0,0.E0,0.E0));

+#2813=DIRECTION('',(0.E0,1.E0,0.E0));

+#2814=AXIS2_PLACEMENT_3D('',#2811,#2812,#2813);

+#2815=PLANE('',#2814);

+#2816=ORIENTED_EDGE('',*,*,#1990,.F.);

+#2817=ORIENTED_EDGE('',*,*,#2036,.T.);

+#2818=ORIENTED_EDGE('',*,*,#2455,.T.);

+#2819=ORIENTED_EDGE('',*,*,#2804,.F.);

+#2820=EDGE_LOOP('',(#2816,#2817,#2818,#2819));

+#2821=FACE_OUTER_BOUND('',#2820,.F.);

+#2822=ADVANCED_FACE('',(#2821),#2815,.T.);

+#2823=CARTESIAN_POINT('',(-1.4E-1,1.6E-1,-2.25E-1));

+#2824=DIRECTION('',(-1.E0,0.E0,0.E0));

+#2825=DIRECTION('',(0.E0,-1.E0,0.E0));

+#2826=AXIS2_PLACEMENT_3D('',#2823,#2824,#2825);

+#2827=PLANE('',#2826);

+#2828=ORIENTED_EDGE('',*,*,#2769,.T.);

+#2829=ORIENTED_EDGE('',*,*,#2447,.T.);

+#2831=ORIENTED_EDGE('',*,*,#2830,.F.);

+#2832=ORIENTED_EDGE('',*,*,#1998,.F.);

+#2833=ORIENTED_EDGE('',*,*,#2595,.F.);

+#2834=ORIENTED_EDGE('',*,*,#2735,.T.);

+#2835=EDGE_LOOP('',(#2828,#2829,#2831,#2832,#2833,#2834));

+#2836=FACE_OUTER_BOUND('',#2835,.F.);

+#2837=ADVANCED_FACE('',(#2836),#2827,.T.);

+#2838=CARTESIAN_POINT('',(-1.4E-1,-5.E-2,-2.25E-1));

+#2839=DIRECTION('',(0.E0,1.E0,0.E0));

+#2840=DIRECTION('',(-1.E0,0.E0,0.E0));

+#2841=AXIS2_PLACEMENT_3D('',#2838,#2839,#2840);

+#2842=PLANE('',#2841);

+#2843=ORIENTED_EDGE('',*,*,#2000,.F.);

+#2844=ORIENTED_EDGE('',*,*,#2830,.T.);

+#2845=ORIENTED_EDGE('',*,*,#2445,.T.);

+#2847=ORIENTED_EDGE('',*,*,#2846,.F.);

+#2848=EDGE_LOOP('',(#2843,#2844,#2845,#2847));

+#2849=FACE_OUTER_BOUND('',#2848,.F.);

+#2850=ADVANCED_FACE('',(#2849),#2842,.T.);

+#2851=CARTESIAN_POINT('',(-1.65E-1,-5.E-2,-2.25E-1));

+#2852=DIRECTION('',(-1.E0,0.E0,0.E0));

+#2853=DIRECTION('',(0.E0,-1.E0,0.E0));

+#2854=AXIS2_PLACEMENT_3D('',#2851,#2852,#2853);

+#2855=PLANE('',#2854);

+#2856=ORIENTED_EDGE('',*,*,#2002,.F.);

+#2857=ORIENTED_EDGE('',*,*,#2846,.T.);

+#2858=ORIENTED_EDGE('',*,*,#2443,.T.);

+#2859=ORIENTED_EDGE('',*,*,#2070,.F.);

+#2860=EDGE_LOOP('',(#2856,#2857,#2858,#2859));

+#2861=FACE_OUTER_BOUND('',#2860,.F.);

+#2862=ADVANCED_FACE('',(#2861),#2855,.T.);

+#2863=CARTESIAN_POINT('',(-1.E-1,1.6E-1,1.12E-1));

+#2864=DIRECTION('',(-1.E0,0.E0,0.E0));

+#2865=DIRECTION('',(0.E0,0.E0,-1.E0));

+#2866=AXIS2_PLACEMENT_3D('',#2863,#2864,#2865);

+#2867=PLANE('',#2866);

+#2868=ORIENTED_EDGE('',*,*,#2731,.F.);

+#2870=ORIENTED_EDGE('',*,*,#2869,.T.);

+#2872=ORIENTED_EDGE('',*,*,#2871,.T.);

+#2873=ORIENTED_EDGE('',*,*,#2772,.F.);

+#2874=EDGE_LOOP('',(#2868,#2870,#2872,#2873));

+#2875=FACE_OUTER_BOUND('',#2874,.F.);

+#2876=ADVANCED_FACE('',(#2875),#2867,.F.);

+#2877=CARTESIAN_POINT('',(1.05E-1,1.15E-1,2.25E-1));

+#2878=DIRECTION('',(0.E0,1.E0,0.E0));

+#2879=DIRECTION('',(-1.E0,0.E0,0.E0));

+#2880=AXIS2_PLACEMENT_3D('',#2877,#2878,#2879);

+#2881=PLANE('',#2880);

+#2882=ORIENTED_EDGE('',*,*,#2754,.T.);

+#2884=ORIENTED_EDGE('',*,*,#2883,.T.);

+#2886=ORIENTED_EDGE('',*,*,#2885,.T.);

+#2888=ORIENTED_EDGE('',*,*,#2887,.T.);

+#2889=ORIENTED_EDGE('',*,*,#2869,.F.);

+#2890=ORIENTED_EDGE('',*,*,#2739,.T.);

+#2891=ORIENTED_EDGE('',*,*,#2529,.F.);

+#2893=ORIENTED_EDGE('',*,*,#2892,.F.);

+#2894=ORIENTED_EDGE('',*,*,#2690,.T.);

+#2895=ORIENTED_EDGE('',*,*,#2715,.T.);

+#2896=EDGE_LOOP('',(#2882,#2884,#2886,#2888,#2889,#2890,#2891,#2893,#2894,

+#2895));

+#2897=FACE_OUTER_BOUND('',#2896,.F.);

+#2898=ADVANCED_FACE('',(#2897),#2881,.F.);

+#2899=CARTESIAN_POINT('',(4.7E-2,1.6E-1,1.12E-1));

+#2900=DIRECTION('',(0.E0,-1.E0,0.E0));

+#2901=DIRECTION('',(-1.E0,0.E0,0.E0));

+#2902=AXIS2_PLACEMENT_3D('',#2899,#2900,#2901);

+#2903=CYLINDRICAL_SURFACE('',#2902,5.3E-2);

+#2904=ORIENTED_EDGE('',*,*,#2752,.T.);

+#2905=ORIENTED_EDGE('',*,*,#2778,.T.);

+#2907=ORIENTED_EDGE('',*,*,#2906,.F.);

+#2908=ORIENTED_EDGE('',*,*,#2883,.F.);

+#2909=EDGE_LOOP('',(#2904,#2905,#2907,#2908));

+#2910=FACE_OUTER_BOUND('',#2909,.F.);

+#2911=ADVANCED_FACE('',(#2910),#2903,.F.);

+#2912=CARTESIAN_POINT('',(4.7E-2,1.6E-1,1.65E-1));

+#2913=DIRECTION('',(0.E0,0.E0,1.E0));

+#2914=DIRECTION('',(-1.E0,0.E0,0.E0));

+#2915=AXIS2_PLACEMENT_3D('',#2912,#2913,#2914);

+#2916=PLANE('',#2915);

+#2917=ORIENTED_EDGE('',*,*,#2906,.T.);

+#2918=ORIENTED_EDGE('',*,*,#2776,.T.);

+#2920=ORIENTED_EDGE('',*,*,#2919,.F.);

+#2921=ORIENTED_EDGE('',*,*,#2885,.F.);

+#2922=EDGE_LOOP('',(#2917,#2918,#2920,#2921));

+#2923=FACE_OUTER_BOUND('',#2922,.F.);

+#2924=ADVANCED_FACE('',(#2923),#2916,.F.);

+#2925=CARTESIAN_POINT('',(-4.7E-2,1.6E-1,1.12E-1));

+#2926=DIRECTION('',(0.E0,-1.E0,0.E0));

+#2927=DIRECTION('',(-1.E0,0.E0,0.E0));

+#2928=AXIS2_PLACEMENT_3D('',#2925,#2926,#2927);

+#2929=CYLINDRICAL_SURFACE('',#2928,5.3E-2);

+#2930=ORIENTED_EDGE('',*,*,#2919,.T.);

+#2931=ORIENTED_EDGE('',*,*,#2774,.T.);

+#2932=ORIENTED_EDGE('',*,*,#2871,.F.);

+#2933=ORIENTED_EDGE('',*,*,#2887,.F.);

+#2934=EDGE_LOOP('',(#2930,#2931,#2932,#2933));

+#2935=FACE_OUTER_BOUND('',#2934,.F.);

+#2936=ADVANCED_FACE('',(#2935),#2929,.F.);

+#2937=CARTESIAN_POINT('',(1.05E-1,1.35E-1,2.25E-1));

+#2938=DIRECTION('',(0.E0,-7.071067811865E-1,7.071067811865E-1));

+#2939=DIRECTION('',(-1.E0,0.E0,0.E0));

+#2940=AXIS2_PLACEMENT_3D('',#2937,#2938,#2939);

+#2941=PLANE('',#2940);

+#2942=ORIENTED_EDGE('',*,*,#2471,.F.);

+#2943=ORIENTED_EDGE('',*,*,#2490,.T.);

+#2944=ORIENTED_EDGE('',*,*,#2892,.T.);

+#2946=ORIENTED_EDGE('',*,*,#2945,.F.);

+#2947=EDGE_LOOP('',(#2942,#2943,#2944,#2946));

+#2948=FACE_OUTER_BOUND('',#2947,.F.);

+#2949=ADVANCED_FACE('',(#2948),#2941,.T.);

+#2950=CARTESIAN_POINT('',(-1.25E-1,1.15E-1,2.25E-1));

+#2951=DIRECTION('',(7.071067811865E-1,0.E0,7.071067811865E-1));

+#2952=DIRECTION('',(0.E0,-1.E0,0.E0));

+#2953=AXIS2_PLACEMENT_3D('',#2950,#2951,#2952);

+#2954=PLANE('',#2953);

+#2955=ORIENTED_EDGE('',*,*,#2473,.F.);

+#2956=ORIENTED_EDGE('',*,*,#2945,.T.);

+#2957=ORIENTED_EDGE('',*,*,#2527,.T.);

+#2958=ORIENTED_EDGE('',*,*,#2508,.F.);

+#2959=EDGE_LOOP('',(#2955,#2956,#2957,#2958));

+#2960=FACE_OUTER_BOUND('',#2959,.F.);

+#2961=ADVANCED_FACE('',(#2960),#2954,.T.);

+#2962=CARTESIAN_POINT('',(-6.E-2,6.5E-2,2.25E-1));

+#2963=DIRECTION('',(0.E0,1.E0,0.E0));

+#2964=DIRECTION('',(-1.E0,0.E0,0.E0));

+#2965=AXIS2_PLACEMENT_3D('',#2962,#2963,#2964);

+#2966=PLANE('',#2965);

+#2968=ORIENTED_EDGE('',*,*,#2967,.F.);

+#2969=ORIENTED_EDGE('',*,*,#2576,.T.);

+#2970=ORIENTED_EDGE('',*,*,#2622,.T.);

+#2971=ORIENTED_EDGE('',*,*,#2522,.F.);

+#2972=EDGE_LOOP('',(#2968,#2969,#2970,#2971));

+#2973=FACE_OUTER_BOUND('',#2972,.F.);

+#2974=ADVANCED_FACE('',(#2973),#2966,.F.);

+#2975=CARTESIAN_POINT('',(-6.E-2,8.5E-2,7.5E-2));

+#2976=DIRECTION('',(0.E0,-7.071067811865E-1,7.071067811865E-1));

+#2977=DIRECTION('',(-1.E0,0.E0,0.E0));

+#2978=AXIS2_PLACEMENT_3D('',#2975,#2976,#2977);

+#2979=PLANE('',#2978);

+#2980=ORIENTED_EDGE('',*,*,#2543,.F.);

+#2981=ORIENTED_EDGE('',*,*,#2578,.T.);

+#2982=ORIENTED_EDGE('',*,*,#2967,.T.);

+#2983=ORIENTED_EDGE('',*,*,#2520,.F.);

+#2984=EDGE_LOOP('',(#2980,#2981,#2982,#2983));

+#2985=FACE_OUTER_BOUND('',#2984,.F.);

+#2986=ADVANCED_FACE('',(#2985),#2979,.T.);

+#2987=CARTESIAN_POINT('',(3.75E-2,0.E0,0.E0));

+#2988=DIRECTION('',(1.E0,0.E0,0.E0));

+#2989=DIRECTION('',(0.E0,0.E0,-1.E0));

+#2990=AXIS2_PLACEMENT_3D('',#2987,#2988,#2989);

+#2991=PLANE('',#2990);

+#2993=ORIENTED_EDGE('',*,*,#2992,.T.);

+#2994=ORIENTED_EDGE('',*,*,#2627,.T.);

+#2996=ORIENTED_EDGE('',*,*,#2995,.T.);

+#2998=ORIENTED_EDGE('',*,*,#2997,.T.);

+#2999=EDGE_LOOP('',(#2993,#2994,#2996,#2998));

+#3000=FACE_OUTER_BOUND('',#2999,.F.);

+#3001=ADVANCED_FACE('',(#3000),#2991,.F.);

+#3002=CARTESIAN_POINT('',(3.75E-2,0.E0,0.E0));

+#3003=DIRECTION('',(1.E0,0.E0,0.E0));

+#3004=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3005=AXIS2_PLACEMENT_3D('',#3002,#3003,#3004);

+#3006=PLANE('',#3005);

+#3008=ORIENTED_EDGE('',*,*,#3007,.T.);

+#3010=ORIENTED_EDGE('',*,*,#3009,.T.);

+#3012=ORIENTED_EDGE('',*,*,#3011,.T.);

+#3014=ORIENTED_EDGE('',*,*,#3013,.F.);

+#3015=EDGE_LOOP('',(#3008,#3010,#3012,#3014));

+#3016=FACE_OUTER_BOUND('',#3015,.F.);

+#3017=ADVANCED_FACE('',(#3016),#3006,.F.);

+#3018=CARTESIAN_POINT('',(3.75E-2,-1.25E-2,1.15E-1));

+#3019=DIRECTION('',(0.E0,-1.E0,0.E0));

+#3020=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3021=AXIS2_PLACEMENT_3D('',#3018,#3019,#3020);

+#3022=PLANE('',#3021);

+#3023=ORIENTED_EDGE('',*,*,#2992,.F.);

+#3025=ORIENTED_EDGE('',*,*,#3024,.T.);

+#3027=ORIENTED_EDGE('',*,*,#3026,.T.);

+#3028=ORIENTED_EDGE('',*,*,#2629,.F.);

+#3029=EDGE_LOOP('',(#3023,#3025,#3027,#3028));

+#3030=FACE_OUTER_BOUND('',#3029,.F.);

+#3031=ADVANCED_FACE('',(#3030),#3022,.T.);

+#3032=CARTESIAN_POINT('',(3.75E-2,1.25E-2,1.15E-1));

+#3033=DIRECTION('',(0.E0,0.E0,1.E0));

+#3034=DIRECTION('',(0.E0,-1.E0,0.E0));

+#3035=AXIS2_PLACEMENT_3D('',#3032,#3033,#3034);

+#3036=PLANE('',#3035);

+#3037=ORIENTED_EDGE('',*,*,#2997,.F.);

+#3039=ORIENTED_EDGE('',*,*,#3038,.T.);

+#3041=ORIENTED_EDGE('',*,*,#3040,.T.);

+#3042=ORIENTED_EDGE('',*,*,#3024,.F.);

+#3043=EDGE_LOOP('',(#3037,#3039,#3041,#3042));

+#3044=FACE_OUTER_BOUND('',#3043,.F.);

+#3045=ADVANCED_FACE('',(#3044),#3036,.T.);

+#3046=CARTESIAN_POINT('',(3.75E-2,1.25E-2,-2.45E-1));

+#3047=DIRECTION('',(0.E0,1.E0,0.E0));

+#3048=DIRECTION('',(0.E0,0.E0,1.E0));

+#3049=AXIS2_PLACEMENT_3D('',#3046,#3047,#3048);

+#3050=PLANE('',#3049);

+#3051=ORIENTED_EDGE('',*,*,#2995,.F.);

+#3052=ORIENTED_EDGE('',*,*,#2633,.T.);

+#3054=ORIENTED_EDGE('',*,*,#3053,.T.);

+#3055=ORIENTED_EDGE('',*,*,#3038,.F.);

+#3056=EDGE_LOOP('',(#3051,#3052,#3054,#3055));

+#3057=FACE_OUTER_BOUND('',#3056,.F.);

+#3058=ADVANCED_FACE('',(#3057),#3050,.T.);

+#3059=CARTESIAN_POINT('',(6.25E-2,0.E0,0.E0));

+#3060=DIRECTION('',(1.E0,0.E0,0.E0));

+#3061=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3062=AXIS2_PLACEMENT_3D('',#3059,#3060,#3061);

+#3063=PLANE('',#3062);

+#3064=ORIENTED_EDGE('',*,*,#3026,.F.);

+#3065=ORIENTED_EDGE('',*,*,#3040,.F.);

+#3066=ORIENTED_EDGE('',*,*,#3053,.F.);

+#3067=ORIENTED_EDGE('',*,*,#2631,.F.);

+#3068=EDGE_LOOP('',(#3064,#3065,#3066,#3067));

+#3069=FACE_OUTER_BOUND('',#3068,.F.);

+#3070=ADVANCED_FACE('',(#3069),#3063,.T.);

+#3071=CARTESIAN_POINT('',(6.25E-2,0.E0,0.E0));

+#3072=DIRECTION('',(1.E0,0.E0,0.E0));

+#3073=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3074=AXIS2_PLACEMENT_3D('',#3071,#3072,#3073);

+#3075=PLANE('',#3074);

+#3077=ORIENTED_EDGE('',*,*,#3076,.F.);

+#3079=ORIENTED_EDGE('',*,*,#3078,.T.);

+#3081=ORIENTED_EDGE('',*,*,#3080,.F.);

+#3083=ORIENTED_EDGE('',*,*,#3082,.F.);

+#3084=EDGE_LOOP('',(#3077,#3079,#3081,#3083));

+#3085=FACE_OUTER_BOUND('',#3084,.F.);

+#3086=ADVANCED_FACE('',(#3085),#3075,.T.);

+#3087=CARTESIAN_POINT('',(3.75E-2,-3.75E-2,-2.7E-1));

+#3088=DIRECTION('',(0.E0,0.E0,1.E0));

+#3089=DIRECTION('',(0.E0,-1.E0,0.E0));

+#3090=AXIS2_PLACEMENT_3D('',#3087,#3088,#3089);

+#3091=PLANE('',#3090);

+#3092=ORIENTED_EDGE('',*,*,#3007,.F.);

+#3094=ORIENTED_EDGE('',*,*,#3093,.T.);

+#3096=ORIENTED_EDGE('',*,*,#3095,.T.);

+#3098=ORIENTED_EDGE('',*,*,#3097,.F.);

+#3099=ORIENTED_EDGE('',*,*,#3076,.T.);

+#3101=ORIENTED_EDGE('',*,*,#3100,.F.);

+#3102=EDGE_LOOP('',(#3092,#3094,#3096,#3098,#3099,#3101));

+#3103=FACE_OUTER_BOUND('',#3102,.F.);

+#3104=ADVANCED_FACE('',(#3103),#3091,.T.);

+#3105=CARTESIAN_POINT('',(3.75E-2,3.E-2,-2.69E-1));

+#3106=DIRECTION('',(1.E0,0.E0,0.E0));

+#3107=DIRECTION('',(0.E0,-1.E0,0.E0));

+#3108=AXIS2_PLACEMENT_3D('',#3105,#3106,#3107);

+#3109=PLANE('',#3108);

+#3111=ORIENTED_EDGE('',*,*,#3110,.F.);

+#3112=ORIENTED_EDGE('',*,*,#3093,.F.);

+#3114=ORIENTED_EDGE('',*,*,#3113,.F.);

+#3116=ORIENTED_EDGE('',*,*,#3115,.T.);

+#3117=EDGE_LOOP('',(#3111,#3112,#3114,#3116));

+#3118=FACE_OUTER_BOUND('',#3117,.F.);

+#3119=ADVANCED_FACE('',(#3118),#3109,.T.);

+#3120=CARTESIAN_POINT('',(3.75E-2,3.E-2,-2.69E-1));

+#3121=DIRECTION('',(1.E0,0.E0,0.E0));

+#3122=DIRECTION('',(0.E0,-1.E0,0.E0));

+#3123=AXIS2_PLACEMENT_3D('',#3120,#3121,#3122);

+#3124=PLANE('',#3123);

+#3126=ORIENTED_EDGE('',*,*,#3125,.T.);

+#3128=ORIENTED_EDGE('',*,*,#3127,.T.);

+#3130=ORIENTED_EDGE('',*,*,#3129,.T.);

+#3132=ORIENTED_EDGE('',*,*,#3131,.T.);

+#3134=ORIENTED_EDGE('',*,*,#3133,.T.);

+#3136=ORIENTED_EDGE('',*,*,#3135,.F.);

+#3137=EDGE_LOOP('',(#3126,#3128,#3130,#3132,#3134,#3136));

+#3138=FACE_OUTER_BOUND('',#3137,.F.);

+#3139=ADVANCED_FACE('',(#3138),#3124,.T.);

+#3140=CARTESIAN_POINT('',(3.75E-2,-3.75E-2,-2.45E-1));

+#3141=DIRECTION('',(1.E0,0.E0,0.E0));

+#3142=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3143=AXIS2_PLACEMENT_3D('',#3140,#3141,#3142);

+#3144=CYLINDRICAL_SURFACE('',#3143,2.5E-2);

+#3145=ORIENTED_EDGE('',*,*,#3095,.F.);

+#3146=ORIENTED_EDGE('',*,*,#3110,.T.);

+#3148=ORIENTED_EDGE('',*,*,#3147,.T.);

+#3150=ORIENTED_EDGE('',*,*,#3149,.F.);

+#3151=EDGE_LOOP('',(#3145,#3146,#3148,#3150));

+#3152=FACE_OUTER_BOUND('',#3151,.F.);

+#3153=ADVANCED_FACE('',(#3152),#3144,.F.);

+#3154=CARTESIAN_POINT('',(-1.E-1,-6.E-2,-2.69E-1));

+#3155=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3156=DIRECTION('',(0.E0,1.E0,0.E0));

+#3157=AXIS2_PLACEMENT_3D('',#3154,#3155,#3156);

+#3158=PLANE('',#3157);

+#3159=ORIENTED_EDGE('',*,*,#3147,.F.);

+#3160=ORIENTED_EDGE('',*,*,#3115,.F.);

+#3162=ORIENTED_EDGE('',*,*,#3161,.T.);

+#3164=ORIENTED_EDGE('',*,*,#3163,.T.);

+#3165=EDGE_LOOP('',(#3159,#3160,#3162,#3164));

+#3166=FACE_OUTER_BOUND('',#3165,.F.);

+#3167=ADVANCED_FACE('',(#3166),#3158,.T.);

+#3168=CARTESIAN_POINT('',(-1.E-1,-6.E-2,-2.69E-1));

+#3169=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3170=DIRECTION('',(0.E0,1.E0,0.E0));

+#3171=AXIS2_PLACEMENT_3D('',#3168,#3169,#3170);

+#3172=PLANE('',#3171);

+#3174=ORIENTED_EDGE('',*,*,#3173,.F.);

+#3176=ORIENTED_EDGE('',*,*,#3175,.F.);

+#3178=ORIENTED_EDGE('',*,*,#3177,.T.);

+#3180=ORIENTED_EDGE('',*,*,#3179,.T.);

+#3181=EDGE_LOOP('',(#3174,#3176,#3178,#3180));

+#3182=FACE_OUTER_BOUND('',#3181,.F.);

+#3183=ADVANCED_FACE('',(#3182),#3172,.T.);

+#3184=CARTESIAN_POINT('',(-1.E-1,-6.E-2,-2.69E-1));

+#3185=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3186=DIRECTION('',(0.E0,1.E0,0.E0));

+#3187=AXIS2_PLACEMENT_3D('',#3184,#3185,#3186);

+#3188=PLANE('',#3187);

+#3190=ORIENTED_EDGE('',*,*,#3189,.T.);

+#3192=ORIENTED_EDGE('',*,*,#3191,.T.);

+#3194=ORIENTED_EDGE('',*,*,#3193,.T.);

+#3195=ORIENTED_EDGE('',*,*,#3129,.F.);

+#3197=ORIENTED_EDGE('',*,*,#3196,.T.);

+#3199=ORIENTED_EDGE('',*,*,#3198,.T.);

+#3201=ORIENTED_EDGE('',*,*,#3200,.F.);

+#3203=ORIENTED_EDGE('',*,*,#3202,.T.);

+#3205=ORIENTED_EDGE('',*,*,#3204,.F.);

+#3207=ORIENTED_EDGE('',*,*,#3206,.F.);

+#3209=ORIENTED_EDGE('',*,*,#3208,.T.);

+#3211=ORIENTED_EDGE('',*,*,#3210,.F.);

+#3212=EDGE_LOOP('',(#3190,#3192,#3194,#3195,#3197,#3199,#3201,#3203,#3205,#3207,

+#3209,#3211));

+#3213=FACE_OUTER_BOUND('',#3212,.F.);

+#3214=ADVANCED_FACE('',(#3213),#3188,.T.);

+#3215=CARTESIAN_POINT('',(-1.E-1,-6.E-2,-2.25E-1));

+#3216=DIRECTION('',(0.E0,-1.E0,0.E0));

+#3217=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3218=AXIS2_PLACEMENT_3D('',#3215,#3216,#3217);

+#3219=PLANE('',#3218);

+#3221=ORIENTED_EDGE('',*,*,#3220,.T.);

+#3223=ORIENTED_EDGE('',*,*,#3222,.T.);

+#3225=ORIENTED_EDGE('',*,*,#3224,.T.);

+#3227=ORIENTED_EDGE('',*,*,#3226,.F.);

+#3228=ORIENTED_EDGE('',*,*,#2012,.T.);

+#3230=ORIENTED_EDGE('',*,*,#3229,.T.);

+#3232=ORIENTED_EDGE('',*,*,#3231,.T.);

+#3234=ORIENTED_EDGE('',*,*,#3233,.F.);

+#3235=ORIENTED_EDGE('',*,*,#3078,.F.);

+#3237=ORIENTED_EDGE('',*,*,#3236,.F.);

+#3238=ORIENTED_EDGE('',*,*,#3161,.F.);

+#3239=ORIENTED_EDGE('',*,*,#3113,.T.);

+#3240=ORIENTED_EDGE('',*,*,#3013,.T.);

+#3241=ORIENTED_EDGE('',*,*,#3135,.T.);

+#3243=ORIENTED_EDGE('',*,*,#3242,.T.);

+#3245=ORIENTED_EDGE('',*,*,#3244,.F.);

+#3247=ORIENTED_EDGE('',*,*,#3246,.F.);

+#3249=ORIENTED_EDGE('',*,*,#3248,.F.);

+#3250=ORIENTED_EDGE('',*,*,#3177,.F.);

+#3252=ORIENTED_EDGE('',*,*,#3251,.T.);

+#3253=EDGE_LOOP('',(#3221,#3223,#3225,#3227,#3228,#3230,#3232,#3234,#3235,#3237,

+#3238,#3239,#3240,#3241,#3243,#3245,#3247,#3249,#3250,#3252));

+#3254=FACE_OUTER_BOUND('',#3253,.F.);

+#3255=ADVANCED_FACE('',(#3254),#3219,.T.);

+#3256=CARTESIAN_POINT('',(-6.25E-2,0.E0,0.E0));

+#3257=DIRECTION('',(1.E0,0.E0,0.E0));

+#3258=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3259=AXIS2_PLACEMENT_3D('',#3256,#3257,#3258);

+#3260=PLANE('',#3259);

+#3261=ORIENTED_EDGE('',*,*,#2637,.T.);

+#3263=ORIENTED_EDGE('',*,*,#3262,.T.);

+#3265=ORIENTED_EDGE('',*,*,#3264,.T.);

+#3267=ORIENTED_EDGE('',*,*,#3266,.T.);

+#3268=EDGE_LOOP('',(#3261,#3263,#3265,#3267));

+#3269=FACE_OUTER_BOUND('',#3268,.F.);

+#3270=ADVANCED_FACE('',(#3269),#3260,.F.);

+#3271=CARTESIAN_POINT('',(-6.25E-2,0.E0,0.E0));

+#3272=DIRECTION('',(1.E0,0.E0,0.E0));

+#3273=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3274=AXIS2_PLACEMENT_3D('',#3271,#3272,#3273);

+#3275=PLANE('',#3274);

+#3276=ORIENTED_EDGE('',*,*,#3220,.F.);

+#3278=ORIENTED_EDGE('',*,*,#3277,.T.);

+#3280=ORIENTED_EDGE('',*,*,#3279,.T.);

+#3282=ORIENTED_EDGE('',*,*,#3281,.T.);

+#3283=EDGE_LOOP('',(#3276,#3278,#3280,#3282));

+#3284=FACE_OUTER_BOUND('',#3283,.F.);

+#3285=ADVANCED_FACE('',(#3284),#3275,.F.);

+#3286=CARTESIAN_POINT('',(-6.25E-2,1.25E-2,-2.45E-1));

+#3287=DIRECTION('',(0.E0,1.E0,0.E0));

+#3288=DIRECTION('',(0.E0,0.E0,1.E0));

+#3289=AXIS2_PLACEMENT_3D('',#3286,#3287,#3288);

+#3290=PLANE('',#3289);

+#3291=ORIENTED_EDGE('',*,*,#2643,.T.);

+#3293=ORIENTED_EDGE('',*,*,#3292,.T.);

+#3295=ORIENTED_EDGE('',*,*,#3294,.F.);

+#3296=ORIENTED_EDGE('',*,*,#3262,.F.);

+#3297=EDGE_LOOP('',(#3291,#3293,#3295,#3296));

+#3298=FACE_OUTER_BOUND('',#3297,.F.);

+#3299=ADVANCED_FACE('',(#3298),#3290,.T.);

+#3300=CARTESIAN_POINT('',(-3.75E-2,0.E0,0.E0));

+#3301=DIRECTION('',(1.E0,0.E0,0.E0));

+#3302=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3303=AXIS2_PLACEMENT_3D('',#3300,#3301,#3302);

+#3304=PLANE('',#3303);

+#3305=ORIENTED_EDGE('',*,*,#2641,.F.);

+#3307=ORIENTED_EDGE('',*,*,#3306,.F.);

+#3309=ORIENTED_EDGE('',*,*,#3308,.F.);

+#3310=ORIENTED_EDGE('',*,*,#3292,.F.);

+#3311=EDGE_LOOP('',(#3305,#3307,#3309,#3310));

+#3312=FACE_OUTER_BOUND('',#3311,.F.);

+#3313=ADVANCED_FACE('',(#3312),#3304,.T.);

+#3314=CARTESIAN_POINT('',(-3.75E-2,0.E0,0.E0));

+#3315=DIRECTION('',(1.E0,0.E0,0.E0));

+#3316=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3317=AXIS2_PLACEMENT_3D('',#3314,#3315,#3316);

+#3318=PLANE('',#3317);

+#3319=ORIENTED_EDGE('',*,*,#3246,.T.);

+#3321=ORIENTED_EDGE('',*,*,#3320,.F.);

+#3323=ORIENTED_EDGE('',*,*,#3322,.F.);

+#3325=ORIENTED_EDGE('',*,*,#3324,.F.);

+#3326=EDGE_LOOP('',(#3319,#3321,#3323,#3325));

+#3327=FACE_OUTER_BOUND('',#3326,.F.);

+#3328=ADVANCED_FACE('',(#3327),#3318,.T.);

+#3329=CARTESIAN_POINT('',(-6.25E-2,-1.25E-2,1.15E-1));

+#3330=DIRECTION('',(0.E0,-1.E0,0.E0));

+#3331=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3332=AXIS2_PLACEMENT_3D('',#3329,#3330,#3331);

+#3333=PLANE('',#3332);

+#3334=ORIENTED_EDGE('',*,*,#2639,.F.);

+#3335=ORIENTED_EDGE('',*,*,#3266,.F.);

+#3337=ORIENTED_EDGE('',*,*,#3336,.T.);

+#3338=ORIENTED_EDGE('',*,*,#3306,.T.);

+#3339=EDGE_LOOP('',(#3334,#3335,#3337,#3338));

+#3340=FACE_OUTER_BOUND('',#3339,.F.);

+#3341=ADVANCED_FACE('',(#3340),#3333,.T.);

+#3342=CARTESIAN_POINT('',(-6.25E-2,1.25E-2,1.15E-1));

+#3343=DIRECTION('',(0.E0,0.E0,1.E0));

+#3344=DIRECTION('',(0.E0,-1.E0,0.E0));

+#3345=AXIS2_PLACEMENT_3D('',#3342,#3343,#3344);

+#3346=PLANE('',#3345);

+#3347=ORIENTED_EDGE('',*,*,#3264,.F.);

+#3348=ORIENTED_EDGE('',*,*,#3294,.T.);

+#3349=ORIENTED_EDGE('',*,*,#3308,.T.);

+#3350=ORIENTED_EDGE('',*,*,#3336,.F.);

+#3351=EDGE_LOOP('',(#3347,#3348,#3349,#3350));

+#3352=FACE_OUTER_BOUND('',#3351,.F.);

+#3353=ADVANCED_FACE('',(#3352),#3346,.T.);

+#3354=CARTESIAN_POINT('',(-6.25E-2,-2.45E-1,-2.95E-1));

+#3355=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3356=DIRECTION('',(0.E0,1.E0,0.E0));

+#3357=AXIS2_PLACEMENT_3D('',#3354,#3355,#3356);

+#3358=PLANE('',#3357);

+#3360=ORIENTED_EDGE('',*,*,#3359,.F.);

+#3361=ORIENTED_EDGE('',*,*,#3281,.F.);

+#3363=ORIENTED_EDGE('',*,*,#3362,.T.);

+#3364=ORIENTED_EDGE('',*,*,#3320,.T.);

+#3366=ORIENTED_EDGE('',*,*,#3365,.T.);

+#3368=ORIENTED_EDGE('',*,*,#3367,.F.);

+#3369=EDGE_LOOP('',(#3360,#3361,#3363,#3364,#3366,#3368));

+#3370=FACE_OUTER_BOUND('',#3369,.F.);

+#3371=ADVANCED_FACE('',(#3370),#3358,.T.);

+#3372=CARTESIAN_POINT('',(-6.25E-2,3.E-2,-2.69E-1));

+#3373=DIRECTION('',(1.E0,0.E0,0.E0));

+#3374=DIRECTION('',(0.E0,-1.E0,0.E0));

+#3375=AXIS2_PLACEMENT_3D('',#3372,#3373,#3374);

+#3376=PLANE('',#3375);

+#3378=ORIENTED_EDGE('',*,*,#3377,.F.);

+#3380=ORIENTED_EDGE('',*,*,#3379,.F.);

+#3381=ORIENTED_EDGE('',*,*,#3251,.F.);

+#3382=ORIENTED_EDGE('',*,*,#3175,.T.);

+#3383=EDGE_LOOP('',(#3378,#3380,#3381,#3382));

+#3384=FACE_OUTER_BOUND('',#3383,.F.);

+#3385=ADVANCED_FACE('',(#3384),#3376,.T.);

+#3386=CARTESIAN_POINT('',(-6.25E-2,3.E-2,-2.69E-1));

+#3387=DIRECTION('',(1.E0,0.E0,0.E0));

+#3388=DIRECTION('',(0.E0,-1.E0,0.E0));

+#3389=AXIS2_PLACEMENT_3D('',#3386,#3387,#3388);

+#3390=PLANE('',#3389);

+#3391=ORIENTED_EDGE('',*,*,#3359,.T.);

+#3393=ORIENTED_EDGE('',*,*,#3392,.T.);

+#3394=ORIENTED_EDGE('',*,*,#3210,.T.);

+#3396=ORIENTED_EDGE('',*,*,#3395,.T.);

+#3398=ORIENTED_EDGE('',*,*,#3397,.T.);

+#3399=ORIENTED_EDGE('',*,*,#3222,.F.);

+#3400=EDGE_LOOP('',(#3391,#3393,#3394,#3396,#3398,#3399));

+#3401=FACE_OUTER_BOUND('',#3400,.F.);

+#3402=ADVANCED_FACE('',(#3401),#3390,.T.);

+#3403=CARTESIAN_POINT('',(-6.25E-2,-3.75E-2,-2.45E-1));

+#3404=DIRECTION('',(1.E0,0.E0,0.E0));

+#3405=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3406=AXIS2_PLACEMENT_3D('',#3403,#3404,#3405);

+#3407=CYLINDRICAL_SURFACE('',#3406,2.5E-2);

+#3408=ORIENTED_EDGE('',*,*,#3173,.T.);

+#3410=ORIENTED_EDGE('',*,*,#3409,.F.);

+#3412=ORIENTED_EDGE('',*,*,#3411,.F.);

+#3413=ORIENTED_EDGE('',*,*,#3377,.T.);

+#3414=EDGE_LOOP('',(#3408,#3410,#3412,#3413));

+#3415=FACE_OUTER_BOUND('',#3414,.F.);

+#3416=ADVANCED_FACE('',(#3415),#3407,.F.);

+#3417=CARTESIAN_POINT('',(-3.75E-2,-6.E-2,-2.69E-1));

+#3418=DIRECTION('',(-1.E0,0.E0,0.E0));

+#3419=DIRECTION('',(0.E0,1.E0,0.E0));

+#3420=AXIS2_PLACEMENT_3D('',#3417,#3418,#3419);

+#3421=PLANE('',#3420);

+#3422=ORIENTED_EDGE('',*,*,#3409,.T.);

+#3423=ORIENTED_EDGE('',*,*,#3179,.F.);

+#3424=ORIENTED_EDGE('',*,*,#3248,.T.);

+#3426=ORIENTED_EDGE('',*,*,#3425,.T.);

+#3427=EDGE_LOOP('',(#3422,#3423,#3424,#3426));

+#3428=FACE_OUTER_BOUND('',#3427,.F.);

+#3429=ADVANCED_FACE('',(#3428),#3421,.T.);

+#3430=CARTESIAN_POINT('',(-3.75E-2,-6.E-2,-2.69E-1));

+#3431=DIRECTION('',(-1.E0,0.E0,0.E0));

+#3432=DIRECTION('',(0.E0,1.E0,0.E0));

+#3433=AXIS2_PLACEMENT_3D('',#3430,#3431,#3432);

+#3434=PLANE('',#3433);

+#3435=ORIENTED_EDGE('',*,*,#3365,.F.);

+#3436=ORIENTED_EDGE('',*,*,#3244,.T.);

+#3438=ORIENTED_EDGE('',*,*,#3437,.T.);

+#3440=ORIENTED_EDGE('',*,*,#3439,.F.);

+#3441=ORIENTED_EDGE('',*,*,#3191,.F.);

+#3443=ORIENTED_EDGE('',*,*,#3442,.F.);

+#3444=EDGE_LOOP('',(#3435,#3436,#3438,#3440,#3441,#3443));

+#3445=FACE_OUTER_BOUND('',#3444,.F.);

+#3446=ADVANCED_FACE('',(#3445),#3434,.T.);

+#3447=CARTESIAN_POINT('',(-6.25E-2,-3.75E-2,-2.7E-1));

+#3448=DIRECTION('',(0.E0,0.E0,1.E0));

+#3449=DIRECTION('',(0.E0,-1.E0,0.E0));

+#3450=AXIS2_PLACEMENT_3D('',#3447,#3448,#3449);

+#3451=PLANE('',#3450);

+#3452=ORIENTED_EDGE('',*,*,#3379,.T.);

+#3453=ORIENTED_EDGE('',*,*,#3411,.T.);

+#3454=ORIENTED_EDGE('',*,*,#3425,.F.);

+#3455=ORIENTED_EDGE('',*,*,#3324,.T.);

+#3457=ORIENTED_EDGE('',*,*,#3456,.F.);

+#3458=ORIENTED_EDGE('',*,*,#3277,.F.);

+#3459=EDGE_LOOP('',(#3452,#3453,#3454,#3455,#3457,#3458));

+#3460=FACE_OUTER_BOUND('',#3459,.F.);

+#3461=ADVANCED_FACE('',(#3460),#3451,.T.);

+#3462=CARTESIAN_POINT('',(-6.25E-2,-2.45E-1,-2.7E-1));

+#3463=DIRECTION('',(0.E0,-1.E0,0.E0));

+#3464=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3465=AXIS2_PLACEMENT_3D('',#3462,#3463,#3464);

+#3466=PLANE('',#3465);

+#3467=ORIENTED_EDGE('',*,*,#3279,.F.);

+#3468=ORIENTED_EDGE('',*,*,#3456,.T.);

+#3469=ORIENTED_EDGE('',*,*,#3322,.T.);

+#3470=ORIENTED_EDGE('',*,*,#3362,.F.);

+#3471=EDGE_LOOP('',(#3467,#3468,#3469,#3470));

+#3472=FACE_OUTER_BOUND('',#3471,.F.);

+#3473=ADVANCED_FACE('',(#3472),#3466,.T.);

+#3474=CARTESIAN_POINT('',(0.E0,0.E0,-3.1E-1));

+#3475=DIRECTION('',(0.E0,0.E0,1.E0));

+#3476=DIRECTION('',(1.E0,0.E0,0.E0));

+#3477=AXIS2_PLACEMENT_3D('',#3474,#3475,#3476);

+#3478=PLANE('',#3477);

+#3480=ORIENTED_EDGE('',*,*,#3479,.F.);

+#3482=ORIENTED_EDGE('',*,*,#3481,.F.);

+#3484=ORIENTED_EDGE('',*,*,#3483,.F.);

+#3485=ORIENTED_EDGE('',*,*,#3231,.F.);

+#3486=EDGE_LOOP('',(#3480,#3482,#3484,#3485));

+#3487=FACE_OUTER_BOUND('',#3486,.F.);

+#3488=ADVANCED_FACE('',(#3487),#3478,.F.);

+#3489=CARTESIAN_POINT('',(0.E0,0.E0,-3.1E-1));

+#3490=DIRECTION('',(0.E0,0.E0,1.E0));

+#3491=DIRECTION('',(1.E0,0.E0,0.E0));

+#3492=AXIS2_PLACEMENT_3D('',#3489,#3490,#3491);

+#3493=PLANE('',#3492);

+#3495=ORIENTED_EDGE('',*,*,#3494,.F.);

+#3497=ORIENTED_EDGE('',*,*,#3496,.F.);

+#3498=ORIENTED_EDGE('',*,*,#3224,.F.);

+#3499=ORIENTED_EDGE('',*,*,#3397,.F.);

+#3500=EDGE_LOOP('',(#3495,#3497,#3498,#3499));

+#3501=FACE_OUTER_BOUND('',#3500,.F.);

+#3502=ADVANCED_FACE('',(#3501),#3493,.F.);

+#3503=CARTESIAN_POINT('',(0.E0,0.E0,-3.1E-1));

+#3504=DIRECTION('',(0.E0,0.E0,1.E0));

+#3505=DIRECTION('',(1.E0,0.E0,0.E0));

+#3506=AXIS2_PLACEMENT_3D('',#3503,#3504,#3505);

+#3507=PLANE('',#3506);

+#3508=ORIENTED_EDGE('',*,*,#3133,.F.);

+#3510=ORIENTED_EDGE('',*,*,#3509,.F.);

+#3511=ORIENTED_EDGE('',*,*,#3437,.F.);

+#3512=ORIENTED_EDGE('',*,*,#3242,.F.);

+#3513=EDGE_LOOP('',(#3508,#3510,#3511,#3512));

+#3514=FACE_OUTER_BOUND('',#3513,.F.);

+#3515=ADVANCED_FACE('',(#3514),#3507,.F.);

+#3516=CARTESIAN_POINT('',(1.E-1,0.E0,0.E0));

+#3517=DIRECTION('',(1.E0,0.E0,0.E0));

+#3518=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3519=AXIS2_PLACEMENT_3D('',#3516,#3517,#3518);

+#3520=PLANE('',#3519);

+#3521=ORIENTED_EDGE('',*,*,#3202,.F.);

+#3523=ORIENTED_EDGE('',*,*,#3522,.T.);

+#3524=ORIENTED_EDGE('',*,*,#3479,.T.);

+#3525=ORIENTED_EDGE('',*,*,#3229,.F.);

+#3526=ORIENTED_EDGE('',*,*,#2010,.T.);

+#3528=ORIENTED_EDGE('',*,*,#3527,.F.);

+#3529=EDGE_LOOP('',(#3521,#3523,#3524,#3525,#3526,#3528));

+#3530=FACE_OUTER_BOUND('',#3529,.F.);

+#3531=ADVANCED_FACE('',(#3530),#3520,.T.);

+#3532=CARTESIAN_POINT('',(6.25E-2,3.E-2,-2.69E-1));

+#3533=DIRECTION('',(0.E0,1.E0,0.E0));

+#3534=DIRECTION('',(1.E0,0.E0,0.E0));

+#3535=AXIS2_PLACEMENT_3D('',#3532,#3533,#3534);

+#3536=PLANE('',#3535);

+#3537=ORIENTED_EDGE('',*,*,#3200,.T.);

+#3539=ORIENTED_EDGE('',*,*,#3538,.T.);

+#3540=ORIENTED_EDGE('',*,*,#3481,.T.);

+#3541=ORIENTED_EDGE('',*,*,#3522,.F.);

+#3542=EDGE_LOOP('',(#3537,#3539,#3540,#3541));

+#3543=FACE_OUTER_BOUND('',#3542,.F.);

+#3544=ADVANCED_FACE('',(#3543),#3536,.T.);

+#3545=CARTESIAN_POINT('',(6.25E-2,-6.E-2,-2.69E-1));

+#3546=DIRECTION('',(-1.E0,0.E0,0.E0));

+#3547=DIRECTION('',(0.E0,1.E0,0.E0));

+#3548=AXIS2_PLACEMENT_3D('',#3545,#3546,#3547);

+#3549=PLANE('',#3548);

+#3550=ORIENTED_EDGE('',*,*,#3149,.T.);

+#3551=ORIENTED_EDGE('',*,*,#3163,.F.);

+#3552=ORIENTED_EDGE('',*,*,#3236,.T.);

+#3553=ORIENTED_EDGE('',*,*,#3097,.T.);

+#3554=EDGE_LOOP('',(#3550,#3551,#3552,#3553));

+#3555=FACE_OUTER_BOUND('',#3554,.F.);

+#3556=ADVANCED_FACE('',(#3555),#3549,.T.);

+#3557=CARTESIAN_POINT('',(6.25E-2,-6.E-2,-2.69E-1));

+#3558=DIRECTION('',(-1.E0,0.E0,0.E0));

+#3559=DIRECTION('',(0.E0,1.E0,0.E0));

+#3560=AXIS2_PLACEMENT_3D('',#3557,#3558,#3559);

+#3561=PLANE('',#3560);

+#3563=ORIENTED_EDGE('',*,*,#3562,.F.);

+#3564=ORIENTED_EDGE('',*,*,#3233,.T.);

+#3565=ORIENTED_EDGE('',*,*,#3483,.T.);

+#3566=ORIENTED_EDGE('',*,*,#3538,.F.);

+#3567=ORIENTED_EDGE('',*,*,#3198,.F.);

+#3569=ORIENTED_EDGE('',*,*,#3568,.F.);

+#3570=EDGE_LOOP('',(#3563,#3564,#3565,#3566,#3567,#3569));

+#3571=FACE_OUTER_BOUND('',#3570,.F.);

+#3572=ADVANCED_FACE('',(#3571),#3561,.T.);

+#3573=CARTESIAN_POINT('',(3.75E-2,-2.45E-1,-2.95E-1));

+#3574=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3575=DIRECTION('',(0.E0,1.E0,0.E0));

+#3576=AXIS2_PLACEMENT_3D('',#3573,#3574,#3575);

+#3577=PLANE('',#3576);

+#3578=ORIENTED_EDGE('',*,*,#3080,.T.);

+#3579=ORIENTED_EDGE('',*,*,#3562,.T.);

+#3581=ORIENTED_EDGE('',*,*,#3580,.F.);

+#3582=ORIENTED_EDGE('',*,*,#3125,.F.);

+#3583=ORIENTED_EDGE('',*,*,#3011,.F.);

+#3585=ORIENTED_EDGE('',*,*,#3584,.T.);

+#3586=EDGE_LOOP('',(#3578,#3579,#3581,#3582,#3583,#3585));

+#3587=FACE_OUTER_BOUND('',#3586,.F.);

+#3588=ADVANCED_FACE('',(#3587),#3577,.T.);

+#3589=CARTESIAN_POINT('',(3.75E-2,-3.75E-2,-2.45E-1));

+#3590=DIRECTION('',(1.E0,0.E0,0.E0));

+#3591=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3592=AXIS2_PLACEMENT_3D('',#3589,#3590,#3591);

+#3593=CYLINDRICAL_SURFACE('',#3592,5.E-2);

+#3594=ORIENTED_EDGE('',*,*,#3580,.T.);

+#3595=ORIENTED_EDGE('',*,*,#3568,.T.);

+#3596=ORIENTED_EDGE('',*,*,#3196,.F.);

+#3597=ORIENTED_EDGE('',*,*,#3127,.F.);

+#3598=EDGE_LOOP('',(#3594,#3595,#3596,#3597));

+#3599=FACE_OUTER_BOUND('',#3598,.F.);

+#3600=ADVANCED_FACE('',(#3599),#3593,.T.);

+#3601=CARTESIAN_POINT('',(3.75E-2,-2.45E-1,-2.7E-1));

+#3602=DIRECTION('',(0.E0,-1.E0,0.E0));

+#3603=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3604=AXIS2_PLACEMENT_3D('',#3601,#3602,#3603);

+#3605=PLANE('',#3604);

+#3606=ORIENTED_EDGE('',*,*,#3009,.F.);

+#3607=ORIENTED_EDGE('',*,*,#3100,.T.);

+#3608=ORIENTED_EDGE('',*,*,#3082,.T.);

+#3609=ORIENTED_EDGE('',*,*,#3584,.F.);

+#3610=EDGE_LOOP('',(#3606,#3607,#3608,#3609));

+#3611=FACE_OUTER_BOUND('',#3610,.F.);

+#3612=ADVANCED_FACE('',(#3611),#3605,.T.);

+#3613=CARTESIAN_POINT('',(-1.E-1,6.E-2,-2.69E-1));

+#3614=DIRECTION('',(0.E0,1.E0,0.E0));

+#3615=DIRECTION('',(0.E0,0.E0,1.E0));

+#3616=AXIS2_PLACEMENT_3D('',#3613,#3614,#3615);

+#3617=PLANE('',#3616);

+#3618=ORIENTED_EDGE('',*,*,#2008,.F.);

+#3620=ORIENTED_EDGE('',*,*,#3619,.F.);

+#3621=ORIENTED_EDGE('',*,*,#3204,.T.);

+#3622=ORIENTED_EDGE('',*,*,#3527,.T.);

+#3623=EDGE_LOOP('',(#3618,#3620,#3621,#3622));

+#3624=FACE_OUTER_BOUND('',#3623,.F.);

+#3625=ADVANCED_FACE('',(#3624),#3617,.T.);

+#3626=CARTESIAN_POINT('',(-1.E-1,0.E0,0.E0));

+#3627=DIRECTION('',(1.E0,0.E0,0.E0));

+#3628=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3629=AXIS2_PLACEMENT_3D('',#3626,#3627,#3628);

+#3630=PLANE('',#3629);

+#3631=ORIENTED_EDGE('',*,*,#3226,.T.);

+#3632=ORIENTED_EDGE('',*,*,#3496,.T.);

+#3634=ORIENTED_EDGE('',*,*,#3633,.F.);

+#3635=ORIENTED_EDGE('',*,*,#3206,.T.);

+#3636=ORIENTED_EDGE('',*,*,#3619,.T.);

+#3637=ORIENTED_EDGE('',*,*,#2006,.F.);

+#3638=EDGE_LOOP('',(#3631,#3632,#3634,#3635,#3636,#3637));

+#3639=FACE_OUTER_BOUND('',#3638,.F.);

+#3640=ADVANCED_FACE('',(#3639),#3630,.F.);

+#3641=CARTESIAN_POINT('',(-1.E-1,3.E-2,-2.69E-1));

+#3642=DIRECTION('',(0.E0,1.E0,0.E0));

+#3643=DIRECTION('',(1.E0,0.E0,0.E0));

+#3644=AXIS2_PLACEMENT_3D('',#3641,#3642,#3643);

+#3645=PLANE('',#3644);

+#3646=ORIENTED_EDGE('',*,*,#3208,.F.);

+#3647=ORIENTED_EDGE('',*,*,#3633,.T.);

+#3648=ORIENTED_EDGE('',*,*,#3494,.T.);

+#3649=ORIENTED_EDGE('',*,*,#3395,.F.);

+#3650=EDGE_LOOP('',(#3646,#3647,#3648,#3649));

+#3651=FACE_OUTER_BOUND('',#3650,.F.);

+#3652=ADVANCED_FACE('',(#3651),#3645,.T.);

+#3653=CARTESIAN_POINT('',(-3.75E-2,3.E-2,-2.69E-1));

+#3654=DIRECTION('',(0.E0,1.E0,0.E0));

+#3655=DIRECTION('',(1.E0,0.E0,0.E0));

+#3656=AXIS2_PLACEMENT_3D('',#3653,#3654,#3655);

+#3657=PLANE('',#3656);

+#3658=ORIENTED_EDGE('',*,*,#3193,.F.);

+#3659=ORIENTED_EDGE('',*,*,#3439,.T.);

+#3660=ORIENTED_EDGE('',*,*,#3509,.T.);

+#3661=ORIENTED_EDGE('',*,*,#3131,.F.);

+#3662=EDGE_LOOP('',(#3658,#3659,#3660,#3661));

+#3663=FACE_OUTER_BOUND('',#3662,.F.);

+#3664=ADVANCED_FACE('',(#3663),#3657,.T.);

+#3665=CARTESIAN_POINT('',(-6.25E-2,-3.75E-2,-2.45E-1));

+#3666=DIRECTION('',(1.E0,0.E0,0.E0));

+#3667=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3668=AXIS2_PLACEMENT_3D('',#3665,#3666,#3667);

+#3669=CYLINDRICAL_SURFACE('',#3668,5.E-2);

+#3670=ORIENTED_EDGE('',*,*,#3189,.F.);

+#3671=ORIENTED_EDGE('',*,*,#3392,.F.);

+#3672=ORIENTED_EDGE('',*,*,#3367,.T.);

+#3673=ORIENTED_EDGE('',*,*,#3442,.T.);

+#3674=EDGE_LOOP('',(#3670,#3671,#3672,#3673));

+#3675=FACE_OUTER_BOUND('',#3674,.F.);

+#3676=ADVANCED_FACE('',(#3675),#3669,.T.);

+#3677=CARTESIAN_POINT('',(-8.E-2,-1.15E-1,-2.25E-1));

+#3678=DIRECTION('',(7.071067811865E-1,-7.071067811865E-1,0.E0));

+#3679=DIRECTION('',(7.071067811865E-1,7.071067811865E-1,0.E0));

+#3680=AXIS2_PLACEMENT_3D('',#3677,#3678,#3679);

+#3681=PLANE('',#3680);

+#3683=ORIENTED_EDGE('',*,*,#3682,.F.);

+#3685=ORIENTED_EDGE('',*,*,#3684,.T.);

+#3686=ORIENTED_EDGE('',*,*,#2438,.T.);

+#3687=ORIENTED_EDGE('',*,*,#2129,.F.);

+#3688=EDGE_LOOP('',(#3683,#3685,#3686,#3687));

+#3689=FACE_OUTER_BOUND('',#3688,.F.);

+#3690=ADVANCED_FACE('',(#3689),#3681,.T.);

+#3691=CARTESIAN_POINT('',(-8.E-2,-1.15E-1,-2.25E-1));

+#3692=DIRECTION('',(7.071067811865E-1,-7.071067811865E-1,0.E0));

+#3693=DIRECTION('',(7.071067811865E-1,7.071067811865E-1,0.E0));

+#3694=AXIS2_PLACEMENT_3D('',#3691,#3692,#3693);

+#3695=PLANE('',#3694);

+#3697=ORIENTED_EDGE('',*,*,#3696,.T.);

+#3698=ORIENTED_EDGE('',*,*,#2117,.F.);

+#3699=ORIENTED_EDGE('',*,*,#1980,.F.);

+#3701=ORIENTED_EDGE('',*,*,#3700,.T.);

+#3702=EDGE_LOOP('',(#3697,#3698,#3699,#3701));

+#3703=FACE_OUTER_BOUND('',#3702,.F.);

+#3704=ADVANCED_FACE('',(#3703),#3695,.T.);

+#3705=CARTESIAN_POINT('',(-1.564234044424E-1,-1.E-1,8.53E-2));

+#3706=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3707=DIRECTION('',(1.E0,0.E0,0.E0));

+#3708=AXIS2_PLACEMENT_3D('',#3705,#3706,#3707);

+#3709=PLANE('',#3708);

+#3711=ORIENTED_EDGE('',*,*,#3710,.T.);

+#3713=ORIENTED_EDGE('',*,*,#3712,.F.);

+#3714=ORIENTED_EDGE('',*,*,#2076,.T.);

+#3716=ORIENTED_EDGE('',*,*,#3715,.T.);

+#3717=ORIENTED_EDGE('',*,*,#3682,.T.);

+#3718=ORIENTED_EDGE('',*,*,#2127,.T.);

+#3720=ORIENTED_EDGE('',*,*,#3719,.T.);

+#3721=EDGE_LOOP('',(#3711,#3713,#3714,#3716,#3717,#3718,#3720));

+#3722=FACE_OUTER_BOUND('',#3721,.F.);

+#3723=ADVANCED_FACE('',(#3722),#3709,.F.);

+#3724=CARTESIAN_POINT('',(-9.434827557301E-2,-1.683556823861E-1,4.63E-2));

+#3725=DIRECTION('',(0.E0,1.E0,0.E0));

+#3726=DIRECTION('',(1.E0,0.E0,0.E0));

+#3727=AXIS2_PLACEMENT_3D('',#3724,#3725,#3726);

+#3728=CONICAL_SURFACE('',#3727,4.441782116695E-2,5.0477E1);

+#3730=ORIENTED_EDGE('',*,*,#3729,.F.);

+#3732=ORIENTED_EDGE('',*,*,#3731,.F.);

+#3734=ORIENTED_EDGE('',*,*,#3733,.F.);

+#3736=ORIENTED_EDGE('',*,*,#3735,.F.);

+#3737=ORIENTED_EDGE('',*,*,#3710,.F.);

+#3739=ORIENTED_EDGE('',*,*,#3738,.F.);

+#3741=ORIENTED_EDGE('',*,*,#3740,.F.);

+#3743=ORIENTED_EDGE('',*,*,#3742,.F.);

+#3745=ORIENTED_EDGE('',*,*,#3744,.F.);

+#3747=ORIENTED_EDGE('',*,*,#3746,.T.);

+#3749=ORIENTED_EDGE('',*,*,#3748,.F.);

+#3751=ORIENTED_EDGE('',*,*,#3750,.F.);

+#3752=EDGE_LOOP('',(#3730,#3732,#3734,#3736,#3737,#3739,#3741,#3743,#3745,#3747,

+#3749,#3751));

+#3753=FACE_OUTER_BOUND('',#3752,.F.);

+#3754=ADVANCED_FACE('',(#3753),#3728,.T.);

+#3755=CARTESIAN_POINT('',(-1.06E-1,-1.E-1,-5.5E-3));

+#3756=DIRECTION('',(8.660254037845E-1,0.E0,5.E-1));

+#3757=DIRECTION('',(-5.E-1,0.E0,8.660254037845E-1));

+#3758=AXIS2_PLACEMENT_3D('',#3755,#3756,#3757);

+#3759=PLANE('',#3758);

+#3761=ORIENTED_EDGE('',*,*,#3760,.T.);

+#3763=ORIENTED_EDGE('',*,*,#3762,.F.);

+#3764=ORIENTED_EDGE('',*,*,#2057,.T.);

+#3766=ORIENTED_EDGE('',*,*,#3765,.T.);

+#3767=ORIENTED_EDGE('',*,*,#3729,.T.);

+#3768=EDGE_LOOP('',(#3761,#3763,#3764,#3766,#3767));

+#3769=FACE_OUTER_BOUND('',#3768,.F.);

+#3770=ADVANCED_FACE('',(#3769),#3759,.F.);

+#3771=CARTESIAN_POINT('',(-9.434827557301E-2,-1.767206638076E-1,4.63E-2));

+#3772=DIRECTION('',(0.E0,1.E0,0.E0));

+#3773=DIRECTION('',(1.E0,0.E0,0.E0));

+#3774=AXIS2_PLACEMENT_3D('',#3771,#3772,#3773);

+#3775=CONICAL_SURFACE('',#3774,3.427858267256E-2,5.0477E1);

+#3776=ORIENTED_EDGE('',*,*,#3760,.F.);

+#3777=ORIENTED_EDGE('',*,*,#3750,.T.);

+#3779=ORIENTED_EDGE('',*,*,#3778,.T.);

+#3780=ORIENTED_EDGE('',*,*,#3746,.F.);

+#3782=ORIENTED_EDGE('',*,*,#3781,.F.);

+#3784=ORIENTED_EDGE('',*,*,#3783,.F.);

+#3786=ORIENTED_EDGE('',*,*,#3785,.F.);

+#3788=ORIENTED_EDGE('',*,*,#3787,.F.);

+#3789=EDGE_LOOP('',(#3776,#3777,#3779,#3780,#3782,#3784,#3786,#3788));

+#3790=FACE_OUTER_BOUND('',#3789,.F.);

+#3791=ADVANCED_FACE('',(#3790),#3775,.T.);

+#3792=CARTESIAN_POINT('',(-9.434827557301E-2,-1.790729432769E-1,4.63E-2));

+#3793=DIRECTION('',(0.E0,-1.E0,0.E0));

+#3794=DIRECTION('',(-1.E0,0.E0,0.E0));

+#3795=AXIS2_PLACEMENT_3D('',#3792,#3793,#3794);

+#3796=SPHERICAL_SURFACE('',#3795,2.E-2);

+#3798=ORIENTED_EDGE('',*,*,#3797,.T.);

+#3800=ORIENTED_EDGE('',*,*,#3799,.F.);

+#3801=ORIENTED_EDGE('',*,*,#3778,.F.);

+#3802=EDGE_LOOP('',(#3798,#3800,#3801));

+#3803=FACE_OUTER_BOUND('',#3802,.F.);

+#3804=ADVANCED_FACE('',(#3803),#3796,.T.);

+#3805=CARTESIAN_POINT('',(-9.434827557301E-2,-1.790729432769E-1,4.63E-2));

+#3806=DIRECTION('',(0.E0,-1.E0,0.E0));

+#3807=DIRECTION('',(-1.E0,0.E0,0.E0));

+#3808=AXIS2_PLACEMENT_3D('',#3805,#3806,#3807);

+#3809=SPHERICAL_SURFACE('',#3808,2.E-2);

+#3810=ORIENTED_EDGE('',*,*,#3797,.F.);

+#3811=ORIENTED_EDGE('',*,*,#3748,.T.);

+#3812=ORIENTED_EDGE('',*,*,#3799,.T.);

+#3813=EDGE_LOOP('',(#3810,#3811,#3812));

+#3814=FACE_OUTER_BOUND('',#3813,.F.);

+#3815=ADVANCED_FACE('',(#3814),#3809,.T.);

+#3816=CARTESIAN_POINT('',(-5.257659555759E-2,-1.E-1,8.356794919243E-2));

+#3817=DIRECTION('',(-8.660254037845E-1,0.E0,5.E-1));

+#3818=DIRECTION('',(-5.E-1,0.E0,-8.660254037845E-1));

+#3819=AXIS2_PLACEMENT_3D('',#3816,#3817,#3818);

+#3820=PLANE('',#3819);

+#3821=ORIENTED_EDGE('',*,*,#3781,.T.);

+#3822=ORIENTED_EDGE('',*,*,#3744,.T.);

+#3824=ORIENTED_EDGE('',*,*,#3823,.F.);

+#3825=ORIENTED_EDGE('',*,*,#2119,.T.);

+#3826=ORIENTED_EDGE('',*,*,#3696,.F.);

+#3828=ORIENTED_EDGE('',*,*,#3827,.T.);

+#3829=ORIENTED_EDGE('',*,*,#2065,.T.);

+#3831=ORIENTED_EDGE('',*,*,#3830,.T.);

+#3832=EDGE_LOOP('',(#3821,#3822,#3824,#3825,#3826,#3828,#3829,#3831));

+#3833=FACE_OUTER_BOUND('',#3832,.F.);

+#3834=ADVANCED_FACE('',(#3833),#3820,.F.);

+#3835=CARTESIAN_POINT('',(-5.015172442699E-2,-1.E-1,8.496794919243E-2));

+#3836=DIRECTION('',(-5.E-1,0.E0,8.660254037845E-1));

+#3837=DIRECTION('',(-8.660254037845E-1,0.E0,-5.E-1));

+#3838=AXIS2_PLACEMENT_3D('',#3835,#3836,#3837);

+#3839=PLANE('',#3838);

+#3840=ORIENTED_EDGE('',*,*,#3742,.T.);

+#3842=ORIENTED_EDGE('',*,*,#3841,.F.);

+#3843=ORIENTED_EDGE('',*,*,#2121,.T.);

+#3844=ORIENTED_EDGE('',*,*,#3823,.T.);

+#3845=EDGE_LOOP('',(#3840,#3842,#3843,#3844));

+#3846=FACE_OUTER_BOUND('',#3845,.F.);

+#3847=ADVANCED_FACE('',(#3846),#3839,.F.);

+#3848=CARTESIAN_POINT('',(-5.115172442699E-2,-1.E-1,8.67E-2));

+#3849=DIRECTION('',(-8.660254037845E-1,0.E0,-5.E-1));

+#3850=DIRECTION('',(5.E-1,0.E0,-8.660254037845E-1));

+#3851=AXIS2_PLACEMENT_3D('',#3848,#3849,#3850);

+#3852=PLANE('',#3851);

+#3853=ORIENTED_EDGE('',*,*,#3740,.T.);

+#3855=ORIENTED_EDGE('',*,*,#3854,.F.);

+#3856=ORIENTED_EDGE('',*,*,#2123,.T.);

+#3857=ORIENTED_EDGE('',*,*,#3841,.T.);

+#3858=EDGE_LOOP('',(#3853,#3855,#3856,#3857));

+#3859=FACE_OUTER_BOUND('',#3858,.F.);

+#3860=ADVANCED_FACE('',(#3859),#3852,.F.);

+#3861=CARTESIAN_POINT('',(-5.357659555759E-2,-1.E-1,8.53E-2));

+#3862=DIRECTION('',(5.E-1,0.E0,-8.660254037844E-1));

+#3863=DIRECTION('',(8.660254037844E-1,0.E0,5.E-1));

+#3864=AXIS2_PLACEMENT_3D('',#3861,#3862,#3863);

+#3865=PLANE('',#3864);

+#3866=ORIENTED_EDGE('',*,*,#3738,.T.);

+#3867=ORIENTED_EDGE('',*,*,#3719,.F.);

+#3868=ORIENTED_EDGE('',*,*,#2125,.T.);

+#3869=ORIENTED_EDGE('',*,*,#3854,.T.);

+#3870=EDGE_LOOP('',(#3866,#3867,#3868,#3869));

+#3871=FACE_OUTER_BOUND('',#3870,.F.);

+#3872=ADVANCED_FACE('',(#3871),#3865,.F.);

+#3873=CARTESIAN_POINT('',(-9.5E-2,-1.E-1,-2.25E-1));

+#3874=DIRECTION('',(-7.071067811865E-1,-7.071067811865E-1,0.E0));

+#3875=DIRECTION('',(7.071067811865E-1,-7.071067811865E-1,0.E0));

+#3876=AXIS2_PLACEMENT_3D('',#3873,#3874,#3875);

+#3877=PLANE('',#3876);

+#3878=ORIENTED_EDGE('',*,*,#3715,.F.);

+#3879=ORIENTED_EDGE('',*,*,#2074,.T.);

+#3880=ORIENTED_EDGE('',*,*,#2440,.T.);

+#3881=ORIENTED_EDGE('',*,*,#3684,.F.);

+#3882=EDGE_LOOP('',(#3878,#3879,#3880,#3881));

+#3883=FACE_OUTER_BOUND('',#3882,.F.);

+#3884=ADVANCED_FACE('',(#3883),#3877,.T.);

+#3885=CARTESIAN_POINT('',(-9.5E-2,-1.E-1,-2.25E-1));

+#3886=DIRECTION('',(-7.071067811865E-1,-7.071067811865E-1,0.E0));

+#3887=DIRECTION('',(7.071067811865E-1,-7.071067811865E-1,0.E0));

+#3888=AXIS2_PLACEMENT_3D('',#3885,#3886,#3887);

+#3889=PLANE('',#3888);

+#3890=ORIENTED_EDGE('',*,*,#3827,.F.);

+#3891=ORIENTED_EDGE('',*,*,#3700,.F.);

+#3892=ORIENTED_EDGE('',*,*,#1978,.F.);

+#3893=ORIENTED_EDGE('',*,*,#2067,.T.);

+#3894=EDGE_LOOP('',(#3890,#3891,#3892,#3893));

+#3895=FACE_OUTER_BOUND('',#3894,.F.);

+#3896=ADVANCED_FACE('',(#3895),#3889,.T.);

+#3897=CARTESIAN_POINT('',(-1.04E-1,-1.E-1,-5.5E-3));

+#3898=DIRECTION('',(-1.E0,0.E0,0.E0));

+#3899=DIRECTION('',(0.E0,0.E0,-1.E0));

+#3900=AXIS2_PLACEMENT_3D('',#3897,#3898,#3899);

+#3901=PLANE('',#3900);

+#3902=ORIENTED_EDGE('',*,*,#3783,.T.);

+#3903=ORIENTED_EDGE('',*,*,#3830,.F.);

+#3904=ORIENTED_EDGE('',*,*,#2063,.T.);

+#3906=ORIENTED_EDGE('',*,*,#3905,.T.);

+#3907=EDGE_LOOP('',(#3902,#3903,#3904,#3906));

+#3908=FACE_OUTER_BOUND('',#3907,.F.);

+#3909=ADVANCED_FACE('',(#3908),#3901,.F.);

+#3910=CARTESIAN_POINT('',(-1.04E-1,-1.E-1,-8.3E-3));

+#3911=DIRECTION('',(0.E0,0.E0,1.E0));

+#3912=DIRECTION('',(-1.E0,0.E0,0.E0));

+#3913=AXIS2_PLACEMENT_3D('',#3910,#3911,#3912);

+#3914=PLANE('',#3913);

+#3915=ORIENTED_EDGE('',*,*,#3785,.T.);

+#3916=ORIENTED_EDGE('',*,*,#3905,.F.);

+#3917=ORIENTED_EDGE('',*,*,#2061,.T.);

+#3919=ORIENTED_EDGE('',*,*,#3918,.T.);

+#3920=EDGE_LOOP('',(#3915,#3916,#3917,#3919));

+#3921=FACE_OUTER_BOUND('',#3920,.F.);

+#3922=ADVANCED_FACE('',(#3921),#3914,.F.);

+#3923=CARTESIAN_POINT('',(-1.06E-1,-1.E-1,-8.3E-3));

+#3924=DIRECTION('',(1.E0,0.E0,0.E0));

+#3925=DIRECTION('',(0.E0,0.E0,1.E0));

+#3926=AXIS2_PLACEMENT_3D('',#3923,#3924,#3925);

+#3927=PLANE('',#3926);

+#3928=ORIENTED_EDGE('',*,*,#3787,.T.);

+#3929=ORIENTED_EDGE('',*,*,#3918,.F.);

+#3930=ORIENTED_EDGE('',*,*,#2059,.T.);

+#3931=ORIENTED_EDGE('',*,*,#3762,.T.);

+#3932=EDGE_LOOP('',(#3928,#3929,#3930,#3931));

+#3933=FACE_OUTER_BOUND('',#3932,.F.);

+#3934=ADVANCED_FACE('',(#3933),#3927,.F.);

+#3935=CARTESIAN_POINT('',(-1.574234044424E-1,-1.E-1,8.356794919243E-2));

+#3936=DIRECTION('',(5.E-1,0.E0,8.660254037844E-1));

+#3937=DIRECTION('',(-8.660254037844E-1,0.E0,5.E-1));

+#3938=AXIS2_PLACEMENT_3D('',#3935,#3936,#3937);

+#3939=PLANE('',#3938);

+#3940=ORIENTED_EDGE('',*,*,#3731,.T.);

+#3941=ORIENTED_EDGE('',*,*,#3765,.F.);

+#3942=ORIENTED_EDGE('',*,*,#2082,.T.);

+#3944=ORIENTED_EDGE('',*,*,#3943,.T.);

+#3945=EDGE_LOOP('',(#3940,#3941,#3942,#3944));

+#3946=FACE_OUTER_BOUND('',#3945,.F.);

+#3947=ADVANCED_FACE('',(#3946),#3939,.F.);

+#3948=CARTESIAN_POINT('',(-1.598482755730E-1,-1.E-1,8.496794919243E-2));

+#3949=DIRECTION('',(8.660254037844E-1,0.E0,-5.E-1));

+#3950=DIRECTION('',(5.E-1,0.E0,8.660254037844E-1));

+#3951=AXIS2_PLACEMENT_3D('',#3948,#3949,#3950);

+#3952=PLANE('',#3951);

+#3953=ORIENTED_EDGE('',*,*,#3733,.T.);

+#3954=ORIENTED_EDGE('',*,*,#3943,.F.);

+#3955=ORIENTED_EDGE('',*,*,#2080,.T.);

+#3957=ORIENTED_EDGE('',*,*,#3956,.T.);

+#3958=EDGE_LOOP('',(#3953,#3954,#3955,#3957));

+#3959=FACE_OUTER_BOUND('',#3958,.F.);

+#3960=ADVANCED_FACE('',(#3959),#3952,.F.);

+#3961=CARTESIAN_POINT('',(-1.588482755730E-1,-1.E-1,8.67E-2));

+#3962=DIRECTION('',(-5.E-1,0.E0,-8.660254037844E-1));

+#3963=DIRECTION('',(8.660254037844E-1,0.E0,-5.E-1));

+#3964=AXIS2_PLACEMENT_3D('',#3961,#3962,#3963);

+#3965=PLANE('',#3964);

+#3966=ORIENTED_EDGE('',*,*,#3735,.T.);

+#3967=ORIENTED_EDGE('',*,*,#3956,.F.);

+#3968=ORIENTED_EDGE('',*,*,#2078,.T.);

+#3969=ORIENTED_EDGE('',*,*,#3712,.T.);

+#3970=EDGE_LOOP('',(#3966,#3967,#3968,#3969));

+#3971=FACE_OUTER_BOUND('',#3970,.F.);

+#3972=ADVANCED_FACE('',(#3971),#3965,.F.);

+#3973=CARTESIAN_POINT('',(2.5E-2,-1.E-1,2.25E-1));

+#3974=DIRECTION('',(1.E0,0.E0,0.E0));

+#3975=DIRECTION('',(0.E0,1.E0,0.E0));

+#3976=AXIS2_PLACEMENT_3D('',#3973,#3974,#3975);

+#3977=PLANE('',#3976);

+#3979=ORIENTED_EDGE('',*,*,#3978,.T.);

+#3981=ORIENTED_EDGE('',*,*,#3980,.F.);

+#3982=ORIENTED_EDGE('',*,*,#2461,.F.);

+#3983=ORIENTED_EDGE('',*,*,#2098,.T.);

+#3984=EDGE_LOOP('',(#3979,#3981,#3982,#3983));

+#3985=FACE_OUTER_BOUND('',#3984,.F.);

+#3986=ADVANCED_FACE('',(#3985),#3977,.F.);

+#3987=CARTESIAN_POINT('',(0.E0,0.E0,-1.25E-1));

+#3988=DIRECTION('',(0.E0,0.E0,1.E0));

+#3989=DIRECTION('',(1.E0,0.E0,0.E0));

+#3990=AXIS2_PLACEMENT_3D('',#3987,#3988,#3989);

+#3991=PLANE('',#3990);

+#3993=ORIENTED_EDGE('',*,*,#3992,.F.);

+#3995=ORIENTED_EDGE('',*,*,#3994,.F.);

+#3996=ORIENTED_EDGE('',*,*,#3978,.F.);

+#3997=ORIENTED_EDGE('',*,*,#2096,.F.);

+#3998=EDGE_LOOP('',(#3993,#3995,#3996,#3997));

+#3999=FACE_OUTER_BOUND('',#3998,.F.);

+#4000=ADVANCED_FACE('',(#3999),#3991,.T.);

+#4001=CARTESIAN_POINT('',(-2.5E-2,-6.E-2,2.25E-1));

+#4002=DIRECTION('',(-1.E0,0.E0,0.E0));

+#4003=DIRECTION('',(0.E0,-1.E0,0.E0));

+#4004=AXIS2_PLACEMENT_3D('',#4001,#4002,#4003);

+#4005=PLANE('',#4004);

+#4006=ORIENTED_EDGE('',*,*,#3992,.T.);

+#4007=ORIENTED_EDGE('',*,*,#2094,.F.);

+#4008=ORIENTED_EDGE('',*,*,#2465,.F.);

+#4010=ORIENTED_EDGE('',*,*,#4009,.T.);

+#4011=EDGE_LOOP('',(#4006,#4007,#4008,#4010));

+#4012=FACE_OUTER_BOUND('',#4011,.F.);

+#4013=ADVANCED_FACE('',(#4012),#4005,.F.);

+#4014=CARTESIAN_POINT('',(2.5E-2,-6.E-2,2.25E-1));

+#4015=DIRECTION('',(0.E0,1.E0,0.E0));

+#4016=DIRECTION('',(-1.E0,0.E0,0.E0));

+#4017=AXIS2_PLACEMENT_3D('',#4014,#4015,#4016);

+#4018=PLANE('',#4017);

+#4019=ORIENTED_EDGE('',*,*,#2463,.F.);

+#4020=ORIENTED_EDGE('',*,*,#3980,.T.);

+#4021=ORIENTED_EDGE('',*,*,#3994,.T.);

+#4022=ORIENTED_EDGE('',*,*,#4009,.F.);

+#4023=EDGE_LOOP('',(#4019,#4020,#4021,#4022));

+#4024=FACE_OUTER_BOUND('',#4023,.F.);

+#4025=ADVANCED_FACE('',(#4024),#4018,.F.);

+#4026=CLOSED_SHELL('',(#2016,#2051,#2086,#2133,#2150,#2173,#2186,#2199,#2212,

+#2232,#2247,#2260,#2279,#2306,#2319,#2330,#2343,#2356,#2368,#2381,#2394,#2406,

+#2419,#2431,#2479,#2494,#2512,#2535,#2550,#2566,#2583,#2607,#2647,#2662,#2676,

+#2695,#2707,#2725,#2743,#2758,#2782,#2797,#2810,#2822,#2837,#2850,#2862,#2876,

+#2898,#2911,#2924,#2936,#2949,#2961,#2974,#2986,#3001,#3017,#3031,#3045,#3058,

+#3070,#3086,#3104,#3119,#3139,#3153,#3167,#3183,#3214,#3255,#3270,#3285,#3299,

+#3313,#3328,#3341,#3353,#3371,#3385,#3402,#3416,#3429,#3446,#3461,#3473,#3488,

+#3502,#3515,#3531,#3544,#3556,#3572,#3588,#3600,#3612,#3625,#3640,#3652,#3664,

+#3676,#3690,#3704,#3723,#3754,#3770,#3791,#3804,#3815,#3834,#3847,#3860,#3872,

+#3884,#3896,#3909,#3922,#3934,#3947,#3960,#3972,#3986,#4000,#4013,#4025));

+#4027=MANIFOLD_SOLID_BREP('',#4026);

+#4028=CARTESIAN_POINT('',(0.E0,0.E0,0.E0));

+#4029=DIRECTION('',(0.E0,0.E0,1.E0));

+#4030=DIRECTION('',(1.E0,0.E0,0.E0));

+#4031=AXIS2_PLACEMENT_3D('CSYS',#4028,#4029,#4030);

+#4032=DIRECTION('',(0.E0,1.E0,0.E0));

+#4033=VECTOR('',#4032,6.6E-2);

+#4034=CARTESIAN_POINT('',(9.434827557301E-2,-2.05E-1,4.63E-2));

+#4035=LINE('',#4034,#4033);

+#4038=DIRECTION('',(0.E0,1.E0,0.E0));

+#4039=VECTOR('',#4038,6.6E-2);

+#4040=CARTESIAN_POINT('',(-9.434827557301E-2,-2.05E-1,4.63E-2));

+#4041=LINE('',#4040,#4039);

+#4043=CARTESIAN_POINT('',(5.E-2,-1.E-1,-2.825E-1));

+#4044=DIRECTION('',(0.E0,-1.E0,0.E0));

+#4045=DIRECTION('',(-1.E0,0.E0,0.E0));

+#4046=AXIS2_PLACEMENT_3D('',#4043,#4044,#4045);

+#4050=CARTESIAN_POINT('',(5.E-2,-1.E-1,-2.825E-1));

+#4051=DIRECTION('',(0.E0,-1.E0,0.E0));

+#4052=DIRECTION('',(1.E0,0.E0,0.E0));

+#4053=AXIS2_PLACEMENT_3D('',#4050,#4051,#4052);

+#4058=CARTESIAN_POINT('',(-5.E-2,-1.E-1,-2.825E-1));

+#4059=DIRECTION('',(0.E0,-1.E0,0.E0));

+#4060=DIRECTION('',(-1.E0,0.E0,0.E0));

+#4061=AXIS2_PLACEMENT_3D('',#4058,#4059,#4060);

+#4065=CARTESIAN_POINT('',(-5.E-2,-1.E-1,-2.825E-1));

+#4066=DIRECTION('',(0.E0,-1.E0,0.E0));

+#4067=DIRECTION('',(1.E0,0.E0,0.E0));

+#4068=AXIS2_PLACEMENT_3D('',#4065,#4066,#4067);

+#4073=CARTESIAN_POINT('',(5.E-2,-2.45E-1,-2.825E-1));

+#4074=DIRECTION('',(0.E0,0.E0,1.E0));

+#4075=DIRECTION('',(1.E0,0.E0,0.E0));

+#4076=AXIS2_PLACEMENT_3D('CS0',#4073,#4074,#4075);

+#4077=CARTESIAN_POINT('',(1.05E-1,-1.E-1,5.75E-2));

+#4078=DIRECTION('',(0.E0,-1.E0,0.E0));

+#4079=DIRECTION('',(-1.E0,0.E0,0.E0));

+#4080=AXIS2_PLACEMENT_3D('',#4077,#4078,#4079);

+#4084=CARTESIAN_POINT('',(1.05E-1,-1.E-1,5.75E-2));

+#4085=DIRECTION('',(0.E0,-1.E0,0.E0));

+#4086=DIRECTION('',(1.E0,0.E0,0.E0));

+#4087=AXIS2_PLACEMENT_3D('',#4084,#4085,#4086);

+#4092=CARTESIAN_POINT('',(-1.05E-1,-1.E-1,5.75E-2));

+#4093=DIRECTION('',(0.E0,-1.E0,0.E0));

+#4094=DIRECTION('',(-1.E0,0.E0,0.E0));

+#4095=AXIS2_PLACEMENT_3D('',#4092,#4093,#4094);

+#4099=CARTESIAN_POINT('',(-1.05E-1,-1.E-1,5.75E-2));

+#4100=DIRECTION('',(0.E0,-1.E0,0.E0));

+#4101=DIRECTION('',(1.E0,0.E0,0.E0));

+#4102=AXIS2_PLACEMENT_3D('',#4099,#4100,#4101);

+#4107=DIMENSIONAL_EXPONENTS(1.E0,0.E0,0.E0,0.E0,0.E0,0.E0,0.E0);

+#4109=LENGTH_MEASURE_WITH_UNIT(LENGTH_MEASURE(2.54E1),#4108);

+#4110=(CONVERSION_BASED_UNIT('INCH',#4109)LENGTH_UNIT()NAMED_UNIT(#4107));

+#4111=DIMENSIONAL_EXPONENTS(0.E0,0.E0,0.E0,0.E0,0.E0,0.E0,0.E0);

+#4113=PLANE_ANGLE_MEASURE_WITH_UNIT(PLANE_ANGLE_MEASURE(1.745329251994E-2),

+#4112);

+#4114=(CONVERSION_BASED_UNIT('DEGREE',#4113)NAMED_UNIT(*)PLANE_ANGLE_UNIT());

+#4116=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(7.540162558127E-5),#4110,

+'closure',

+'Maximum model space distance between geometric entities at asserted connectivities');

+#4117=(GEOMETRIC_REPRESENTATION_CONTEXT(3)GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((

+#4116))GLOBAL_UNIT_ASSIGNED_CONTEXT((#4110,#4114,#4115))REPRESENTATION_CONTEXT(

+'ID1','3'));

+#4037=GEOMETRIC_SET('',(#4036,#4042,#4057,#4072,#4091,#4106));

+#4121=SHAPE_REPRESENTATION_RELATIONSHIP('','',#4120,#4118);

+#4122=SHAPE_REPRESENTATION_RELATIONSHIP('','',#4120,#4119);

+#4123=APPLICATION_CONTEXT(

+'CONFIGURATION CONTROLLED 3D DESIGNS OF MECHANICAL PARTS AND ASSEMBLIES');

+#4124=APPLICATION_PROTOCOL_DEFINITION('international standard',

+'config_control_design',1994,#4123);

+#4125=DESIGN_CONTEXT('',#4123,'design');

+#4126=MECHANICAL_CONTEXT('',#4123,'mechanical');

+#4127=PRODUCT('705550036','705550036','NOT SPECIFIED',(#4126));

+#4128=PRODUCT_DEFINITION_FORMATION_WITH_SPECIFIED_SOURCE('1','LAST_VERSION',

+#4127,.MADE.);

+#4132=PRODUCT_CATEGORY('part','');

+#4133=PRODUCT_RELATED_PRODUCT_CATEGORY('detail','',(#4127));

+#4134=PRODUCT_CATEGORY_RELATIONSHIP('','',#4132,#4133);

+#4135=SECURITY_CLASSIFICATION_LEVEL('unclassified');

+#4136=SECURITY_CLASSIFICATION('','',#4135);

+#4137=CC_DESIGN_SECURITY_CLASSIFICATION(#4136,(#4128));

+#4138=APPROVAL_STATUS('approved');

+#4139=APPROVAL(#4138,'');

+#4140=CC_DESIGN_APPROVAL(#4139,(#4136,#4128,#4129));

+#4141=CALENDAR_DATE(116,6,12);

+#4142=COORDINATED_UNIVERSAL_TIME_OFFSET(5,30,.AHEAD.);

+#4143=LOCAL_TIME(12,37,5.2E1,#4142);

+#4144=DATE_AND_TIME(#4141,#4143);

+#4145=APPROVAL_DATE_TIME(#4144,#4139);

+#4146=DATE_TIME_ROLE('creation_date');

+#4147=CC_DESIGN_DATE_AND_TIME_ASSIGNMENT(#4144,#4146,(#4129));

+#4148=DATE_TIME_ROLE('classification_date');

+#4149=CC_DESIGN_DATE_AND_TIME_ASSIGNMENT(#4144,#4148,(#4136));

+#4150=PERSON('UNSPECIFIED','UNSPECIFIED',$,$,$,$);

+#4151=ORGANIZATION('UNSPECIFIED','UNSPECIFIED','UNSPECIFIED');

+#4152=PERSON_AND_ORGANIZATION(#4150,#4151);

+#4153=APPROVAL_ROLE('approver');

+#4154=APPROVAL_PERSON_ORGANIZATION(#4152,#4139,#4153);

+#4155=PERSON_AND_ORGANIZATION_ROLE('creator');

+#4156=CC_DESIGN_PERSON_AND_ORGANIZATION_ASSIGNMENT(#4152,#4155,(#4128,#4129));

+#4157=PERSON_AND_ORGANIZATION_ROLE('design_supplier');

+#4158=CC_DESIGN_PERSON_AND_ORGANIZATION_ASSIGNMENT(#4152,#4157,(#4128));

+#4159=PERSON_AND_ORGANIZATION_ROLE('classification_officer');

+#4160=CC_DESIGN_PERSON_AND_ORGANIZATION_ASSIGNMENT(#4152,#4159,(#4136));

+#4161=PERSON_AND_ORGANIZATION_ROLE('design_owner');

+#4162=CC_DESIGN_PERSON_AND_ORGANIZATION_ASSIGNMENT(#4152,#4161,(#4127));

+#245=B_SPLINE_CURVE_WITH_KNOTS('',3,(#233,#234,#235,#236,#237,#238,#239,#240,

+#241,#242,#243,#244),.UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,4),(0.E0,

+1.111111111111E-1,2.222222222222E-1,3.333333333333E-1,4.444444444444E-1,

+5.555555555556E-1,6.666666666667E-1,7.777777777778E-1,8.888888888889E-1,1.E0),

+.UNSPECIFIED.);

+#254=B_SPLINE_CURVE_WITH_KNOTS('',3,(#246,#247,#248,#249,#250,#251,#252,#253),

+.UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,4),(0.E0,2.E-1,4.E-1,6.E-1,8.E-1,1.E0),

+.UNSPECIFIED.);

+#264=B_SPLINE_CURVE_WITH_KNOTS('',3,(#259,#260,#261,#262,#263),.UNSPECIFIED.,

+.F.,.F.,(4,1,4),(0.E0,5.E-1,1.E0),.UNSPECIFIED.);

+#274=B_SPLINE_CURVE_WITH_KNOTS('',3,(#269,#270,#271,#272,#273),.UNSPECIFIED.,

+.F.,.F.,(4,1,4),(0.E0,5.E-1,1.E0),.UNSPECIFIED.);

+#284=B_SPLINE_CURVE_WITH_KNOTS('',3,(#279,#280,#281,#282,#283),.UNSPECIFIED.,

+.F.,.F.,(4,1,4),(0.E0,5.E-1,1.E0),.UNSPECIFIED.);

+#303=B_SPLINE_CURVE_WITH_KNOTS('',3,(#289,#290,#291,#292,#293,#294,#295,#296,

+#297,#298,#299,#300,#301,#302),.UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,4),(

+0.E0,9.090909090909E-2,1.818181818182E-1,2.727272727273E-1,3.636363636364E-1,

+4.545454545455E-1,5.454545454545E-1,6.363636363636E-1,7.272727272727E-1,

+8.181818181818E-1,9.090909090909E-1,1.E0),.UNSPECIFIED.);

+#318=B_SPLINE_CURVE_WITH_KNOTS('',3,(#308,#309,#310,#311,#312,#313,#314,#315,

+#316,#317),.UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,4),(0.E0,1.428571428571E-1,

+2.857142857143E-1,4.285714285714E-1,5.714285714286E-1,7.142857142857E-1,

+8.571428571429E-1,1.E0),.UNSPECIFIED.);

+#351=B_SPLINE_CURVE_WITH_KNOTS('',3,(#335,#336,#337,#338,#339,#340,#341,#342,

+#343,#344,#345,#346,#347,#348,#349,#350),.UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,

+1,1,1,1,1,4),(0.E0,7.692307692308E-2,1.538461538462E-1,2.307692307692E-1,

+3.076923076923E-1,3.846153846154E-1,4.615384615385E-1,5.384615384615E-1,

+6.153846153846E-1,6.923076923077E-1,7.692307692308E-1,8.461538461538E-1,

+9.230769230769E-1,1.E0),.UNSPECIFIED.);

+#368=CIRCLE('',#367,2.E-2);

+#373=CIRCLE('',#372,2.E-2);

+#378=CIRCLE('',#377,1.272775837960E-2);

+#383=CIRCLE('',#382,1.272775837960E-2);

+#389=B_SPLINE_CURVE_WITH_KNOTS('',3,(#384,#385,#386,#387,#388),.UNSPECIFIED.,

+.F.,.F.,(4,1,4),(0.E0,5.E-1,1.E0),.UNSPECIFIED.);

+#399=B_SPLINE_CURVE_WITH_KNOTS('',3,(#394,#395,#396,#397,#398),.UNSPECIFIED.,

+.F.,.F.,(4,1,4),(0.E0,5.E-1,1.E0),.UNSPECIFIED.);

+#409=B_SPLINE_CURVE_WITH_KNOTS('',3,(#404,#405,#406,#407,#408),.UNSPECIFIED.,

+.F.,.F.,(4,1,4),(0.E0,5.E-1,1.E0),.UNSPECIFIED.);

+#419=B_SPLINE_CURVE_WITH_KNOTS('',3,(#414,#415,#416,#417,#418),.UNSPECIFIED.,

+.F.,.F.,(4,1,4),(0.E0,5.E-1,1.E0),.UNSPECIFIED.);

+#429=B_SPLINE_CURVE_WITH_KNOTS('',3,(#424,#425,#426,#427,#428),.UNSPECIFIED.,

+.F.,.F.,(4,1,4),(0.E0,5.E-1,1.E0),.UNSPECIFIED.);

+#439=B_SPLINE_CURVE_WITH_KNOTS('',3,(#434,#435,#436,#437,#438),.UNSPECIFIED.,

+.F.,.F.,(4,1,4),(0.E0,5.E-1,1.E0),.UNSPECIFIED.);

+#736=CIRCLE('',#735,5.3E-2);

+#745=CIRCLE('',#744,5.3E-2);

+#798=CIRCLE('',#797,5.3E-2);

+#807=CIRCLE('',#806,5.3E-2);

+#936=CIRCLE('',#935,2.5E-2);

+#949=CIRCLE('',#948,5.E-2);

+#1122=CIRCLE('',#1121,2.5E-2);

+#1135=CIRCLE('',#1134,5.E-2);

+#1144=CIRCLE('',#1143,2.5E-2);

+#1165=CIRCLE('',#1164,5.E-2);

+#1242=CIRCLE('',#1241,2.5E-2);

+#1263=CIRCLE('',#1262,5.E-2);

+#1336=B_SPLINE_CURVE_WITH_KNOTS('',3,(#1320,#1321,#1322,#1323,#1324,#1325,#1326,

+#1327,#1328,#1329,#1330,#1331,#1332,#1333,#1334,#1335),.UNSPECIFIED.,.F.,.F.,(4,

+1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E0,7.692307692308E-2,1.538461538462E-1,

+2.307692307692E-1,3.076923076923E-1,3.846153846154E-1,4.615384615385E-1,

+5.384615384615E-1,6.153846153846E-1,6.923076923077E-1,7.692307692308E-1,

+8.461538461538E-1,9.230769230769E-1,1.E0),.UNSPECIFIED.);

+#1361=B_SPLINE_CURVE_WITH_KNOTS('',3,(#1349,#1350,#1351,#1352,#1353,#1354,#1355,

+#1356,#1357,#1358,#1359,#1360),.UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,4),

+(0.E0,1.111111111111E-1,2.222222222222E-1,3.333333333333E-1,4.444444444444E-1,

+5.555555555556E-1,6.666666666667E-1,7.777777777778E-1,8.888888888889E-1,1.E0),

+.UNSPECIFIED.);

+#1374=B_SPLINE_CURVE_WITH_KNOTS('',3,(#1366,#1367,#1368,#1369,#1370,#1371,#1372,

+#1373),.UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,4),(0.E0,2.E-1,4.E-1,6.E-1,8.E-1,1.E0),

+.UNSPECIFIED.);

+#1379=CIRCLE('',#1378,1.272775837960E-2);

+#1384=CIRCLE('',#1383,2.E-2);

+#1389=CIRCLE('',#1388,1.272775837960E-2);

+#1394=CIRCLE('',#1393,2.E-2);

+#1409=B_SPLINE_CURVE_WITH_KNOTS('',3,(#1395,#1396,#1397,#1398,#1399,#1400,#1401,

+#1402,#1403,#1404,#1405,#1406,#1407,#1408),.UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,

+1,1,1,1,4),(0.E0,9.090909090909E-2,1.818181818182E-1,2.727272727273E-1,

+3.636363636364E-1,4.545454545455E-1,5.454545454545E-1,6.363636363636E-1,

+7.272727272727E-1,8.181818181818E-1,9.090909090909E-1,1.E0),.UNSPECIFIED.);

+#1420=B_SPLINE_CURVE_WITH_KNOTS('',3,(#1410,#1411,#1412,#1413,#1414,#1415,#1416,

+#1417,#1418,#1419),.UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,4),(0.E0,

+1.428571428571E-1,2.857142857143E-1,4.285714285714E-1,5.714285714286E-1,

+7.142857142857E-1,8.571428571429E-1,1.E0),.UNSPECIFIED.);

+#1430=B_SPLINE_CURVE_WITH_KNOTS('',3,(#1425,#1426,#1427,#1428,#1429),

+.UNSPECIFIED.,.F.,.F.,(4,1,4),(0.E0,5.E-1,1.E0),.UNSPECIFIED.);

+#1440=B_SPLINE_CURVE_WITH_KNOTS('',3,(#1435,#1436,#1437,#1438,#1439),

+.UNSPECIFIED.,.F.,.F.,(4,1,4),(0.E0,5.E-1,1.E0),.UNSPECIFIED.);

+#1450=B_SPLINE_CURVE_WITH_KNOTS('',3,(#1445,#1446,#1447,#1448,#1449),

+.UNSPECIFIED.,.F.,.F.,(4,1,4),(0.E0,5.E-1,1.E0),.UNSPECIFIED.);

+#1468=B_SPLINE_CURVE_WITH_KNOTS('',3,(#1463,#1464,#1465,#1466,#1467),

+.UNSPECIFIED.,.F.,.F.,(4,1,4),(0.E0,5.E-1,1.E0),.UNSPECIFIED.);

+#1478=B_SPLINE_CURVE_WITH_KNOTS('',3,(#1473,#1474,#1475,#1476,#1477),

+.UNSPECIFIED.,.F.,.F.,(4,1,4),(0.E0,5.E-1,1.E0),.UNSPECIFIED.);

+#1488=B_SPLINE_CURVE_WITH_KNOTS('',3,(#1483,#1484,#1485,#1486,#1487),

+.UNSPECIFIED.,.F.,.F.,(4,1,4),(0.E0,5.E-1,1.E0),.UNSPECIFIED.);

+#1498=B_SPLINE_CURVE_WITH_KNOTS('',3,(#1493,#1494,#1495,#1496,#1497),

+.UNSPECIFIED.,.F.,.F.,(4,1,4),(0.E0,5.E-1,1.E0),.UNSPECIFIED.);

+#1508=B_SPLINE_CURVE_WITH_KNOTS('',3,(#1503,#1504,#1505,#1506,#1507),

+.UNSPECIFIED.,.F.,.F.,(4,1,4),(0.E0,5.E-1,1.E0),.UNSPECIFIED.);

+#1518=B_SPLINE_CURVE_WITH_KNOTS('',3,(#1513,#1514,#1515,#1516,#1517),

+.UNSPECIFIED.,.F.,.F.,(4,1,4),(0.E0,5.E-1,1.E0),.UNSPECIFIED.);

+#1976=EDGE_CURVE('',#1692,#1728,#4,.T.);

+#1978=EDGE_CURVE('',#1728,#1742,#8,.T.);

+#1980=EDGE_CURVE('',#1742,#1731,#12,.T.);

+#1982=EDGE_CURVE('',#1731,#1732,#16,.T.);

+#1984=EDGE_CURVE('',#1732,#1740,#20,.T.);

+#1986=EDGE_CURVE('',#1740,#1734,#24,.T.);

+#1988=EDGE_CURVE('',#1734,#1695,#28,.T.);

+#1990=EDGE_CURVE('',#1695,#1696,#32,.T.);

+#1992=EDGE_CURVE('',#1696,#1606,#36,.T.);

+#1994=EDGE_CURVE('',#1606,#1597,#40,.T.);

+#1996=EDGE_CURVE('',#1597,#1598,#44,.T.);

+#1998=EDGE_CURVE('',#1598,#1604,#48,.T.);

+#2000=EDGE_CURVE('',#1604,#1690,#52,.T.);

+#2002=EDGE_CURVE('',#1690,#1692,#56,.T.);

+#2006=EDGE_CURVE('',#1711,#1712,#60,.T.);

+#2008=EDGE_CURVE('',#1712,#1716,#64,.T.);

+#2010=EDGE_CURVE('',#1715,#1716,#68,.T.);

+#2012=EDGE_CURVE('',#1711,#1715,#72,.T.);

+#2022=EDGE_CURVE('',#1765,#1766,#76,.T.);

+#2024=EDGE_CURVE('',#1766,#1768,#80,.T.);

+#2026=EDGE_CURVE('',#1768,#1770,#84,.T.);

+#2028=EDGE_CURVE('',#1770,#1772,#88,.T.);

+#2030=EDGE_CURVE('',#1772,#1774,#92,.T.);

+#2032=EDGE_CURVE('',#1774,#1738,#96,.T.);

+#2034=EDGE_CURVE('',#1738,#1703,#503,.T.);

+#2036=EDGE_CURVE('',#1695,#1703,#765,.T.);

+#2039=EDGE_CURVE('',#1734,#1796,#100,.T.);

+#2041=EDGE_CURVE('',#1796,#1818,#104,.T.);

+#2043=EDGE_CURVE('',#1818,#1789,#108,.T.);

+#2045=EDGE_CURVE('',#1789,#1790,#112,.T.);

+#2047=EDGE_CURVE('',#1790,#1765,#116,.T.);

+#2057=EDGE_CURVE('',#1821,#1822,#120,.T.);

+#2059=EDGE_CURVE('',#1846,#1821,#124,.T.);

+#2061=EDGE_CURVE('',#1845,#1846,#128,.T.);

+#2063=EDGE_CURVE('',#1874,#1845,#132,.T.);

+#2065=EDGE_CURVE('',#1852,#1874,#136,.T.);

+#2067=EDGE_CURVE('',#1728,#1852,#140,.T.);

+#2070=EDGE_CURVE('',#1692,#1700,#144,.T.);

+#2072=EDGE_CURVE('',#1700,#1736,#471,.T.);

+#2074=EDGE_CURVE('',#1830,#1736,#148,.T.);

+#2076=EDGE_CURVE('',#1828,#1830,#152,.T.);

+#2078=EDGE_CURVE('',#1826,#1828,#156,.T.);

+#2080=EDGE_CURVE('',#1824,#1826,#160,.T.);

+#2082=EDGE_CURVE('',#1822,#1824,#164,.T.);

+#2092=EDGE_CURVE('',#1750,#1756,#459,.T.);

+#2094=EDGE_CURVE('',#1756,#1758,#168,.T.);

+#2096=EDGE_CURVE('',#1758,#1762,#172,.T.);

+#2098=EDGE_CURVE('',#1921,#1762,#176,.T.);

+#2100=EDGE_CURVE('',#1921,#1745,#515,.T.);

+#2102=EDGE_CURVE('',#1777,#1745,#180,.T.);

+#2104=EDGE_CURVE('',#1777,#1778,#184,.T.);

+#2106=EDGE_CURVE('',#1778,#1780,#188,.T.);

+#2108=EDGE_CURVE('',#1780,#1782,#192,.T.);

+#2110=EDGE_CURVE('',#1782,#1784,#196,.T.);

+#2112=EDGE_CURVE('',#1784,#1786,#200,.T.);

+#2114=EDGE_CURVE('',#1732,#1786,#204,.T.);

+#2117=EDGE_CURVE('',#1731,#1842,#208,.T.);

+#2119=EDGE_CURVE('',#1840,#1842,#212,.T.);

+#2121=EDGE_CURVE('',#1838,#1840,#216,.T.);

+#2123=EDGE_CURVE('',#1836,#1838,#220,.T.);

+#2125=EDGE_CURVE('',#1834,#1836,#224,.T.);

+#2127=EDGE_CURVE('',#1833,#1834,#228,.T.);

+#2129=EDGE_CURVE('',#1833,#1750,#232,.T.);

+#2139=EDGE_CURVE('',#1803,#1804,#245,.T.);

+#2141=EDGE_CURVE('',#1805,#1803,#254,.T.);

+#2143=EDGE_CURVE('',#1766,#1805,#443,.T.);

+#2146=EDGE_CURVE('',#1765,#1804,#258,.T.);

+#2157=EDGE_CURVE('',#1804,#1816,#264,.T.);

+#2159=EDGE_CURVE('',#1816,#1815,#274,.T.);

+#2161=EDGE_CURVE('',#1815,#1813,#284,.T.);

+#2163=EDGE_CURVE('',#1813,#1814,#303,.T.);

+#2165=EDGE_CURVE('',#1814,#1802,#363,.T.);

+#2167=EDGE_CURVE('',#1799,#1802,#383,.T.);

+#2169=EDGE_CURVE('',#1803,#1799,#359,.T.);

+#2182=EDGE_CURVE('',#1790,#1816,#268,.T.);

+#2195=EDGE_CURVE('',#1789,#1815,#278,.T.);

+#2208=EDGE_CURVE('',#1818,#1813,#288,.T.);

+#2221=EDGE_CURVE('',#1794,#1796,#330,.T.);

+#2223=EDGE_CURVE('',#1786,#1794,#451,.T.);

+#2226=EDGE_CURVE('',#1784,#1812,#307,.T.);

+#2228=EDGE_CURVE('',#1814,#1812,#318,.T.);

+#2238=EDGE_CURVE('',#1792,#1774,#322,.T.);

+#2240=EDGE_CURVE('',#1792,#1746,#326,.T.);

+#2242=EDGE_CURVE('',#1746,#1738,#507,.T.);

+#2256=EDGE_CURVE('',#1740,#1794,#334,.T.);

+#2266=EDGE_CURVE('',#1809,#1808,#351,.T.);

+#2268=EDGE_CURVE('',#1778,#1809,#413,.T.);

+#2271=EDGE_CURVE('',#1777,#1792,#447,.T.);

+#2275=EDGE_CURVE('',#1772,#1808,#355,.T.);

+#2287=EDGE_CURVE('',#1799,#1802,#378,.T.);

+#2291=EDGE_CURVE('',#1812,#1811,#389,.T.);

+#2293=EDGE_CURVE('',#1811,#1810,#399,.T.);

+#2295=EDGE_CURVE('',#1810,#1809,#409,.T.);

+#2298=EDGE_CURVE('',#1808,#1807,#419,.T.);

+#2300=EDGE_CURVE('',#1807,#1806,#429,.T.);

+#2302=EDGE_CURVE('',#1806,#1805,#439,.T.);

+#2312=EDGE_CURVE('',#1799,#1800,#368,.T.);

+#2314=EDGE_CURVE('',#1802,#1800,#373,.T.);

+#2339=EDGE_CURVE('',#1782,#1811,#393,.T.);

+#2352=EDGE_CURVE('',#1780,#1810,#403,.T.);

+#2377=EDGE_CURVE('',#1770,#1807,#423,.T.);

+#2390=EDGE_CURVE('',#1768,#1806,#433,.T.);

+#2414=EDGE_CURVE('',#1745,#1746,#511,.T.);

+#2438=EDGE_CURVE('',#1748,#1750,#463,.T.);

+#2440=EDGE_CURVE('',#1736,#1748,#467,.T.);

+#2443=EDGE_CURVE('',#1698,#1700,#475,.T.);

+#2445=EDGE_CURVE('',#1586,#1698,#479,.T.);

+#2447=EDGE_CURVE('',#1585,#1586,#483,.T.);

+#2449=EDGE_CURVE('',#1590,#1585,#487,.T.);

+#2451=EDGE_CURVE('',#1589,#1590,#491,.T.);

+#2453=EDGE_CURVE('',#1704,#1589,#495,.T.);

+#2455=EDGE_CURVE('',#1703,#1704,#499,.T.);

+#2461=EDGE_CURVE('',#1921,#1922,#519,.T.);

+#2463=EDGE_CURVE('',#1922,#1760,#523,.T.);

+#2465=EDGE_CURVE('',#1760,#1756,#527,.T.);

+#2469=EDGE_CURVE('',#1667,#1668,#531,.T.);

+#2471=EDGE_CURVE('',#1668,#1659,#831,.T.);

+#2473=EDGE_CURVE('',#1659,#1660,#843,.T.);

+#2475=EDGE_CURVE('',#1660,#1667,#455,.T.);

+#2486=EDGE_CURVE('',#1667,#1671,#535,.T.);

+#2488=EDGE_CURVE('',#1671,#1672,#539,.T.);

+#2490=EDGE_CURVE('',#1668,#1672,#543,.T.);

+#2500=EDGE_CURVE('',#1619,#1664,#551,.T.);

+#2502=EDGE_CURVE('',#1619,#1620,#615,.T.);

+#2504=EDGE_CURVE('',#1671,#1620,#683,.T.);

+#2508=EDGE_CURVE('',#1660,#1664,#851,.T.);

+#2518=EDGE_CURVE('',#1612,#1684,#563,.T.);

+#2520=EDGE_CURVE('',#1684,#1688,#871,.T.);

+#2522=EDGE_CURVE('',#1688,#1624,#547,.T.);

+#2524=EDGE_CURVE('',#1624,#1619,#619,.T.);

+#2527=EDGE_CURVE('',#1663,#1664,#847,.T.);

+#2529=EDGE_CURVE('',#1663,#1726,#555,.T.);

+#2531=EDGE_CURVE('',#1726,#1612,#707,.T.);

+#2541=EDGE_CURVE('',#1630,#1683,#559,.T.);

+#2543=EDGE_CURVE('',#1683,#1684,#859,.T.);

+#2546=EDGE_CURVE('',#1612,#1630,#583,.T.);

+#2556=EDGE_CURVE('',#1675,#1676,#667,.T.);

+#2558=EDGE_CURVE('',#1632,#1675,#567,.T.);

+#2560=EDGE_CURVE('',#1632,#1609,#595,.T.);

+#2562=EDGE_CURVE('',#1676,#1609,#571,.T.);

+#2572=EDGE_CURVE('',#1630,#1627,#579,.T.);

+#2574=EDGE_CURVE('',#1627,#1623,#627,.T.);

+#2576=EDGE_CURVE('',#1687,#1623,#855,.T.);

+#2578=EDGE_CURVE('',#1683,#1687,#863,.T.);

+#2589=EDGE_CURVE('',#1627,#1628,#575,.T.);

+#2593=EDGE_CURVE('',#1612,#1600,#587,.T.);

+#2595=EDGE_CURVE('',#1600,#1598,#773,.T.);

+#2598=EDGE_CURVE('',#1602,#1597,#753,.T.);

+#2600=EDGE_CURVE('',#1602,#1609,#591,.T.);

+#2603=EDGE_CURVE('',#1632,#1628,#599,.T.);

+#2614=EDGE_CURVE('',#1616,#1628,#603,.T.);

+#2616=EDGE_CURVE('',#1615,#1616,#607,.T.);

+#2618=EDGE_CURVE('',#1620,#1615,#611,.T.);

+#2622=EDGE_CURVE('',#1623,#1624,#623,.T.);

+#2627=EDGE_CURVE('',#1560,#1562,#631,.T.);

+#2629=EDGE_CURVE('',#1560,#1564,#635,.T.);

+#2631=EDGE_CURVE('',#1564,#1566,#639,.T.);

+#2633=EDGE_CURVE('',#1562,#1566,#643,.T.);

+#2637=EDGE_CURVE('',#1941,#1942,#647,.T.);

+#2639=EDGE_CURVE('',#1941,#1945,#651,.T.);

+#2641=EDGE_CURVE('',#1945,#1946,#655,.T.);

+#2643=EDGE_CURVE('',#1942,#1946,#659,.T.);

+#2655=EDGE_CURVE('',#1675,#1679,#679,.T.);

+#2657=EDGE_CURVE('',#1679,#1616,#663,.T.);

+#2669=EDGE_CURVE('',#1676,#1680,#671,.T.);

+#2671=EDGE_CURVE('',#1679,#1680,#675,.T.);

+#2684=EDGE_CURVE('',#1680,#1615,#687,.T.);

+#2688=EDGE_CURVE('',#1609,#1610,#699,.T.);

+#2690=EDGE_CURVE('',#1672,#1610,#815,.T.);

+#2713=EDGE_CURVE('',#1635,#1636,#691,.T.);

+#2715=EDGE_CURVE('',#1610,#1635,#695,.T.);

+#2719=EDGE_CURVE('',#1602,#1594,#749,.T.);

+#2721=EDGE_CURVE('',#1594,#1636,#719,.T.);

+#2731=EDGE_CURVE('',#1639,#1640,#703,.T.);

+#2733=EDGE_CURVE('',#1640,#1592,#727,.T.);

+#2735=EDGE_CURVE('',#1600,#1592,#777,.T.);

+#2739=EDGE_CURVE('',#1639,#1726,#711,.T.);

+#2750=EDGE_CURVE('',#1636,#1644,#715,.T.);

+#2752=EDGE_CURVE('',#1643,#1644,#819,.T.);

+#2754=EDGE_CURVE('',#1635,#1643,#793,.T.);

+#2766=EDGE_CURVE('',#1594,#1590,#723,.T.);

+#2769=EDGE_CURVE('',#1592,#1585,#769,.T.);

+#2772=EDGE_CURVE('',#1640,#1656,#731,.T.);

+#2774=EDGE_CURVE('',#1652,#1656,#736,.T.);

+#2776=EDGE_CURVE('',#1648,#1652,#740,.T.);

+#2778=EDGE_CURVE('',#1644,#1648,#745,.T.);

+#2792=EDGE_CURVE('',#1606,#1589,#757,.T.);

+#2804=EDGE_CURVE('',#1696,#1704,#761,.T.);

+#2830=EDGE_CURVE('',#1604,#1586,#781,.T.);

+#2846=EDGE_CURVE('',#1690,#1698,#785,.T.);

+#2869=EDGE_CURVE('',#1639,#1655,#811,.T.);

+#2871=EDGE_CURVE('',#1655,#1656,#789,.T.);

+#2883=EDGE_CURVE('',#1643,#1647,#798,.T.);

+#2885=EDGE_CURVE('',#1647,#1651,#802,.T.);

+#2887=EDGE_CURVE('',#1651,#1655,#807,.T.);

+#2892=EDGE_CURVE('',#1672,#1663,#835,.T.);

+#2906=EDGE_CURVE('',#1647,#1648,#823,.T.);

+#2919=EDGE_CURVE('',#1651,#1652,#827,.T.);

+#2945=EDGE_CURVE('',#1659,#1663,#839,.T.);

+#2967=EDGE_CURVE('',#1687,#1688,#867,.T.);

+#2992=EDGE_CURVE('',#1546,#1560,#875,.T.);

+#2995=EDGE_CURVE('',#1562,#1545,#879,.T.);

+#2997=EDGE_CURVE('',#1545,#1546,#883,.T.);

+#3007=EDGE_CURVE('',#1568,#1549,#887,.T.);

+#3009=EDGE_CURVE('',#1549,#1550,#891,.T.);

+#3011=EDGE_CURVE('',#1550,#1578,#895,.T.);

+#3013=EDGE_CURVE('',#1568,#1578,#1045,.T.);

+#3024=EDGE_CURVE('',#1546,#1554,#899,.T.);

+#3026=EDGE_CURVE('',#1554,#1564,#907,.T.);

+#3038=EDGE_CURVE('',#1545,#1553,#903,.T.);

+#3040=EDGE_CURVE('',#1553,#1554,#911,.T.);

+#3053=EDGE_CURVE('',#1566,#1553,#915,.T.);

+#3076=EDGE_CURVE('',#1570,#1557,#919,.T.);

+#3078=EDGE_CURVE('',#1570,#1572,#1037,.T.);

+#3080=EDGE_CURVE('',#1558,#1572,#923,.T.);

+#3082=EDGE_CURVE('',#1557,#1558,#927,.T.);

+#3093=EDGE_CURVE('',#1568,#1581,#940,.T.);

+#3095=EDGE_CURVE('',#1581,#1582,#931,.T.);

+#3097=EDGE_CURVE('',#1570,#1582,#1250,.T.);

+#3100=EDGE_CURVE('',#1549,#1557,#1275,.T.);

+#3110=EDGE_CURVE('',#1581,#1719,#936,.T.);

+#3113=EDGE_CURVE('',#1917,#1568,#1041,.T.);

+#3115=EDGE_CURVE('',#1917,#1719,#961,.T.);

+#3125=EDGE_CURVE('',#1578,#1575,#944,.T.);

+#3127=EDGE_CURVE('',#1575,#1723,#949,.T.);

+#3129=EDGE_CURVE('',#1723,#1910,#1001,.T.);

+#3131=EDGE_CURVE('',#1910,#1893,#953,.T.);

+#3133=EDGE_CURVE('',#1893,#1894,#1209,.T.);

+#3135=EDGE_CURVE('',#1578,#1894,#1049,.T.);

+#3147=EDGE_CURVE('',#1719,#1720,#957,.T.);

+#3149=EDGE_CURVE('',#1582,#1720,#1242,.T.);

+#3161=EDGE_CURVE('',#1917,#1918,#965,.T.);

+#3163=EDGE_CURVE('',#1918,#1720,#969,.T.);

+#3173=EDGE_CURVE('',#1957,#1958,#973,.T.);

+#3175=EDGE_CURVE('',#1913,#1957,#977,.T.);

+#3177=EDGE_CURVE('',#1913,#1914,#981,.T.);

+#3179=EDGE_CURVE('',#1914,#1958,#985,.T.);

+#3189=EDGE_CURVE('',#1961,#1962,#989,.T.);

+#3191=EDGE_CURVE('',#1962,#1909,#993,.T.);

+#3193=EDGE_CURVE('',#1909,#1910,#997,.T.);

+#3196=EDGE_CURVE('',#1723,#1724,#1005,.T.);

+#3198=EDGE_CURVE('',#1724,#1902,#1009,.T.);

+#3200=EDGE_CURVE('',#1901,#1902,#1013,.T.);

+#3202=EDGE_CURVE('',#1901,#1708,#1225,.T.);

+#3204=EDGE_CURVE('',#1707,#1708,#1279,.T.);

+#3206=EDGE_CURVE('',#1905,#1707,#1287,.T.);

+#3208=EDGE_CURVE('',#1905,#1906,#1017,.T.);

+#3210=EDGE_CURVE('',#1961,#1906,#1021,.T.);

+#3220=EDGE_CURVE('',#1949,#1950,#1025,.T.);

+#3222=EDGE_CURVE('',#1950,#1888,#1029,.T.);

+#3224=EDGE_CURVE('',#1888,#1890,#1201,.T.);

+#3226=EDGE_CURVE('',#1711,#1890,#1283,.T.);

+#3229=EDGE_CURVE('',#1715,#1878,#1033,.T.);

+#3231=EDGE_CURVE('',#1878,#1880,#1189,.T.);

+#3233=EDGE_CURVE('',#1572,#1880,#1258,.T.);

+#3236=EDGE_CURVE('',#1918,#1570,#1246,.T.);

+#3242=EDGE_CURVE('',#1894,#1896,#1221,.T.);

+#3244=EDGE_CURVE('',#1954,#1896,#1160,.T.);

+#3246=EDGE_CURVE('',#1953,#1954,#1053,.T.);

+#3248=EDGE_CURVE('',#1914,#1953,#1148,.T.);

+#3251=EDGE_CURVE('',#1913,#1949,#1057,.T.);

+#3262=EDGE_CURVE('',#1942,#1925,#1061,.T.);

+#3264=EDGE_CURVE('',#1925,#1926,#1065,.T.);

+#3266=EDGE_CURVE('',#1926,#1941,#1069,.T.);

+#3277=EDGE_CURVE('',#1949,#1929,#1073,.T.);

+#3279=EDGE_CURVE('',#1929,#1930,#1077,.T.);

+#3281=EDGE_CURVE('',#1930,#1950,#1081,.T.);

+#3292=EDGE_CURVE('',#1946,#1933,#1093,.T.);

+#3294=EDGE_CURVE('',#1925,#1933,#1113,.T.);

+#3306=EDGE_CURVE('',#1934,#1945,#1085,.T.);

+#3308=EDGE_CURVE('',#1933,#1934,#1089,.T.);

+#3320=EDGE_CURVE('',#1938,#1954,#1097,.T.);

+#3322=EDGE_CURVE('',#1937,#1938,#1101,.T.);

+#3324=EDGE_CURVE('',#1953,#1937,#1105,.T.);

+#3336=EDGE_CURVE('',#1926,#1934,#1109,.T.);

+#3359=EDGE_CURVE('',#1950,#1966,#1130,.T.);

+#3362=EDGE_CURVE('',#1930,#1938,#1117,.T.);

+#3365=EDGE_CURVE('',#1954,#1970,#1156,.T.);

+#3367=EDGE_CURVE('',#1966,#1970,#1303,.T.);

+#3377=EDGE_CURVE('',#1964,#1957,#1122,.T.);

+#3379=EDGE_CURVE('',#1949,#1964,#1126,.T.);

+#3392=EDGE_CURVE('',#1966,#1961,#1135,.T.);

+#3395=EDGE_CURVE('',#1906,#1886,#1139,.T.);

+#3397=EDGE_CURVE('',#1886,#1888,#1205,.T.);

+#3409=EDGE_CURVE('',#1968,#1958,#1144,.T.);

+#3411=EDGE_CURVE('',#1964,#1968,#1169,.T.);

+#3425=EDGE_CURVE('',#1953,#1968,#1152,.T.);

+#3437=EDGE_CURVE('',#1896,#1898,#1217,.T.);

+#3439=EDGE_CURVE('',#1909,#1898,#1299,.T.);

+#3442=EDGE_CURVE('',#1970,#1962,#1165,.T.);

+#3456=EDGE_CURVE('',#1929,#1937,#1173,.T.);

+#3479=EDGE_CURVE('',#1877,#1878,#1177,.T.);

+#3481=EDGE_CURVE('',#1882,#1877,#1181,.T.);

+#3483=EDGE_CURVE('',#1880,#1882,#1185,.T.);

+#3494=EDGE_CURVE('',#1885,#1886,#1193,.T.);

+#3496=EDGE_CURVE('',#1890,#1885,#1197,.T.);

+#3509=EDGE_CURVE('',#1898,#1893,#1213,.T.);

+#3522=EDGE_CURVE('',#1901,#1877,#1229,.T.);

+#3527=EDGE_CURVE('',#1708,#1716,#1233,.T.);

+#3538=EDGE_CURVE('',#1902,#1882,#1237,.T.);

+#3562=EDGE_CURVE('',#1572,#1576,#1254,.T.);

+#3568=EDGE_CURVE('',#1576,#1724,#1263,.T.);

+#3580=EDGE_CURVE('',#1575,#1576,#1271,.T.);

+#3584=EDGE_CURVE('',#1550,#1558,#1267,.T.);

+#3619=EDGE_CURVE('',#1707,#1712,#1291,.T.);

+#3633=EDGE_CURVE('',#1905,#1885,#1295,.T.);

+#3682=EDGE_CURVE('',#1848,#1833,#1307,.T.);

+#3684=EDGE_CURVE('',#1848,#1748,#1311,.T.);

+#3696=EDGE_CURVE('',#1851,#1842,#1315,.T.);

+#3700=EDGE_CURVE('',#1742,#1851,#1319,.T.);

+#3710=EDGE_CURVE('',#1865,#1864,#1336,.T.);

+#3712=EDGE_CURVE('',#1828,#1864,#1340,.T.);

+#3715=EDGE_CURVE('',#1830,#1848,#1458,.T.);

+#3719=EDGE_CURVE('',#1834,#1865,#1454,.T.);

+#3729=EDGE_CURVE('',#1861,#1859,#1374,.T.);

+#3731=EDGE_CURVE('',#1862,#1861,#1498,.T.);

+#3733=EDGE_CURVE('',#1863,#1862,#1508,.T.);

+#3735=EDGE_CURVE('',#1864,#1863,#1518,.T.);

+#3738=EDGE_CURVE('',#1866,#1865,#1450,.T.);

+#3740=EDGE_CURVE('',#1867,#1866,#1440,.T.);

+#3742=EDGE_CURVE('',#1868,#1867,#1430,.T.);

+#3744=EDGE_CURVE('',#1870,#1868,#1420,.T.);

+#3746=EDGE_CURVE('',#1870,#1858,#1344,.T.);

+#3748=EDGE_CURVE('',#1855,#1858,#1389,.T.);

+#3750=EDGE_CURVE('',#1859,#1855,#1348,.T.);

+#3760=EDGE_CURVE('',#1859,#1860,#1361,.T.);

+#3762=EDGE_CURVE('',#1821,#1860,#1365,.T.);

+#3765=EDGE_CURVE('',#1822,#1861,#1502,.T.);

+#3778=EDGE_CURVE('',#1855,#1858,#1379,.T.);

+#3781=EDGE_CURVE('',#1869,#1870,#1409,.T.);

+#3783=EDGE_CURVE('',#1871,#1869,#1468,.T.);

+#3785=EDGE_CURVE('',#1872,#1871,#1478,.T.);

+#3787=EDGE_CURVE('',#1860,#1872,#1488,.T.);

+#3797=EDGE_CURVE('',#1855,#1856,#1384,.T.);

+#3799=EDGE_CURVE('',#1858,#1856,#1394,.T.);

+#3823=EDGE_CURVE('',#1840,#1868,#1424,.T.);

+#3827=EDGE_CURVE('',#1851,#1852,#1462,.T.);

+#3830=EDGE_CURVE('',#1874,#1869,#1472,.T.);

+#3841=EDGE_CURVE('',#1838,#1867,#1434,.T.);

+#3854=EDGE_CURVE('',#1836,#1866,#1444,.T.);

+#3905=EDGE_CURVE('',#1845,#1871,#1482,.T.);

+#3918=EDGE_CURVE('',#1846,#1872,#1492,.T.);

+#3943=EDGE_CURVE('',#1824,#1862,#1512,.T.);

+#3956=EDGE_CURVE('',#1826,#1863,#1522,.T.);

+#3978=EDGE_CURVE('',#1762,#1753,#1534,.T.);

+#3980=EDGE_CURVE('',#1922,#1753,#1542,.T.);

+#3992=EDGE_CURVE('',#1754,#1758,#1526,.T.);

+#3994=EDGE_CURVE('',#1753,#1754,#1530,.T.);

+#4009=EDGE_CURVE('',#1760,#1754,#1538,.T.);

+#4036=TRIMMED_CURVE('A_13',#4035,(PARAMETER_VALUE(0.E0)),(PARAMETER_VALUE

+(1.E0)),.T.,.UNSPECIFIED.);

+#4042=TRIMMED_CURVE('A_14',#4041,(PARAMETER_VALUE(0.E0)),(PARAMETER_VALUE

+(1.E0)),.T.,.UNSPECIFIED.);

+#4047=CIRCLE('',#4046,2.15E-2);

+#4048=TRIMMED_CURVE('',#4047,(PARAMETER_VALUE(0.E0)),(PARAMETER_VALUE(1.8E2)),

+.T.,.UNSPECIFIED.);

+#4049=COMPOSITE_CURVE_SEGMENT(.CONTINUOUS.,.T.,#4048);

+#4054=CIRCLE('',#4053,2.15E-2);

+#4055=TRIMMED_CURVE('',#4054,(PARAMETER_VALUE(0.E0)),(PARAMETER_VALUE(1.8E2)),

+.T.,.UNSPECIFIED.);

+#4056=COMPOSITE_CURVE_SEGMENT(.CONTINUOUS.,.T.,#4055);

+#4057=COMPOSITE_CURVE('',(#4049,#4056),.F.);

+#4062=CIRCLE('',#4061,2.15E-2);

+#4063=TRIMMED_CURVE('',#4062,(PARAMETER_VALUE(0.E0)),(PARAMETER_VALUE(1.8E2)),

+.T.,.UNSPECIFIED.);

+#4064=COMPOSITE_CURVE_SEGMENT(.CONTINUOUS.,.T.,#4063);

+#4069=CIRCLE('',#4068,2.15E-2);

+#4070=TRIMMED_CURVE('',#4069,(PARAMETER_VALUE(0.E0)),(PARAMETER_VALUE(1.8E2)),

+.T.,.UNSPECIFIED.);

+#4071=COMPOSITE_CURVE_SEGMENT(.CONTINUOUS.,.T.,#4070);

+#4072=COMPOSITE_CURVE('',(#4064,#4071),.F.);

+#4081=CIRCLE('',#4080,6.7E-2);

+#4082=TRIMMED_CURVE('',#4081,(PARAMETER_VALUE(0.E0)),(PARAMETER_VALUE(1.8E2)),

+.T.,.UNSPECIFIED.);

+#4083=COMPOSITE_CURVE_SEGMENT(.CONTINUOUS.,.T.,#4082);

+#4088=CIRCLE('',#4087,6.7E-2);

+#4089=TRIMMED_CURVE('',#4088,(PARAMETER_VALUE(0.E0)),(PARAMETER_VALUE(1.8E2)),

+.T.,.UNSPECIFIED.);

+#4090=COMPOSITE_CURVE_SEGMENT(.CONTINUOUS.,.T.,#4089);

+#4091=COMPOSITE_CURVE('',(#4083,#4090),.F.);

+#4096=CIRCLE('',#4095,6.7E-2);

+#4097=TRIMMED_CURVE('',#4096,(PARAMETER_VALUE(0.E0)),(PARAMETER_VALUE(1.8E2)),

+.T.,.UNSPECIFIED.);

+#4098=COMPOSITE_CURVE_SEGMENT(.CONTINUOUS.,.T.,#4097);

+#4103=CIRCLE('',#4102,6.7E-2);

+#4104=TRIMMED_CURVE('',#4103,(PARAMETER_VALUE(0.E0)),(PARAMETER_VALUE(1.8E2)),

+.T.,.UNSPECIFIED.);

+#4105=COMPOSITE_CURVE_SEGMENT(.CONTINUOUS.,.T.,#4104);

+#4106=COMPOSITE_CURVE('',(#4098,#4105),.F.);

+#4108=(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.));

+#4112=(NAMED_UNIT(*)PLANE_ANGLE_UNIT()SI_UNIT($,.RADIAN.));

+#4115=(NAMED_UNIT(*)SI_UNIT($,.STERADIAN.)SOLID_ANGLE_UNIT());

+#4118=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#4027),#4117);

+#4119=GEOMETRICALLY_BOUNDED_SURFACE_SHAPE_REPRESENTATION('',(#4037),#4117);

+#4120=SHAPE_REPRESENTATION('',(#4031,#4076),#4117);

+#4129=PRODUCT_DEFINITION('design','',#4128,#4125);

+#4130=PRODUCT_DEFINITION_SHAPE('','SHAPE FOR 705550036.',#4129);

+#4131=SHAPE_DEFINITION_REPRESENTATION(#4130,#4120);

+ENDSEC;

+END-ISO-10303-21;

diff --git a/circuit_boards/CAN_boards/CAN_Terminator/CAN_terminator.kicad_pcb b/circuit_boards/CAN_boards/CAN_Terminator/CAN_terminator.kicad_pcb
new file mode 100644
index 0000000..abc1fbf
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/CAN_terminator.kicad_pcb
@@ -0,0 +1,287 @@
+(kicad_pcb (version 20211014) (generator pcbnew)
+
+  (general
+    (thickness 1.6)
+  )
+
+  (paper "A4")
+  (layers
+    (0 "F.Cu" signal)
+    (31 "B.Cu" signal)
+    (32 "B.Adhes" user "B.Adhesive")
+    (33 "F.Adhes" user "F.Adhesive")
+    (34 "B.Paste" user)
+    (35 "F.Paste" user)
+    (36 "B.SilkS" user "B.Silkscreen")
+    (37 "F.SilkS" user "F.Silkscreen")
+    (38 "B.Mask" user)
+    (39 "F.Mask" user)
+    (40 "Dwgs.User" user "User.Drawings")
+    (41 "Cmts.User" user "User.Comments")
+    (42 "Eco1.User" user "User.Eco1")
+    (43 "Eco2.User" user "User.Eco2")
+    (44 "Edge.Cuts" user)
+    (45 "Margin" user)
+    (46 "B.CrtYd" user "B.Courtyard")
+    (47 "F.CrtYd" user "F.Courtyard")
+    (48 "B.Fab" user)
+    (49 "F.Fab" user)
+    (50 "User.1" user)
+    (51 "User.2" user)
+    (52 "User.3" user)
+    (53 "User.4" user)
+    (54 "User.5" user)
+    (55 "User.6" user)
+    (56 "User.7" user)
+    (57 "User.8" user)
+    (58 "User.9" user)
+  )
+
+  (setup
+    (pad_to_mask_clearance 0)
+    (grid_origin 160.528 112.395)
+    (pcbplotparams
+      (layerselection 0x00010fc_ffffffff)
+      (disableapertmacros false)
+      (usegerberextensions false)
+      (usegerberattributes true)
+      (usegerberadvancedattributes true)
+      (creategerberjobfile true)
+      (svguseinch false)
+      (svgprecision 6)
+      (excludeedgelayer true)
+      (plotframeref false)
+      (viasonmask false)
+      (mode 1)
+      (useauxorigin false)
+      (hpglpennumber 1)
+      (hpglpenspeed 20)
+      (hpglpendiameter 15.000000)
+      (dxfpolygonmode true)
+      (dxfimperialunits true)
+      (dxfusepcbnewfont true)
+      (psnegative false)
+      (psa4output false)
+      (plotreference false)
+      (plotvalue false)
+      (plotinvisibletext false)
+      (sketchpadsonfab false)
+      (subtractmaskfromsilk false)
+      (outputformat 1)
+      (mirror false)
+      (drillshape 0)
+      (scaleselection 1)
+      (outputdirectory "")
+    )
+  )
+
+  (net 0 "")
+  (net 1 "/CAN_H")
+  (net 2 "/CAN_L")
+
+  (footprint "CAN_terminator:70555-0036" (layer "F.Cu")
+    (tedit 0) (tstamp 8d0d01c9-18ba-497b-83f4-d68b8bd509e6)
+    (at 147.32 116.84 90)
+    (property "Sheetfile" "CAN_terminator.kicad_sch")
+    (property "Sheetname" "")
+    (path "/e8bffcfe-c870-4593-8a2b-7a667dd6382d")
+    (attr through_hole)
+    (fp_text reference "J1" (at -1.143 1.905 90) (layer "F.SilkS")
+      (effects (font (size 1 1) (thickness 0.15)))
+      (tstamp e04bb6f0-926f-4f5c-9e46-22f411f9535c)
+    )
+    (fp_text value "2 pin molex sl right angle" (at 0 -2.54 90) (layer "F.SilkS") hide
+      (effects (font (size 1 1) (thickness 0.15)))
+      (tstamp 0e178f07-4799-45e2-9612-a0b4b941366e)
+    )
+    (fp_text user "Copyright 2021 Accelerated Designs. All rights reserved." (at 0 0 90) (layer "Cmts.User")
+      (effects (font (size 0.127 0.127) (thickness 0.002)))
+      (tstamp b7886193-911b-4762-81a8-f396711fb4d1)
+    )
+    (fp_text user "*" (at 0 0 90) (layer "F.Fab")
+      (effects (font (size 1 1) (thickness 0.15)))
+      (tstamp 9d02aef6-b68e-47d5-ba6c-779a67d416ad)
+    )
+    (fp_line (start -5.7277 9.61133) (end -5.7277 13.066001) (layer "F.SilkS") (width 0.12) (tstamp 4ab750dd-e9a2-4fcc-a78a-174239fb8039))
+    (fp_line (start 3.1877 7.668671) (end 3.1877 -0.777) (layer "F.SilkS") (width 0.12) (tstamp 7739ac46-ca6b-43f5-9b39-883e29a2155f))
+    (fp_line (start -3.311185 -0.777) (end -5.7277 -0.777) (layer "F.SilkS") (width 0.12) (tstamp 8431235c-55eb-4742-9f6c-053041d297ad))
+    (fp_line (start -5.7277 -0.777) (end -5.7277 7.668671) (layer "F.SilkS") (width 0.12) (tstamp a699b636-e2e1-419a-a51e-ec6d6cc2fb70))
+    (fp_line (start 3.1877 13.066001) (end 3.1877 9.61133) (layer "F.SilkS") (width 0.12) (tstamp b3c7ca3a-97ef-43ae-96a9-28f4a4a5efc7))
+    (fp_line (start 3.1877 -0.777) (end 1.27 -0.777) (layer "F.SilkS") (width 0.12) (tstamp c7a3db25-81fe-4cc3-be0d-441c41ab85f4))
+    (fp_line (start -5.7277 13.066001) (end 3.1877 13.066001) (layer "F.SilkS") (width 0.12) (tstamp cf71b566-4811-4b0a-bab1-be46bd03652d))
+    (fp_line (start -14.9128 -0.396) (end -14.6588 -0.396) (layer "Cmts.User") (width 0.1) (tstamp 0451fd0c-c650-439a-a514-37d31ed2b220))
+    (fp_line (start -3.94 -8.636) (end 1.4 -8.636) (layer "Cmts.User") (width 0.1) (tstamp 0ab5104b-77cb-4bdc-9fe1-5eb13feace23))
+    (fp_line (start 5.4737 0.254) (end 5.7277 0.254) (layer "Cmts.User") (width 0.1) (tstamp 0b382ed5-eceb-4891-b0ae-2068b4d7f5e1))
+    (fp_line (start 3.0607 -11.176) (end 2.8067 -11.049) (layer "Cmts.User") (width 0.1) (tstamp 149b12b2-7d13-4fd4-b885-0bf4f6d5816c))
+    (fp_line (start 2.8067 -11.303) (end 2.8067 -11.049) (layer "Cmts.User") (width 0.1) (tstamp 1525e3c9-4ba2-4762-8905-9730daf4d016))
+    (fp_line (start 0 -5.461) (end 0.254 -5.334) (layer "Cmts.User") (width 0.1) (tstamp 19385824-3683-4bf5-aa2c-b0663eb4e603))
+    (fp_line (start -16.1828 -0.904) (end -15.9288 -0.904) (layer "Cmts.User") (width 0.1) (tstamp 1a197982-fa33-491a-8bbd-e38e27f0e74c))
+    (fp_line (start -16.0558 -0.65) (end -15.9288 -0.904) (layer "Cmts.User") (width 0.1) (tstamp 1a7425cb-8b4a-465a-bbac-f2220b58e655))
+    (fp_line (start -2.54 0) (end -2.54 -5.842) (layer "Cmts.User") (width 0.1) (tstamp 1b27a54e-6eee-44be-a486-5b10429360e9))
+    (fp_line (start 3.0607 -11.176) (end 2.8067 -11.303) (layer "Cmts.User") (width 0.1) (tstamp 20296511-e13c-449f-a421-fed6648b0759))
+    (fp_line (start 0 0) (end 5.9817 0) (layer "Cmts.User") (width 0.1) (tstamp 2a9908b3-69b3-4e9f-bb66-cfc673dbb1bc))
+    (fp_line (start -14.7858 12.939001) (end -14.6588 12.685001) (layer "Cmts.User") (width 0.1) (tstamp 2af10347-00c0-460e-9a95-217fa19788ce))
+    (fp_line (start -5.3467 -11.303) (end -5.3467 -11.049) (layer "Cmts.User") (width 0.1) (tstamp 2c09ffe8-cd48-46fd-b279-8c43d1a6bc1b))
+    (fp_line (start -16.0558 -0.65) (end -16.0558 -1.92) (layer "Cmts.User") (width 0.1) (tstamp 2cbccbfe-f0fd-4064-ab82-53a269f8b338))
+    (fp_line (start -7.9278 8.386001) (end -7.6738 8.386001) (layer "Cmts.User") (width 0.1) (tstamp 2e15f00e-83dd-478e-a7dd-98bc5a390430))
+    (fp_line (start 1.146 -8.763) (end 1.146 -8.509) (layer "Cmts.User") (width 0.1) (tstamp 37dcbdbc-b212-40eb-9fc4-bcbcf625f395))
+    (fp_line (start 1.4 8.640001) (end -8.1818 8.640001) (layer "Cmts.User") (width 0.1) (tstamp 3af25191-67ed-46b3-82d1-adfcab706aaa))
+    (fp_line (start -16.0558 0) (end -16.0558 1.27) (layer "Cmts.User") (width 0.1) (tstamp 3f2586e8-9966-47b8-90fc-5338ea9bd680))
+    (fp_line (start 5.6007 0) (end 5.4737 0.254) (layer "Cmts.User") (width 0.1) (tstamp 443e0bce-b507-44e0-bad1-6a0250193bc5))
+    (fp_line (start 5.6007 0) (end 5.6007 -1.27) (layer "Cmts.User") (width 0.1) (tstamp 4a968524-8474-465c-9a5e-e6f0f0ae44e9))
+    (fp_line (start -3.94 -8.636) (end -3.686 -8.763) (layer "Cmts.User") (width 0.1) (tstamp 4b0edace-c31a-473a-9307-1dd08e5e2a35))
+    (fp_line (start -5.6007 -0.65) (end -5.6007 -11.557) (layer "Cmts.User") (width 0.1) (tstamp 4d0b16bf-25fa-49af-8193-eee0d14d2b15))
+    (fp_line (start -14.9128 12.685001) (end -14.6588 12.685001) (layer "Cmts.User") (width 0.1) (tstamp 5ee6d8a4-1517-45f7-af0a-27aee9b9af74))
+    (fp_line (start 5.6007 0) (end 5.4737 -0.254) (layer "Cmts.User") (width 0.1) (tstamp 606c94e7-bc9c-4062-8758-a5431e41fdcb))
+    (fp_line (start -3.94 8.640001) (end -3.94 -9.017) (layer "Cmts.User") (width 0.1) (tstamp 66cc15ed-ab47-4158-aa71-ff8ebbf1711d))
+    (fp_line (start -7.9278 0.254) (end -7.6738 0.254) (layer "Cmts.User") (width 0.1) (tstamp 695dac98-0b16-4aa5-beca-21b55678f616))
+    (fp_line (start -5.6007 12.939001) (end -15.1668 12.939001) (layer "Cmts.User") (width 0.1) (tstamp 6d9a61e0-6d76-41bb-9649-b956b6fc3b34))
+    (fp_line (start -3.686 -8.763) (end -3.686 -8.509) (layer "Cmts.User") (width 0.1) (tstamp 71935ac6-1b80-42e9-848a-db3816287512))
+    (fp_line (start -5.6007 -11.176) (end -5.3467 -11.303) (layer "Cmts.User") (width 0.1) (tstamp 7201ebac-e2c0-4c7c-9fdc-e55b4cef1d76))
+    (fp_line (start -14.7858 -0.65) (end -14.7858 12.939001) (layer "Cmts.User") (width 0.1) (tstamp 83db32c0-f45c-456a-9ff7-62f7984db177))
+    (fp_line (start -2.54 -5.461) (end -2.794 -5.588) (layer "Cmts.User") (width 0.1) (tstamp 84ba8161-4339-4d7f-8366-f8f208e418a1))
+    (fp_line (start 5.6007 0) (end 5.6007 1.27) (layer "Cmts.User") (width 0.1) (tstamp 86728636-2d6b-4848-a8db-979cc5166ab5))
+    (fp_line (start -16.1828 0.254) (end -15.9288 0.254) (layer "Cmts.User") (width 0.1) (tstamp 88fe52dd-a3a7-42e2-b218-323cdae7baf6))
+    (fp_line (start 0.254 -5.588) (end 0.254 -5.334) (layer "Cmts.User") (width 0.1) (tstamp 8ce07e24-ce84-43c0-8aa1-98a5246b9ff4))
+    (fp_line (start 1.4 -8.636) (end 1.146 -8.509) (layer "Cmts.User") (width 0.1) (tstamp 91f9bcec-1b8d-4028-8714-4880368192f9))
+    (fp_line (start -5.6007 -11.176) (end -5.3467 -11.049) (layer "Cmts.User") (width 0.1) (tstamp 93bbfea5-84d7-4c47-860f-066cc90edbe3))
+    (fp_line (start -2.54 -5.461) (end -2.794 -5.334) (layer "Cmts.User") (width 0.1) (tstamp 95178b0f-9dc6-4d63-9357-cb46aa41867f))
+    (fp_line (start -1.27 0) (end -16.4368 0) (layer "Cmts.User") (width 0.1) (tstamp 952ba59a-449a-461b-ac1e-95e029b26601))
+    (fp_line (start -14.7858 12.939001) (end -14.9128 12.685001) (layer "Cmts.User") (width 0.1) (tstamp 994f91d0-f4d8-4f9d-b7c3-657bc14e66ca))
+    (fp_line (start -5.6007 -0.65) (end -16.4368 -0.65) (layer "Cmts.User") (width 0.1) (tstamp 9a6da229-63be-4997-a15c-94513be14718))
+    (fp_line (start -16.0558 0) (end -15.9288 0.254) (layer "Cmts.User") (width 0.1) (tstamp 9b7de360-5ffe-4798-ad3b-5c774ab09176))
+    (fp_line (start 5.6007 0) (end 5.7277 0.254) (layer "Cmts.User") (width 0.1) (tstamp 9d716695-cc63-411b-8c3f-3e9efdf5554c))
+    (fp_line (start 3.0607 -0.65) (end 3.0607 -11.557) (layer "Cmts.User") (width 0.1) (tstamp a1cd2766-582d-43b1-92a0-8a69b3c6a4e8))
+    (fp_line (start -7.8008 0) (end -7.6738 0.254) (layer "Cmts.User") (width 0.1) (tstamp a2099b73-8ad2-414e-a610-edbd301dcbcd))
+    (fp_line (start 1.4 -8.636) (end 1.146 -8.763) (layer "Cmts.User") (width 0.1) (tstamp a3d9ecc5-e32a-420c-8d8c-b3d489ba1d05))
+    (fp_line (start -14.7858 -0.65) (end -14.6588 -0.396) (layer "Cmts.User") (width 0.1) (tstamp a3f4caaf-0cef-4abe-ab87-a5bd56dfb401))
+    (fp_line (start -14.7858 -0.65) (end -14.9128 -0.396) (layer "Cmts.User") (width 0.1) (tstamp af588402-66b9-434f-bf75-00a638f94319))
+    (fp_line (start -5.6007 -11.176) (end 3.0607 -11.176) (layer "Cmts.User") (width 0.1) (tstamp b11b9fad-dd0a-4158-8fc4-5ed38ce5205b))
+    (fp_line (start -7.8008 8.640001) (end -7.9278 8.386001) (layer "Cmts.User") (width 0.1) (tstamp b5105f05-a85c-49d6-94bf-5713a472283a))
+    (fp_line (start 0 -5.461) (end 0.254 -5.588) (layer "Cmts.User") (width 0.1) (tstamp be096b2f-1286-4288-a881-759501c0cf77))
+    (fp_line (start -3.94 -8.636) (end -3.686 -8.509) (layer "Cmts.User") (width 0.1) (tstamp be1d98a7-5423-4259-99f5-f002cff439d1))
+    (fp_line (start -7.8008 8.640001) (end -7.6738 8.386001) (layer "Cmts.User") (width 0.1) (tstamp c313110d-b275-4d48-865c-446ddf750cac))
+    (fp_line (start -7.8008 0) (end -7.9278 0.254) (layer "Cmts.User") (width 0.1) (tstamp c71a0024-4414-4043-a86a-a6e107f4b2d1))
+    (fp_line (start 0 0) (end 0 -5.842) (layer "Cmts.User") (width 0.1) (tstamp c821b87e-6250-4a84-8d88-b99443b90c64))
+    (fp_line (start 0 0) (end 5.9817 0) (layer "Cmts.User") (width 0.1) (tstamp c9399dd8-5c90-4325-b389-bf3103624bec))
+    (fp_line (start 0 -5.461) (end 1.27 -5.461) (layer "Cmts.User") (width 0.1) (tstamp cb48d39d-3b2b-400a-9b46-c1d32718986b))
+    (fp_line (start -2.54 -5.461) (end -3.81 -5.461) (layer "Cmts.User") (width 0.1) (tstamp de557e3f-efed-47f9-90f8-ac5d9fce2601))
+    (fp_line (start -1.27 0) (end -8.1818 0) (layer "Cmts.User") (width 0.1) (tstamp df0777da-5b7f-4341-a6a5-5fc162a9a7ef))
+    (fp_line (start -16.0558 -0.65) (end -16.1828 -0.904) (layer "Cmts.User") (width 0.1) (tstamp e5c24f3a-2fcc-4559-97f3-e0b7cddb0c48))
+    (fp_line (start 1.4 8.640001) (end 1.4 -9.017) (layer "Cmts.User") (width 0.1) (tstamp e6c9e7f2-c898-4429-8902-78b402b76e4e))
+    (fp_line (start -5.6007 -0.65) (end -15.1668 -0.65) (layer "Cmts.User") (width 0.1) (tstamp eabec006-aad6-4e4b-b4d6-e688f33f7e1d))
+    (fp_line (start -16.0558 0) (end -16.1828 0.254) (layer "Cmts.User") (width 0.1) (tstamp f4147886-1d72-4fe1-b32a-57d9e7e86cf2))
+    (fp_line (start -7.8008 0) (end -7.8008 8.640001) (layer "Cmts.User") (width 0.1) (tstamp f5a9dc58-05e6-4677-9eed-deb5fd24965f))
+    (fp_line (start -2.794 -5.588) (end -2.794 -5.334) (layer "Cmts.User") (width 0.1) (tstamp f7220b3d-c3b5-46e2-98ca-e330b30390ca))
+    (fp_line (start 5.4737 -0.254) (end 5.7277 -0.254) (layer "Cmts.User") (width 0.1) (tstamp fabdd97b-fba7-4595-8cc6-ce801d292bf8))
+    (fp_line (start 5.6007 0) (end 5.7277 -0.254) (layer "Cmts.User") (width 0.1) (tstamp fd370dcf-dbc7-4117-96f0-a142e67e27d7))
+    (fp_line (start 3.3558 -1.016) (end -5.8958 -1.016) (layer "F.CrtYd") (width 0.05) (tstamp 15771939-fb81-4cff-a40e-65699aae3cf2))
+    (fp_line (start -5.8958 -1.016) (end -5.8958 13.193001) (layer "F.CrtYd") (width 0.05) (tstamp 2e44d9d5-1b1b-4cc1-afb6-adf703b02956))
+    (fp_line (start -5.8958 13.193001) (end 3.3558 13.193001) (layer "F.CrtYd") (width 0.05) (tstamp 9204eda4-1759-423e-8a98-81483dc07b5b))
+    (fp_line (start 3.3558 -1.016) (end -5.8958 -1.016) (layer "F.CrtYd") (width 0.05) (tstamp 96609c51-acda-483b-90f3-fbd8eea64e08))
+    (fp_line (start 3.3558 13.193001) (end 3.3558 -1.016) (layer "F.CrtYd") (width 0.05) (tstamp b4665ccb-5983-403b-87c3-b4f193b1ea43))
+    (fp_line (start 3.3558 13.193001) (end 3.3558 -1.016) (layer "F.CrtYd") (width 0.05) (tstamp b89111b5-5ee4-41ef-ba85-21abf67a1663))
+    (fp_line (start -5.8958 -1.016) (end -5.8958 13.193001) (layer "F.CrtYd") (width 0.05) (tstamp bed2a07d-6a29-4bb6-a711-bff108ed9b5a))
+    (fp_line (start -5.8958 13.193001) (end 3.3558 13.193001) (layer "F.CrtYd") (width 0.05) (tstamp d105aa25-9ccd-4ea5-8463-40d8efc30252))
+    (fp_line (start 3.0607 12.939001) (end 3.0607 -0.65) (layer "F.Fab") (width 0.1) (tstamp 11cc679d-69d4-44ee-a945-c556a0b393d2))
+    (fp_line (start -5.6007 12.939001) (end 3.0607 12.939001) (layer "F.Fab") (width 0.1) (tstamp 7d57d82b-6583-4958-8478-b4caeee3ba20))
+    (fp_line (start 3.0607 -0.65) (end -5.6007 -0.65) (layer "F.Fab") (width 0.1) (tstamp c9146377-97b3-49b3-b510-106f521ea531))
+    (fp_line (start -5.6007 -0.65) (end -5.6007 12.939001) (layer "F.Fab") (width 0.1) (tstamp d85f85a4-15fa-40cf-aab3-a16f7baeaf9d))
+    (fp_circle (center 0 -1.905) (end 0.381 -1.905) (layer "F.Fab") (width 0.1) (fill none) (tstamp f5e198df-6127-41a8-b74d-9cc10cbe6949))
+    (pad "1" thru_hole rect (at 0 0 90) (size 1.524 1.524) (drill 0.9652) (layers *.Cu *.Mask)
+      (net 1 "/CAN_H") (pinfunction "Pin_1") (pintype "passive") (tstamp 3aebf8b6-92e0-46ee-ac67-2c509c754a55))
+    (pad "2" thru_hole circle (at -2.54 0 90) (size 1.524 1.524) (drill 0.9652) (layers *.Cu *.Mask)
+      (net 2 "/CAN_L") (pinfunction "Pin_2") (pintype "passive") (tstamp c9d60483-08cc-43bc-beaf-a163675e6344))
+    (pad "3" thru_hole circle (at -3.94 8.640001 90) (size 3.4036 3.4036) (drill 3.4036) (layers *.Cu *.Mask) (tstamp 767fef50-24ac-4d80-ba31-60a68addd71c))
+    (pad "4" thru_hole circle (at 1.4 8.640001 90) (size 3.4036 3.4036) (drill 3.4036) (layers *.Cu *.Mask) (tstamp ec9f380e-5b8b-4225-8633-df2c495f3d57))
+    (model "${KIPRJMOD}/3D model/70555-0036_stp/705550036.stp"
+      (offset (xyz -1.27 -7.1882 2.794))
+      (scale (xyz 1 1 1))
+      (rotate (xyz -90 0 0))
+    )
+  )
+
+  (footprint "Resistor_SMD:R_0805_2012Metric" (layer "B.Cu")
+    (tedit 5F68FEEE) (tstamp fbf9bb3f-e722-456b-92cb-cae8f83948d0)
+    (at 150.876 118.0065 -90)
+    (descr "Resistor SMD 0805 (2012 Metric), square (rectangular) end terminal, IPC_7351 nominal, (Body size source: IPC-SM-782 page 72, https://www.pcb-3d.com/wordpress/wp-content/uploads/ipc-sm-782a_amendment_1_and_2.pdf), generated with kicad-footprint-generator")
+    (tags "resistor")
+    (property "Sheetfile" "CAN_terminator.kicad_sch")
+    (property "Sheetname" "")
+    (path "/c78ffe14-863e-4c6e-af99-6d6cf6e857aa")
+    (attr smd)
+    (fp_text reference "R1" (at 0 1.65 90) (layer "B.SilkS")
+      (effects (font (size 1 1) (thickness 0.15)) (justify mirror))
+      (tstamp 62cebe0e-5aba-4ac0-bf4f-517466fd3628)
+    )
+    (fp_text value "120 ohm" (at 0 -1.65 90) (layer "B.Fab")
+      (effects (font (size 1 1) (thickness 0.15)) (justify mirror))
+      (tstamp b1559971-4af8-42b9-b8ff-c57891196380)
+    )
+    (fp_text user "${REFERENCE}" (at 0 0 90) (layer "B.Fab")
+      (effects (font (size 0.5 0.5) (thickness 0.08)) (justify mirror))
+      (tstamp d2f839d4-b74d-4900-8e06-437630edc13f)
+    )
+    (fp_line (start -0.227064 0.735) (end 0.227064 0.735) (layer "B.SilkS") (width 0.12) (tstamp aa852fef-25c5-4952-b6d2-4d55a79ea555))
+    (fp_line (start -0.227064 -0.735) (end 0.227064 -0.735) (layer "B.SilkS") (width 0.12) (tstamp c39a0fc1-d6ac-4bba-97ad-ea715058d535))
+    (fp_line (start 1.68 -0.95) (end -1.68 -0.95) (layer "B.CrtYd") (width 0.05) (tstamp 06d108ef-4e52-471b-9b08-82185fe7102d))
+    (fp_line (start 1.68 0.95) (end 1.68 -0.95) (layer "B.CrtYd") (width 0.05) (tstamp 26a1615f-7d49-4b0d-ab0f-3a499b651970))
+    (fp_line (start -1.68 0.95) (end 1.68 0.95) (layer "B.CrtYd") (width 0.05) (tstamp ac245935-1b6f-43a3-91b6-10168040ee49))
+    (fp_line (start -1.68 -0.95) (end -1.68 0.95) (layer "B.CrtYd") (width 0.05) (tstamp cff434b2-1fdf-4d2a-94fe-b6396a86563b))
+    (fp_line (start 1 -0.625) (end -1 -0.625) (layer "B.Fab") (width 0.1) (tstamp 2b57236f-07a6-4412-a3a3-d7bf53a94854))
+    (fp_line (start -1 0.625) (end 1 0.625) (layer "B.Fab") (width 0.1) (tstamp 404a71a5-669d-48c0-a9b4-b7d86cd16ca6))
+    (fp_line (start -1 -0.625) (end -1 0.625) (layer "B.Fab") (width 0.1) (tstamp 8072f038-202d-4c08-85a2-b85b06b0c1d7))
+    (fp_line (start 1 0.625) (end 1 -0.625) (layer "B.Fab") (width 0.1) (tstamp f026ab72-b40c-4e66-99b8-4dfa1e3a8bc4))
+    (pad "1" smd roundrect (at -0.9125 0 270) (size 1.025 1.4) (layers "B.Cu" "B.Paste" "B.Mask") (roundrect_rratio 0.243902439)
+      (net 1 "/CAN_H") (pintype "passive") (tstamp 4d65a789-97ff-4775-b21f-65d823affd00))
+    (pad "2" smd roundrect (at 0.9125 0 270) (size 1.025 1.4) (layers "B.Cu" "B.Paste" "B.Mask") (roundrect_rratio 0.243902439)
+      (net 2 "/CAN_L") (pintype "passive") (tstamp 2c7bea0b-bf40-4a13-8ac7-f7cdb3ef4239))
+    (model "${KICAD6_3DMODEL_DIR}/Resistor_SMD.3dshapes/R_0805_2012Metric.wrl"
+      (offset (xyz 0 0 0))
+      (scale (xyz 1 1 1))
+      (rotate (xyz 0 0 0))
+    )
+  )
+
+  (gr_poly
+    (pts
+      (xy 146.177 116.84)
+      (xy 145.542 116.205)
+      (xy 145.542 117.475)
+    ) (layer "F.SilkS") (width 0.15) (fill solid) (tstamp 2cedb083-c93c-4b1c-b36e-fbae4ac29a06))
+  (gr_arc (start 160.528 123.19) (mid 160.342013 123.639013) (end 159.893 123.825) (layer "Edge.Cuts") (width 0.1) (tstamp 049d31ed-c080-40c6-a27f-8f7f4b2fcb00))
+  (gr_arc (start 145.415 113.03) (mid 145.600987 112.580987) (end 146.05 112.395) (layer "Edge.Cuts") (width 0.1) (tstamp 183182d7-acc2-4f63-b8d1-5e98abdd2fdb))
+  (gr_line (start 145.415 113.03) (end 145.415 123.19) (layer "Edge.Cuts") (width 0.1) (tstamp 5601d0d4-083c-40bb-bb72-9163cf06877a))
+  (gr_line (start 160.528 113.03) (end 160.528 123.19) (layer "Edge.Cuts") (width 0.1) (tstamp 58133a47-f708-43ad-a5ee-a634a59b4b09))
+  (gr_line (start 146.05 123.825) (end 159.893 123.825) (layer "Edge.Cuts") (width 0.1) (tstamp 911f2eda-bbfe-4e1c-8cee-a449d0264f5a))
+  (gr_arc (start 146.05 123.825) (mid 145.600987 123.639013) (end 145.415 123.19) (layer "Edge.Cuts") (width 0.1) (tstamp aa08abe8-1f15-49aa-8bd2-671f43ff0144))
+  (gr_line (start 146.05 112.395) (end 159.893 112.395) (layer "Edge.Cuts") (width 0.1) (tstamp b1b733fd-ef3b-4971-848c-7ee41106c9da))
+  (gr_arc (start 159.893 112.395) (mid 160.342013 112.580987) (end 160.528 113.03) (layer "Edge.Cuts") (width 0.1) (tstamp d70741fb-9769-459d-85d7-06287068793b))
+  (gr_text "CAN Term" (at 152.9334 118.0846 90) (layer "B.SilkS") (tstamp 15dc4b2e-003f-454e-bdaf-e1febd8c55e0)
+    (effects (font (size 0.9906 0.9906) (thickness 0.127)) (justify mirror))
+  )
+  (gr_text "971" (at 155.9814 118.1862) (layer "B.SilkS") (tstamp 188ae16b-4163-436c-8af9-1112c99f2627)
+    (effects (font (size 0.889 0.889) (thickness 0.127)) (justify mirror))
+  )
+  (gr_text "V1.0 2022" (at 159.131 118.0084 90) (layer "B.SilkS") (tstamp 351426b8-c99c-48b2-b00a-5d5590d56c96)
+    (effects (font (size 1 1) (thickness 0.15)) (justify mirror))
+  )
+  (gr_text "CAN_H" (at 149.098 114.935) (layer "B.SilkS") (tstamp b6090a9a-81cf-4c25-90e4-f9b29d34d5b9)
+    (effects (font (size 1 1) (thickness 0.15)) (justify mirror))
+  )
+  (gr_text "CAN_L" (at 149.098 121.285) (layer "B.SilkS") (tstamp eef45872-f083-41bb-912f-de7a762baadb)
+    (effects (font (size 1 1) (thickness 0.15)) (justify mirror))
+  )
+
+  (segment (start 147.32 116.332) (end 147.32 116.84) (width 0.508) (layer "B.Cu") (net 1) (tstamp 4548c7ec-5e75-4078-a0d3-6fdc3ee75387))
+  (segment (start 149.606 115.062) (end 148.59 115.062) (width 0.508) (layer "B.Cu") (net 1) (tstamp 9a7279b8-4e1d-4cbb-894c-76ac725b7377))
+  (segment (start 150.876 116.332) (end 150.876 117.094) (width 0.508) (layer "B.Cu") (net 1) (tstamp f4ea9409-85a3-452f-88dd-f039b61cb093))
+  (arc (start 149.606 115.062) (mid 150.504026 115.433974) (end 150.876 116.332) (width 0.508) (layer "B.Cu") (net 1) (tstamp 87a6cea5-7eeb-4a7c-9676-cd20559c5b4c))
+  (arc (start 148.59 115.062) (mid 147.691974 115.433974) (end 147.32 116.332) (width 0.508) (layer "B.Cu") (net 1) (tstamp 92918a26-f3e7-4ab1-b6fe-c7c8e0d4fcc9))
+  (segment (start 148.59 121.158) (end 149.606 121.158) (width 0.508) (layer "B.Cu") (net 2) (tstamp 1cff39b5-dffa-45c8-9d9e-ad67501381ac))
+  (segment (start 147.32 119.38) (end 147.32 119.888) (width 0.508) (layer "B.Cu") (net 2) (tstamp 24f848e3-8dee-4c26-bb11-62626cc4794d))
+  (segment (start 150.876 119.888) (end 150.876 118.919) (width 0.508) (layer "B.Cu") (net 2) (tstamp 51bb6c19-543d-4f8b-8732-511f3eeb46a8))
+  (arc (start 147.32 119.888) (mid 147.691974 120.786026) (end 148.59 121.158) (width 0.508) (layer "B.Cu") (net 2) (tstamp 691576cc-9fda-40be-bc7d-3fe2fa68f19a))
+  (arc (start 149.606 121.158) (mid 150.504026 120.786026) (end 150.876 119.888) (width 0.508) (layer "B.Cu") (net 2) (tstamp 97686675-611c-4b29-9007-70bf5369feb4))
+
+)
diff --git a/circuit_boards/CAN_boards/CAN_Terminator/CAN_terminator.kicad_prl b/circuit_boards/CAN_boards/CAN_Terminator/CAN_terminator.kicad_prl
new file mode 100644
index 0000000..a71949c
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/CAN_terminator.kicad_prl
@@ -0,0 +1,75 @@
+{
+  "board": {
+    "active_layer": 36,
+    "active_layer_preset": "",
+    "auto_track_width": true,
+    "hidden_nets": [],
+    "high_contrast_mode": 0,
+    "net_color_mode": 1,
+    "opacity": {
+      "pads": 1.0,
+      "tracks": 1.0,
+      "vias": 1.0,
+      "zones": 0.6
+    },
+    "ratsnest_display_mode": 0,
+    "selection_filter": {
+      "dimensions": true,
+      "footprints": true,
+      "graphics": true,
+      "keepouts": true,
+      "lockedItems": true,
+      "otherItems": true,
+      "pads": true,
+      "text": true,
+      "tracks": true,
+      "vias": true,
+      "zones": true
+    },
+    "visible_items": [
+      0,
+      1,
+      2,
+      3,
+      4,
+      5,
+      8,
+      9,
+      10,
+      11,
+      12,
+      13,
+      14,
+      15,
+      16,
+      17,
+      18,
+      19,
+      20,
+      21,
+      22,
+      23,
+      24,
+      25,
+      26,
+      27,
+      28,
+      29,
+      30,
+      32,
+      33,
+      34,
+      35,
+      36
+    ],
+    "visible_layers": "ffffdff_ffffffff",
+    "zone_display_mode": 0
+  },
+  "meta": {
+    "filename": "CAN_terminator.kicad_prl",
+    "version": 3
+  },
+  "project": {
+    "files": []
+  }
+}
diff --git a/circuit_boards/CAN_boards/CAN_Terminator/CAN_terminator.kicad_pro b/circuit_boards/CAN_boards/CAN_Terminator/CAN_terminator.kicad_pro
new file mode 100644
index 0000000..de08af6
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/CAN_terminator.kicad_pro
@@ -0,0 +1,326 @@
+{
+  "board": {
+    "design_settings": {
+      "defaults": {
+        "board_outline_line_width": 0.1,
+        "copper_line_width": 0.2,
+        "copper_text_size_h": 1.5,
+        "copper_text_size_v": 1.5,
+        "copper_text_thickness": 0.3,
+        "other_line_width": 0.15,
+        "silk_line_width": 0.15,
+        "silk_text_size_h": 1.0,
+        "silk_text_size_v": 1.0,
+        "silk_text_thickness": 0.15
+      },
+      "diff_pair_dimensions": [],
+      "drc_exclusions": [],
+      "rules": {
+        "min_copper_edge_clearance": 0.0,
+        "solder_mask_clearance": 0.0,
+        "solder_mask_min_width": 0.0
+      },
+      "track_widths": [],
+      "via_dimensions": []
+    },
+    "layer_presets": []
+  },
+  "boards": [],
+  "cvpcb": {
+    "equivalence_files": []
+  },
+  "erc": {
+    "erc_exclusions": [],
+    "meta": {
+      "version": 0
+    },
+    "pin_map": [
+      [
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        2
+      ],
+      [
+        0,
+        2,
+        0,
+        1,
+        0,
+        0,
+        1,
+        0,
+        2,
+        2,
+        2,
+        2
+      ],
+      [
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        1,
+        0,
+        1,
+        2
+      ],
+      [
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        1,
+        2,
+        1,
+        1,
+        2
+      ],
+      [
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        2
+      ],
+      [
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        2
+      ],
+      [
+        1,
+        1,
+        1,
+        1,
+        1,
+        0,
+        1,
+        1,
+        1,
+        1,
+        1,
+        2
+      ],
+      [
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        2
+      ],
+      [
+        0,
+        2,
+        1,
+        2,
+        0,
+        0,
+        1,
+        0,
+        2,
+        2,
+        2,
+        2
+      ],
+      [
+        0,
+        2,
+        0,
+        1,
+        0,
+        0,
+        1,
+        0,
+        2,
+        0,
+        0,
+        2
+      ],
+      [
+        0,
+        2,
+        1,
+        1,
+        0,
+        0,
+        1,
+        0,
+        2,
+        0,
+        0,
+        2
+      ],
+      [
+        2,
+        2,
+        2,
+        2,
+        2,
+        2,
+        2,
+        2,
+        2,
+        2,
+        2,
+        2
+      ]
+    ],
+    "rule_severities": {
+      "bus_definition_conflict": "error",
+      "bus_entry_needed": "error",
+      "bus_label_syntax": "error",
+      "bus_to_bus_conflict": "error",
+      "bus_to_net_conflict": "error",
+      "different_unit_footprint": "error",
+      "different_unit_net": "error",
+      "duplicate_reference": "error",
+      "duplicate_sheet_names": "error",
+      "extra_units": "error",
+      "global_label_dangling": "warning",
+      "hier_label_mismatch": "error",
+      "label_dangling": "error",
+      "lib_symbol_issues": "warning",
+      "multiple_net_names": "warning",
+      "net_not_bus_member": "warning",
+      "no_connect_connected": "warning",
+      "no_connect_dangling": "warning",
+      "pin_not_connected": "error",
+      "pin_not_driven": "error",
+      "pin_to_pin": "warning",
+      "power_pin_not_driven": "error",
+      "similar_labels": "warning",
+      "unannotated": "error",
+      "unit_value_mismatch": "error",
+      "unresolved_variable": "error",
+      "wire_dangling": "error"
+    }
+  },
+  "libraries": {
+    "pinned_footprint_libs": [],
+    "pinned_symbol_libs": []
+  },
+  "meta": {
+    "filename": "CAN_terminator.kicad_pro",
+    "version": 1
+  },
+  "net_settings": {
+    "classes": [
+      {
+        "bus_width": 12.0,
+        "clearance": 0.2,
+        "diff_pair_gap": 0.25,
+        "diff_pair_via_gap": 0.25,
+        "diff_pair_width": 0.2,
+        "line_style": 0,
+        "microvia_diameter": 0.3,
+        "microvia_drill": 0.1,
+        "name": "Default",
+        "pcb_color": "rgba(0, 0, 0, 0.000)",
+        "schematic_color": "rgba(0, 0, 0, 0.000)",
+        "track_width": 0.25,
+        "via_diameter": 0.8,
+        "via_drill": 0.4,
+        "wire_width": 6.0
+      }
+    ],
+    "meta": {
+      "version": 2
+    },
+    "net_colors": null
+  },
+  "pcbnew": {
+    "last_paths": {
+      "gencad": "",
+      "idf": "",
+      "netlist": "",
+      "specctra_dsn": "",
+      "step": "",
+      "vrml": ""
+    },
+    "page_layout_descr_file": ""
+  },
+  "schematic": {
+    "annotate_start_num": 0,
+    "drawing": {
+      "default_line_thickness": 6.0,
+      "default_text_size": 50.0,
+      "field_names": [],
+      "intersheets_ref_own_page": false,
+      "intersheets_ref_prefix": "",
+      "intersheets_ref_short": false,
+      "intersheets_ref_show": false,
+      "intersheets_ref_suffix": "",
+      "junction_size_choice": 3,
+      "label_size_ratio": 0.375,
+      "pin_symbol_size": 25.0,
+      "text_offset_ratio": 0.15
+    },
+    "legacy_lib_dir": "",
+    "legacy_lib_list": [],
+    "meta": {
+      "version": 1
+    },
+    "net_format_name": "",
+    "ngspice": {
+      "fix_include_paths": true,
+      "fix_passive_vals": false,
+      "meta": {
+        "version": 0
+      },
+      "model_mode": 0,
+      "workbook_filename": ""
+    },
+    "page_layout_descr_file": "",
+    "plot_directory": "./",
+    "spice_adjust_passive_values": false,
+    "spice_external_command": "spice \"%I\"",
+    "subpart_first_id": 65,
+    "subpart_id_separator": 0
+  },
+  "sheets": [
+    [
+      "8caaff45-1713-4013-a97e-9b4f033294ef",
+      ""
+    ]
+  ],
+  "text_variables": {}
+}
diff --git a/circuit_boards/CAN_boards/CAN_Terminator/CAN_terminator.kicad_sch b/circuit_boards/CAN_boards/CAN_Terminator/CAN_terminator.kicad_sch
new file mode 100644
index 0000000..54cf30c
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/CAN_terminator.kicad_sch
@@ -0,0 +1,239 @@
+(kicad_sch (version 20211123) (generator eeschema)

+

+  (uuid 8caaff45-1713-4013-a97e-9b4f033294ef)

+

+  (paper "USLetter")

+

+  (title_block

+    (title "CAN Terminator")

+    (date "2022-08-20")

+    (rev "V1.0")

+    (company "971 Spartan Robotics")

+  )

+

+  (lib_symbols

+    (symbol "Connector_Generic:Conn_01x02" (pin_names (offset 1.016) hide) (in_bom yes) (on_board yes)

+      (property "Reference" "J" (id 0) (at 0 2.54 0)

+        (effects (font (size 1.27 1.27)))

+      )

+      (property "Value" "Conn_01x02" (id 1) (at 0 -5.08 0)

+        (effects (font (size 1.27 1.27)))

+      )

+      (property "Footprint" "" (id 2) (at 0 0 0)

+        (effects (font (size 1.27 1.27)) hide)

+      )

+      (property "Datasheet" "~" (id 3) (at 0 0 0)

+        (effects (font (size 1.27 1.27)) hide)

+      )

+      (property "ki_keywords" "connector" (id 4) (at 0 0 0)

+        (effects (font (size 1.27 1.27)) hide)

+      )

+      (property "ki_description" "Generic connector, single row, 01x02, script generated (kicad-library-utils/schlib/autogen/connector/)" (id 5) (at 0 0 0)

+        (effects (font (size 1.27 1.27)) hide)

+      )

+      (property "ki_fp_filters" "Connector*:*_1x??_*" (id 6) (at 0 0 0)

+        (effects (font (size 1.27 1.27)) hide)

+      )

+      (symbol "Conn_01x02_1_1"

+        (rectangle (start -1.27 -2.413) (end 0 -2.667)

+          (stroke (width 0.1524) (type default) (color 0 0 0 0))

+          (fill (type none))

+        )

+        (rectangle (start -1.27 0.127) (end 0 -0.127)

+          (stroke (width 0.1524) (type default) (color 0 0 0 0))

+          (fill (type none))

+        )

+        (rectangle (start -1.27 1.27) (end 1.27 -3.81)

+          (stroke (width 0.254) (type default) (color 0 0 0 0))

+          (fill (type background))

+        )

+        (pin passive line (at -5.08 0 0) (length 3.81)

+          (name "Pin_1" (effects (font (size 1.27 1.27))))

+          (number "1" (effects (font (size 1.27 1.27))))

+        )

+        (pin passive line (at -5.08 -2.54 0) (length 3.81)

+          (name "Pin_2" (effects (font (size 1.27 1.27))))

+          (number "2" (effects (font (size 1.27 1.27))))

+        )

+      )

+    )

+    (symbol "Device:R_US" (pin_numbers hide) (pin_names (offset 0)) (in_bom yes) (on_board yes)

+      (property "Reference" "R" (id 0) (at 2.54 0 90)

+        (effects (font (size 1.27 1.27)))

+      )

+      (property "Value" "R_US" (id 1) (at -2.54 0 90)

+        (effects (font (size 1.27 1.27)))

+      )

+      (property "Footprint" "" (id 2) (at 1.016 -0.254 90)

+        (effects (font (size 1.27 1.27)) hide)

+      )

+      (property "Datasheet" "~" (id 3) (at 0 0 0)

+        (effects (font (size 1.27 1.27)) hide)

+      )

+      (property "ki_keywords" "R res resistor" (id 4) (at 0 0 0)

+        (effects (font (size 1.27 1.27)) hide)

+      )

+      (property "ki_description" "Resistor, US symbol" (id 5) (at 0 0 0)

+        (effects (font (size 1.27 1.27)) hide)

+      )

+      (property "ki_fp_filters" "R_*" (id 6) (at 0 0 0)

+        (effects (font (size 1.27 1.27)) hide)

+      )

+      (symbol "R_US_0_1"

+        (polyline

+          (pts

+            (xy 0 -2.286)

+            (xy 0 -2.54)

+          )

+          (stroke (width 0) (type default) (color 0 0 0 0))

+          (fill (type none))

+        )

+        (polyline

+          (pts

+            (xy 0 2.286)

+            (xy 0 2.54)

+          )

+          (stroke (width 0) (type default) (color 0 0 0 0))

+          (fill (type none))

+        )

+        (polyline

+          (pts

+            (xy 0 -0.762)

+            (xy 1.016 -1.143)

+            (xy 0 -1.524)

+            (xy -1.016 -1.905)

+            (xy 0 -2.286)

+          )

+          (stroke (width 0) (type default) (color 0 0 0 0))

+          (fill (type none))

+        )

+        (polyline

+          (pts

+            (xy 0 0.762)

+            (xy 1.016 0.381)

+            (xy 0 0)

+            (xy -1.016 -0.381)

+            (xy 0 -0.762)

+          )

+          (stroke (width 0) (type default) (color 0 0 0 0))

+          (fill (type none))

+        )

+        (polyline

+          (pts

+            (xy 0 2.286)

+            (xy 1.016 1.905)

+            (xy 0 1.524)

+            (xy -1.016 1.143)

+            (xy 0 0.762)

+          )

+          (stroke (width 0) (type default) (color 0 0 0 0))

+          (fill (type none))

+        )

+      )

+      (symbol "R_US_1_1"

+        (pin passive line (at 0 3.81 270) (length 1.27)

+          (name "~" (effects (font (size 1.27 1.27))))

+          (number "1" (effects (font (size 1.27 1.27))))

+        )

+        (pin passive line (at 0 -3.81 90) (length 1.27)

+          (name "~" (effects (font (size 1.27 1.27))))

+          (number "2" (effects (font (size 1.27 1.27))))

+        )

+      )

+    )

+  )

+

+

+  (wire (pts (xy 128.27 111.76) (xy 128.27 105.41))

+    (stroke (width 0) (type default) (color 0 0 0 0))

+    (uuid 132da87c-3baa-426d-9066-829f7a1c28ca)

+  )

+  (wire (pts (xy 105.41 105.41) (xy 105.41 109.22))

+    (stroke (width 0) (type default) (color 0 0 0 0))

+    (uuid 16080253-9679-4f09-ae3c-0d227a31af37)

+  )

+  (wire (pts (xy 128.27 120.65) (xy 128.27 114.3))

+    (stroke (width 0) (type default) (color 0 0 0 0))

+    (uuid 1c2bb4d1-25e3-4267-b31c-15fa51f79efa)

+  )

+  (wire (pts (xy 105.41 120.65) (xy 128.27 120.65))

+    (stroke (width 0) (type default) (color 0 0 0 0))

+    (uuid 2b854e78-cf10-434e-80f3-55de5d8462ca)

+  )

+  (wire (pts (xy 128.27 105.41) (xy 105.41 105.41))

+    (stroke (width 0) (type default) (color 0 0 0 0))

+    (uuid 42c66820-779d-4567-afe2-0ac717e72ae9)

+  )

+  (wire (pts (xy 133.35 111.76) (xy 128.27 111.76))

+    (stroke (width 0) (type default) (color 0 0 0 0))

+    (uuid 8ce9985d-a05c-4d8e-a69f-f4915fbf5366)

+  )

+  (wire (pts (xy 105.41 116.84) (xy 105.41 120.65))

+    (stroke (width 0) (type default) (color 0 0 0 0))

+    (uuid d46455f0-49d8-4114-b429-ae1e6176838d)

+  )

+  (wire (pts (xy 128.27 114.3) (xy 133.35 114.3))

+    (stroke (width 0) (type default) (color 0 0 0 0))

+    (uuid d82afd32-f227-4439-bb6a-506a8bdec942)

+  )

+

+  (label "CAN_L" (at 113.03 120.65 0)

+    (effects (font (size 1.27 1.27)) (justify left bottom))

+    (uuid 00c76874-07d7-4d12-98ee-a7a628c5ea6b)

+  )

+  (label "CAN_H" (at 113.03 105.41 0)

+    (effects (font (size 1.27 1.27)) (justify left bottom))

+    (uuid a15fc3cb-764b-4b2b-b58a-28846c8afa34)

+  )

+

+  (symbol (lib_id "Device:R_US") (at 105.41 113.03 0) (unit 1)

+    (in_bom yes) (on_board yes) (fields_autoplaced)

+    (uuid c78ffe14-863e-4c6e-af99-6d6cf6e857aa)

+    (property "Reference" "R1" (id 0) (at 107.95 111.7599 0)

+      (effects (font (size 1.27 1.27)) (justify left))

+    )

+    (property "Value" "120 ohm" (id 1) (at 107.95 114.2999 0)

+      (effects (font (size 1.27 1.27)) (justify left))

+    )

+    (property "Footprint" "Resistor_SMD:R_0805_2012Metric" (id 2) (at 106.426 113.284 90)

+      (effects (font (size 1.27 1.27)) hide)

+    )

+    (property "Datasheet" "~" (id 3) (at 105.41 113.03 0)

+      (effects (font (size 1.27 1.27)) hide)

+    )

+    (pin "1" (uuid 8571995a-c61d-4156-aab5-9044acb9c951))

+    (pin "2" (uuid a0e40ed7-58f5-46d0-95bb-25409f92731d))

+  )

+

+  (symbol (lib_id "Connector_Generic:Conn_01x02") (at 138.43 111.76 0) (unit 1)

+    (in_bom yes) (on_board yes) (fields_autoplaced)

+    (uuid e8bffcfe-c870-4593-8a2b-7a667dd6382d)

+    (property "Reference" "J1" (id 0) (at 140.97 111.7599 0)

+      (effects (font (size 1.27 1.27)) (justify left))

+    )

+    (property "Value" "2 pin molex sl right angle" (id 1) (at 140.97 114.2999 0)

+      (effects (font (size 1.27 1.27)) (justify left))

+    )

+    (property "Footprint" "CAN_terminator:70555-0036" (id 2) (at 138.43 111.76 0)

+      (effects (font (size 1.27 1.27)) hide)

+    )

+    (property "Datasheet" "~" (id 3) (at 138.43 111.76 0)

+      (effects (font (size 1.27 1.27)) hide)

+    )

+    (pin "1" (uuid 50d9b5e2-32db-4f4d-a17c-99ca7d722b41))

+    (pin "2" (uuid 0bf9eb86-5378-4e96-91ca-e45822cd719e))

+  )

+

+  (sheet_instances

+    (path "/" (page "1"))

+  )

+

+  (symbol_instances

+    (path "/e8bffcfe-c870-4593-8a2b-7a667dd6382d"

+      (reference "J1") (unit 1) (value "2 pin molex sl right angle") (footprint "CAN_terminator:70555-0036")

+    )

+    (path "/c78ffe14-863e-4c6e-af99-6d6cf6e857aa"

+      (reference "R1") (unit 1) (value "120 ohm") (footprint "Resistor_SMD:R_0805_2012Metric")

+    )

+  )

+)

diff --git a/circuit_boards/CAN_boards/CAN_Terminator/CAN_terminator.pdf b/circuit_boards/CAN_boards/CAN_Terminator/CAN_terminator.pdf
new file mode 100644
index 0000000..6c32349
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/CAN_terminator.pdf
Binary files differ
diff --git a/circuit_boards/CAN_boards/CAN_Terminator/CAN_terminator.pretty/70555-0036.kicad_mod b/circuit_boards/CAN_boards/CAN_Terminator/CAN_terminator.pretty/70555-0036.kicad_mod
new file mode 100644
index 0000000..dbaaf03
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/CAN_terminator.pretty/70555-0036.kicad_mod
@@ -0,0 +1,104 @@
+(footprint "70555-0036" (version 20211014) (generator pcbnew)

+  (layer "F.Cu")

+  (tedit 0)

+  (attr through_hole)

+  (fp_text reference "REF**" (at -1.27 -2.54) (layer "F.SilkS") hide

+    (effects (font (size 1 1) (thickness 0.15)))

+    (tstamp a637ec7c-75ad-40cc-bf25-c9128703e18e)

+  )

+  (fp_text value "CONN_70555-0036_MOL" (at -1.27 -2.54) (layer "F.SilkS") hide

+    (effects (font (size 1 1) (thickness 0.15)))

+    (tstamp 4fc22d93-c3d7-4028-98ce-997a45d349f0)

+  )

+  (fp_line (start 3.1877 7.668671) (end 3.1877 -0.777) (layer "F.SilkS") (width 0.12) (tstamp 0055cacd-42a1-4b46-9ede-aec3cb861e96))

+  (fp_line (start -5.7277 9.61133) (end -5.7277 13.066001) (layer "F.SilkS") (width 0.12) (tstamp 13fd91cb-1534-4e0c-9633-a112903a58be))

+  (fp_line (start -5.7277 13.066001) (end 3.1877 13.066001) (layer "F.SilkS") (width 0.12) (tstamp 3fe370f9-d589-4d90-bb35-419cf6f2007c))

+  (fp_line (start 3.1877 -0.777) (end 1.27 -0.777) (layer "F.SilkS") (width 0.12) (tstamp a1955cab-c40f-4eb5-b49a-9e55b820c051))

+  (fp_line (start 3.1877 13.066001) (end 3.1877 9.61133) (layer "F.SilkS") (width 0.12) (tstamp b1c4628e-4ca7-47d3-a192-095c5d42a51c))

+  (fp_line (start -3.311185 -0.777) (end -5.7277 -0.777) (layer "F.SilkS") (width 0.12) (tstamp c708b7e6-0f15-4151-8f31-12b7dba4ce4e))

+  (fp_line (start -5.7277 -0.777) (end -5.7277 7.668671) (layer "F.SilkS") (width 0.12) (tstamp cde8d358-6018-4574-84ff-062501c7ea31))

+  (fp_line (start -16.0558 0) (end -16.1828 0.254) (layer "Cmts.User") (width 0.1) (tstamp 04ae59ee-7a5f-42e0-8b3c-12636c325aee))

+  (fp_line (start -3.94 -8.636) (end -3.686 -8.509) (layer "Cmts.User") (width 0.1) (tstamp 05ef887f-4540-49c7-be70-43c58844c91f))

+  (fp_line (start 1.4 8.640001) (end -8.1818 8.640001) (layer "Cmts.User") (width 0.1) (tstamp 086b87e8-bfef-4521-baa4-9ee2ded580c9))

+  (fp_line (start -2.54 0) (end -2.54 -5.842) (layer "Cmts.User") (width 0.1) (tstamp 096ead2f-3032-410c-9480-3f5584fdd801))

+  (fp_line (start 3.0607 -11.176) (end 2.8067 -11.303) (layer "Cmts.User") (width 0.1) (tstamp 11b3f72a-26ec-4967-a815-144a5592b95c))

+  (fp_line (start -16.0558 -0.65) (end -15.9288 -0.904) (layer "Cmts.User") (width 0.1) (tstamp 11b8f84d-7fac-48e4-bb25-0f97c8fc70f2))

+  (fp_line (start -5.6007 12.939001) (end -15.1668 12.939001) (layer "Cmts.User") (width 0.1) (tstamp 11d481ec-b267-4e5c-b519-bb7d01c6da97))

+  (fp_line (start 0 -5.461) (end 0.254 -5.334) (layer "Cmts.User") (width 0.1) (tstamp 1ec515b2-8a95-4b3e-85c8-389200ccf123))

+  (fp_line (start -16.1828 -0.904) (end -15.9288 -0.904) (layer "Cmts.User") (width 0.1) (tstamp 24bbb1ee-9b37-4dc1-9128-ddf9c1b809a0))

+  (fp_line (start -7.9278 0.254) (end -7.6738 0.254) (layer "Cmts.User") (width 0.1) (tstamp 28f54e94-ec22-4cd2-8029-d541b7f0f513))

+  (fp_line (start -16.0558 -0.65) (end -16.0558 -1.92) (layer "Cmts.User") (width 0.1) (tstamp 378009e1-b239-433e-8679-51e5961ad562))

+  (fp_line (start 5.6007 0) (end 5.4737 0.254) (layer "Cmts.User") (width 0.1) (tstamp 381834cc-3b73-43a9-8c39-b915d8c6099c))

+  (fp_line (start 0.254 -5.588) (end 0.254 -5.334) (layer "Cmts.User") (width 0.1) (tstamp 38a048bd-3dd1-42c8-8463-fc2b1b59d751))

+  (fp_line (start -14.9128 -0.396) (end -14.6588 -0.396) (layer "Cmts.User") (width 0.1) (tstamp 40ab74e1-98e2-4646-bf5b-23606a24427a))

+  (fp_line (start -16.0558 -0.65) (end -16.1828 -0.904) (layer "Cmts.User") (width 0.1) (tstamp 40c1eb3e-3696-40e3-9674-5db7853a51a2))

+  (fp_line (start -3.686 -8.763) (end -3.686 -8.509) (layer "Cmts.User") (width 0.1) (tstamp 41981f46-b053-4690-ab8d-137ca090545d))

+  (fp_line (start -5.6007 -11.176) (end -5.3467 -11.303) (layer "Cmts.User") (width 0.1) (tstamp 42008337-8863-4b41-b559-aae67c980ac8))

+  (fp_line (start 3.0607 -11.176) (end 2.8067 -11.049) (layer "Cmts.User") (width 0.1) (tstamp 4ce4c298-707b-4516-964d-f74e3aea4d52))

+  (fp_line (start -5.6007 -11.176) (end 3.0607 -11.176) (layer "Cmts.User") (width 0.1) (tstamp 50e78a7b-2763-4de7-8724-6296f95282fc))

+  (fp_line (start -5.6007 -0.65) (end -16.4368 -0.65) (layer "Cmts.User") (width 0.1) (tstamp 52234db8-4ba9-43e2-9b57-f95144b2a41b))

+  (fp_line (start 0 0) (end 5.9817 0) (layer "Cmts.User") (width 0.1) (tstamp 5e41804b-414e-4a5e-a4e8-2bafc930e1a9))

+  (fp_line (start -2.794 -5.588) (end -2.794 -5.334) (layer "Cmts.User") (width 0.1) (tstamp 5f05f809-7aae-4fa0-bde1-19292a9bc622))

+  (fp_line (start 5.6007 0) (end 5.6007 -1.27) (layer "Cmts.User") (width 0.1) (tstamp 6760ce19-7881-46ec-9487-399928257ff6))

+  (fp_line (start 1.4 -8.636) (end 1.146 -8.509) (layer "Cmts.User") (width 0.1) (tstamp 6a3b209d-ffa3-4cce-b869-df2a1eb58b21))

+  (fp_line (start -14.7858 -0.65) (end -14.7858 12.939001) (layer "Cmts.User") (width 0.1) (tstamp 6fb5189d-b997-4bb3-870d-29119785040b))

+  (fp_line (start -7.8008 8.640001) (end -7.9278 8.386001) (layer "Cmts.User") (width 0.1) (tstamp 72742d07-33e8-46fc-b079-4be459be1d61))

+  (fp_line (start 5.6007 0) (end 5.6007 1.27) (layer "Cmts.User") (width 0.1) (tstamp 7882e5ec-1952-4fe4-a435-3e01dd9d8644))

+  (fp_line (start -1.27 0) (end -16.4368 0) (layer "Cmts.User") (width 0.1) (tstamp 7969d0a4-70da-43b7-9298-d0d962b319f4))

+  (fp_line (start -14.7858 12.939001) (end -14.6588 12.685001) (layer "Cmts.User") (width 0.1) (tstamp 859be0b6-7ae7-4cc1-b5d3-4e4bb54ac7b4))

+  (fp_line (start 0 0) (end 5.9817 0) (layer "Cmts.User") (width 0.1) (tstamp 884deb15-7009-4afb-a7b5-8a6f2e44da55))

+  (fp_line (start -5.6007 -0.65) (end -15.1668 -0.65) (layer "Cmts.User") (width 0.1) (tstamp 89ea3951-c0a0-40fc-9546-54cbf97559ef))

+  (fp_line (start 1.146 -8.763) (end 1.146 -8.509) (layer "Cmts.User") (width 0.1) (tstamp 8b3c93fd-90a8-40aa-bcb2-b63067e41902))

+  (fp_line (start -3.94 -8.636) (end -3.686 -8.763) (layer "Cmts.User") (width 0.1) (tstamp 8d56ad97-87f6-495a-9ea0-d5eacd8ac8be))

+  (fp_line (start -7.8008 8.640001) (end -7.6738 8.386001) (layer "Cmts.User") (width 0.1) (tstamp 8f839a12-f6f4-47ac-aa9e-670c126bae8f))

+  (fp_line (start 1.4 -8.636) (end 1.146 -8.763) (layer "Cmts.User") (width 0.1) (tstamp 9336c6e0-89b1-47b6-a338-18b48e3eccdb))

+  (fp_line (start -5.6007 -0.65) (end -5.6007 -11.557) (layer "Cmts.User") (width 0.1) (tstamp 9a92e568-5b1b-48d8-843a-d115fac522d1))

+  (fp_line (start -3.94 -8.636) (end 1.4 -8.636) (layer "Cmts.User") (width 0.1) (tstamp 9c524846-b502-40f2-8c1e-7f0ca29abcce))

+  (fp_line (start 3.0607 -0.65) (end 3.0607 -11.557) (layer "Cmts.User") (width 0.1) (tstamp a9b50f99-5e96-49e2-94c4-69771c4fab6d))

+  (fp_line (start 2.8067 -11.303) (end 2.8067 -11.049) (layer "Cmts.User") (width 0.1) (tstamp abf9614e-67ac-40dd-a0cf-62bb43ba6390))

+  (fp_line (start -7.8008 0) (end -7.8008 8.640001) (layer "Cmts.User") (width 0.1) (tstamp ada78ddd-0212-4df5-993e-6a96b08962c8))

+  (fp_line (start -5.6007 -11.176) (end -5.3467 -11.049) (layer "Cmts.User") (width 0.1) (tstamp af35deb4-61bd-4ffa-835a-1088e4086461))

+  (fp_line (start -16.1828 0.254) (end -15.9288 0.254) (layer "Cmts.User") (width 0.1) (tstamp b15a20d3-2f8e-4e0c-ae47-a0ff0d9ba72d))

+  (fp_line (start 5.4737 0.254) (end 5.7277 0.254) (layer "Cmts.User") (width 0.1) (tstamp b2225a88-9450-49ef-aa59-6dcda4bc793a))

+  (fp_line (start 0 0) (end 0 -5.842) (layer "Cmts.User") (width 0.1) (tstamp b566ea23-5d96-4503-8b21-6ff130407837))

+  (fp_line (start -3.94 8.640001) (end -3.94 -9.017) (layer "Cmts.User") (width 0.1) (tstamp b84d37c1-0f6b-4959-a96e-2ec215df2f55))

+  (fp_line (start 0 -5.461) (end 0.254 -5.588) (layer "Cmts.User") (width 0.1) (tstamp b857338b-7569-4a1f-a2e5-33863b256df6))

+  (fp_line (start -7.8008 0) (end -7.9278 0.254) (layer "Cmts.User") (width 0.1) (tstamp bcf26687-fa97-4825-8279-aeb9f3ebf98e))

+  (fp_line (start -14.7858 -0.65) (end -14.6588 -0.396) (layer "Cmts.User") (width 0.1) (tstamp bd8d5c78-fb56-42aa-9471-49f80b981d41))

+  (fp_line (start 5.6007 0) (end 5.7277 -0.254) (layer "Cmts.User") (width 0.1) (tstamp c0267093-3637-4e52-8ff6-fb45a153a6cf))

+  (fp_line (start -7.8008 0) (end -7.6738 0.254) (layer "Cmts.User") (width 0.1) (tstamp c487eed0-25d5-4cb9-8e98-41dc4d62f0cb))

+  (fp_line (start -5.3467 -11.303) (end -5.3467 -11.049) (layer "Cmts.User") (width 0.1) (tstamp d274b4f7-b31e-462d-9cae-7f1c6ed5a3d7))

+  (fp_line (start -2.54 -5.461) (end -3.81 -5.461) (layer "Cmts.User") (width 0.1) (tstamp d7d5f5cd-5c60-4d54-819e-a0ef8dc4022e))

+  (fp_line (start 5.6007 0) (end 5.7277 0.254) (layer "Cmts.User") (width 0.1) (tstamp dab0cfb2-4c57-4f6d-907c-5502896a59f6))

+  (fp_line (start -16.0558 0) (end -15.9288 0.254) (layer "Cmts.User") (width 0.1) (tstamp dafa37ff-e739-45d8-9e55-e09577a94397))

+  (fp_line (start -14.7858 12.939001) (end -14.9128 12.685001) (layer "Cmts.User") (width 0.1) (tstamp dc73bb49-e5e5-4062-887b-79f0bff305e1))

+  (fp_line (start 5.4737 -0.254) (end 5.7277 -0.254) (layer "Cmts.User") (width 0.1) (tstamp e44a7830-31a3-48bd-96d5-863125ee7e25))

+  (fp_line (start -1.27 0) (end -8.1818 0) (layer "Cmts.User") (width 0.1) (tstamp e5a2238b-5459-4d8c-bb93-f5262d5086e8))

+  (fp_line (start -7.9278 8.386001) (end -7.6738 8.386001) (layer "Cmts.User") (width 0.1) (tstamp e959cc5a-d690-4e74-a4a0-ad0be6db0eca))

+  (fp_line (start -2.54 -5.461) (end -2.794 -5.334) (layer "Cmts.User") (width 0.1) (tstamp eb85b359-bde4-46ff-8294-dfe1e63f1ec8))

+  (fp_line (start 5.6007 0) (end 5.4737 -0.254) (layer "Cmts.User") (width 0.1) (tstamp efc7c558-4deb-4150-bebc-9c103d33280f))

+  (fp_line (start 1.4 8.640001) (end 1.4 -9.017) (layer "Cmts.User") (width 0.1) (tstamp f1403736-0953-4201-a37f-a75754507117))

+  (fp_line (start -16.0558 0) (end -16.0558 1.27) (layer "Cmts.User") (width 0.1) (tstamp f2b12c8b-695e-41fd-ab8d-a84f0af72566))

+  (fp_line (start -14.9128 12.685001) (end -14.6588 12.685001) (layer "Cmts.User") (width 0.1) (tstamp fa7212a2-3046-4606-9a1a-14e7b8a52907))

+  (fp_line (start -2.54 -5.461) (end -2.794 -5.588) (layer "Cmts.User") (width 0.1) (tstamp fd3438dc-1570-4d59-8a64-56e57c425b93))

+  (fp_line (start 0 -5.461) (end 1.27 -5.461) (layer "Cmts.User") (width 0.1) (tstamp febb73c9-ccaa-4a28-be70-3792d5c07fc5))

+  (fp_line (start -14.7858 -0.65) (end -14.9128 -0.396) (layer "Cmts.User") (width 0.1) (tstamp ff0552ae-0194-4155-b5f2-8a4fa1645e29))

+  (fp_line (start -5.842 -1.016) (end -5.842 13.208) (layer "F.CrtYd") (width 0.05) (tstamp 0eb2133e-f922-42b1-8334-81a9257d1b04))

+  (fp_line (start 3.302 13.208) (end 3.302 -1.016) (layer "F.CrtYd") (width 0.05) (tstamp 389cf10b-5e4c-458a-b100-30d6fee48ae1))

+  (fp_line (start -5.842 13.208) (end 3.302 13.208) (layer "F.CrtYd") (width 0.05) (tstamp 38bd06ba-0acd-4765-9dd9-7db0a93c3b06))

+  (fp_line (start 3.302 -1.016) (end -5.842 -1.016) (layer "F.CrtYd") (width 0.05) (tstamp 6f780366-0098-4a09-9d36-8e89ccb494e4))

+  (fp_line (start 3.0607 12.939001) (end 3.0607 -0.65) (layer "F.Fab") (width 0.1) (tstamp 247e8377-ffdd-494d-a3a6-b3e8589d068d))

+  (fp_line (start -5.6007 -0.65) (end -5.6007 12.939001) (layer "F.Fab") (width 0.1) (tstamp 91bbe933-a48f-4848-b09e-babd44079522))

+  (fp_line (start 3.0607 -0.65) (end -5.6007 -0.65) (layer "F.Fab") (width 0.1) (tstamp b73ad2b5-47c3-4925-95a7-df028fb933ee))

+  (fp_line (start -5.6007 12.939001) (end 3.0607 12.939001) (layer "F.Fab") (width 0.1) (tstamp c465551b-e82a-4556-a907-cdd41b17bab0))

+  (fp_circle (center 0 -1.905) (end 0.381 -1.905) (layer "F.Fab") (width 0.1) (fill none) (tstamp 09c78b92-293a-4eac-aa0d-01023d43de6a))

+  (pad "1" thru_hole rect (at 0 0) (size 1.524 1.524) (drill 0.9652) (layers *.Cu *.Mask) (tstamp 5e7583cd-f0e4-4f71-8690-fcca5cd247d0))

+  (pad "2" thru_hole circle (at -2.54 0) (size 1.524 1.524) (drill 0.9652) (layers *.Cu *.Mask) (tstamp 1029a2f1-6126-4559-a7d0-2ba50ff85553))

+  (pad "3" thru_hole circle (at -3.94 8.640001) (size 3.4036 3.4036) (drill 3.4036) (layers *.Cu *.Mask) (tstamp 91f31940-2bb5-4543-9fe4-bebc2171c010))

+  (pad "4" thru_hole circle (at 1.4 8.640001) (size 3.4036 3.4036) (drill 3.4036) (layers *.Cu *.Mask) (tstamp c82e0ef2-1666-473e-98f1-5cb9bb1c6ca1))

+  (model "${KIPRJMOD}/3D model/70555-0036_stp/705550036.stp"

+    (offset (xyz -1.27 -7.1882 2.794))

+    (scale (xyz 1 1 1))

+    (rotate (xyz -90 0 0))

+  )

+)

diff --git a/circuit_boards/CAN_boards/CAN_Terminator/fp-info-cache b/circuit_boards/CAN_boards/CAN_Terminator/fp-info-cache
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/fp-info-cache
@@ -0,0 +1 @@
+0
diff --git a/circuit_boards/CAN_boards/CAN_Terminator/fp-lib-table b/circuit_boards/CAN_boards/CAN_Terminator/fp-lib-table
new file mode 100644
index 0000000..c2ac0ea
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/fp-lib-table
@@ -0,0 +1,3 @@
+(fp_lib_table

+  (lib (name "CAN_terminator")(type "KiCad")(uri "${KIPRJMOD}/CAN_terminator.pretty")(options "")(descr ""))

+)

diff --git a/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-B_Cu.gbr b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-B_Cu.gbr
new file mode 100644
index 0000000..7be1df4
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-B_Cu.gbr
@@ -0,0 +1,107 @@
+%TF.GenerationSoftware,KiCad,Pcbnew,6.0.7-f9a2dced07~116~ubuntu20.04.1*%
+%TF.CreationDate,2022-09-17T13:59:53-07:00*%
+%TF.ProjectId,CAN_terminator,43414e5f-7465-4726-9d69-6e61746f722e,rev?*%
+%TF.SameCoordinates,Original*%
+%TF.FileFunction,Copper,L2,Bot*%
+%TF.FilePolarity,Positive*%
+%FSLAX46Y46*%
+G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
+G04 Created by KiCad (PCBNEW 6.0.7-f9a2dced07~116~ubuntu20.04.1) date 2022-09-17 13:59:53*
+%MOMM*%
+%LPD*%
+G01*
+G04 APERTURE LIST*
+G04 Aperture macros list*
+%AMRoundRect*
+0 Rectangle with rounded corners*
+0 $1 Rounding radius*
+0 $2 $3 $4 $5 $6 $7 $8 $9 X,Y pos of 4 corners*
+0 Add a 4 corners polygon primitive as box body*
+4,1,4,$2,$3,$4,$5,$6,$7,$8,$9,$2,$3,0*
+0 Add four circle primitives for the rounded corners*
+1,1,$1+$1,$2,$3*
+1,1,$1+$1,$4,$5*
+1,1,$1+$1,$6,$7*
+1,1,$1+$1,$8,$9*
+0 Add four rect primitives between the rounded corners*
+20,1,$1+$1,$2,$3,$4,$5,0*
+20,1,$1+$1,$4,$5,$6,$7,0*
+20,1,$1+$1,$6,$7,$8,$9,0*
+20,1,$1+$1,$8,$9,$2,$3,0*%
+G04 Aperture macros list end*
+%TA.AperFunction,ComponentPad*%
+%ADD10R,1.524000X1.524000*%
+%TD*%
+%TA.AperFunction,ComponentPad*%
+%ADD11C,1.524000*%
+%TD*%
+%TA.AperFunction,ComponentPad*%
+%ADD12C,3.403600*%
+%TD*%
+%TA.AperFunction,SMDPad,CuDef*%
+%ADD13RoundRect,0.250000X-0.450000X0.262500X-0.450000X-0.262500X0.450000X-0.262500X0.450000X0.262500X0*%
+%TD*%
+%TA.AperFunction,Conductor*%
+%ADD14C,0.508000*%
+%TD*%
+G04 APERTURE END LIST*
+D10*
+%TO.P,J1,1,Pin_1*%
+%TO.N,/CAN_H*%
+X147320000Y-116840000D03*
+D11*
+%TO.P,J1,2,Pin_2*%
+%TO.N,/CAN_L*%
+X147320000Y-119380000D03*
+D12*
+%TO.P,J1,3*%
+%TO.N,N/C*%
+X155960001Y-120780000D03*
+%TO.P,J1,4*%
+X155960001Y-115440000D03*
+%TD*%
+D13*
+%TO.P,R1,1*%
+%TO.N,/CAN_H*%
+X150876000Y-117094000D03*
+%TO.P,R1,2*%
+%TO.N,/CAN_L*%
+X150876000Y-118919000D03*
+%TD*%
+D14*
+%TO.N,/CAN_H*%
+X147320000Y-116332000D02*
+X147320000Y-116840000D01*
+X149606000Y-115062000D02*
+X148590000Y-115062000D01*
+X150876000Y-116332000D02*
+X150876000Y-117094000D01*
+X150876000Y-116332000D02*
+G75*
+G03*
+X149606000Y-115062000I-1270000J0D01*
+G01*
+X147320000Y-116332000D02*
+G75*
+G02*
+X148590000Y-115062000I1270000J0D01*
+G01*
+%TO.N,/CAN_L*%
+X148590000Y-121158000D02*
+X149606000Y-121158000D01*
+X147320000Y-119380000D02*
+X147320000Y-119888000D01*
+X150876000Y-119888000D02*
+X150876000Y-118919000D01*
+X148590000Y-121158000D02*
+G75*
+G02*
+X147320000Y-119888000I0J1270000D01*
+G01*
+X150876000Y-119888000D02*
+G75*
+G02*
+X149606000Y-121158000I-1270000J0D01*
+G01*
+%TD*%
+M02*
diff --git a/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-B_Mask.gbr b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-B_Mask.gbr
new file mode 100644
index 0000000..14a0e36
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-B_Mask.gbr
@@ -0,0 +1,51 @@
+%TF.GenerationSoftware,KiCad,Pcbnew,6.0.7-f9a2dced07~116~ubuntu20.04.1*%
+%TF.CreationDate,2022-09-17T13:59:53-07:00*%
+%TF.ProjectId,CAN_terminator,43414e5f-7465-4726-9d69-6e61746f722e,rev?*%
+%TF.SameCoordinates,Original*%
+%TF.FileFunction,Soldermask,Bot*%
+%TF.FilePolarity,Negative*%
+%FSLAX46Y46*%
+G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
+G04 Created by KiCad (PCBNEW 6.0.7-f9a2dced07~116~ubuntu20.04.1) date 2022-09-17 13:59:53*
+%MOMM*%
+%LPD*%
+G01*
+G04 APERTURE LIST*
+G04 Aperture macros list*
+%AMRoundRect*
+0 Rectangle with rounded corners*
+0 $1 Rounding radius*
+0 $2 $3 $4 $5 $6 $7 $8 $9 X,Y pos of 4 corners*
+0 Add a 4 corners polygon primitive as box body*
+4,1,4,$2,$3,$4,$5,$6,$7,$8,$9,$2,$3,0*
+0 Add four circle primitives for the rounded corners*
+1,1,$1+$1,$2,$3*
+1,1,$1+$1,$4,$5*
+1,1,$1+$1,$6,$7*
+1,1,$1+$1,$8,$9*
+0 Add four rect primitives between the rounded corners*
+20,1,$1+$1,$2,$3,$4,$5,0*
+20,1,$1+$1,$4,$5,$6,$7,0*
+20,1,$1+$1,$6,$7,$8,$9,0*
+20,1,$1+$1,$8,$9,$2,$3,0*%
+G04 Aperture macros list end*
+%ADD10R,1.524000X1.524000*%
+%ADD11C,1.524000*%
+%ADD12C,3.403600*%
+%ADD13RoundRect,0.250000X-0.450000X0.262500X-0.450000X-0.262500X0.450000X-0.262500X0.450000X0.262500X0*%
+G04 APERTURE END LIST*
+D10*
+%TO.C,J1*%
+X147320000Y-116840000D03*
+D11*
+X147320000Y-119380000D03*
+D12*
+X155960001Y-120780000D03*
+X155960001Y-115440000D03*
+%TD*%
+D13*
+%TO.C,R1*%
+X150876000Y-117094000D03*
+X150876000Y-118919000D03*
+%TD*%
+M02*
diff --git a/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-B_Paste.gbr b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-B_Paste.gbr
new file mode 100644
index 0000000..5285611
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-B_Paste.gbr
@@ -0,0 +1,39 @@
+%TF.GenerationSoftware,KiCad,Pcbnew,6.0.7-f9a2dced07~116~ubuntu20.04.1*%
+%TF.CreationDate,2022-09-17T13:59:53-07:00*%
+%TF.ProjectId,CAN_terminator,43414e5f-7465-4726-9d69-6e61746f722e,rev?*%
+%TF.SameCoordinates,Original*%
+%TF.FileFunction,Paste,Bot*%
+%TF.FilePolarity,Positive*%
+%FSLAX46Y46*%
+G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
+G04 Created by KiCad (PCBNEW 6.0.7-f9a2dced07~116~ubuntu20.04.1) date 2022-09-17 13:59:53*
+%MOMM*%
+%LPD*%
+G01*
+G04 APERTURE LIST*
+G04 Aperture macros list*
+%AMRoundRect*
+0 Rectangle with rounded corners*
+0 $1 Rounding radius*
+0 $2 $3 $4 $5 $6 $7 $8 $9 X,Y pos of 4 corners*
+0 Add a 4 corners polygon primitive as box body*
+4,1,4,$2,$3,$4,$5,$6,$7,$8,$9,$2,$3,0*
+0 Add four circle primitives for the rounded corners*
+1,1,$1+$1,$2,$3*
+1,1,$1+$1,$4,$5*
+1,1,$1+$1,$6,$7*
+1,1,$1+$1,$8,$9*
+0 Add four rect primitives between the rounded corners*
+20,1,$1+$1,$2,$3,$4,$5,0*
+20,1,$1+$1,$4,$5,$6,$7,0*
+20,1,$1+$1,$6,$7,$8,$9,0*
+20,1,$1+$1,$8,$9,$2,$3,0*%
+G04 Aperture macros list end*
+%ADD10RoundRect,0.250000X-0.450000X0.262500X-0.450000X-0.262500X0.450000X-0.262500X0.450000X0.262500X0*%
+G04 APERTURE END LIST*
+D10*
+%TO.C,R1*%
+X150876000Y-117094000D03*
+X150876000Y-118919000D03*
+%TD*%
+M02*
diff --git a/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-B_Silkscreen.gbr b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-B_Silkscreen.gbr
new file mode 100644
index 0000000..803aa18
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-B_Silkscreen.gbr
@@ -0,0 +1,279 @@
+%TF.GenerationSoftware,KiCad,Pcbnew,6.0.7-f9a2dced07~116~ubuntu20.04.1*%
+%TF.CreationDate,2022-09-17T13:59:53-07:00*%
+%TF.ProjectId,CAN_terminator,43414e5f-7465-4726-9d69-6e61746f722e,rev?*%
+%TF.SameCoordinates,Original*%
+%TF.FileFunction,Legend,Bot*%
+%TF.FilePolarity,Positive*%
+%FSLAX46Y46*%
+G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
+G04 Created by KiCad (PCBNEW 6.0.7-f9a2dced07~116~ubuntu20.04.1) date 2022-09-17 13:59:53*
+%MOMM*%
+%LPD*%
+G01*
+G04 APERTURE LIST*
+%ADD10C,0.150000*%
+%ADD11C,0.127000*%
+%ADD12C,0.120000*%
+G04 APERTURE END LIST*
+D10*
+X158583380Y-114198876D02*
+X159583380Y-114532209D01*
+X158583380Y-114865542D01*
+X159583380Y-115722685D02*
+X159583380Y-115151257D01*
+X159583380Y-115436971D02*
+X158583380Y-115436971D01*
+X158726238Y-115341733D01*
+X158821476Y-115246495D01*
+X158869095Y-115151257D01*
+X159488142Y-116151257D02*
+X159535761Y-116198876D01*
+X159583380Y-116151257D01*
+X159535761Y-116103638D01*
+X159488142Y-116151257D01*
+X159583380Y-116151257D01*
+X158583380Y-116817923D02*
+X158583380Y-116913161D01*
+X158631000Y-117008400D01*
+X158678619Y-117056019D01*
+X158773857Y-117103638D01*
+X158964333Y-117151257D01*
+X159202428Y-117151257D01*
+X159392904Y-117103638D01*
+X159488142Y-117056019D01*
+X159535761Y-117008400D01*
+X159583380Y-116913161D01*
+X159583380Y-116817923D01*
+X159535761Y-116722685D01*
+X159488142Y-116675066D01*
+X159392904Y-116627447D01*
+X159202428Y-116579828D01*
+X158964333Y-116579828D01*
+X158773857Y-116627447D01*
+X158678619Y-116675066D01*
+X158631000Y-116722685D01*
+X158583380Y-116817923D01*
+X158678619Y-118294114D02*
+X158631000Y-118341733D01*
+X158583380Y-118436971D01*
+X158583380Y-118675066D01*
+X158631000Y-118770304D01*
+X158678619Y-118817923D01*
+X158773857Y-118865542D01*
+X158869095Y-118865542D01*
+X159011952Y-118817923D01*
+X159583380Y-118246495D01*
+X159583380Y-118865542D01*
+X158583380Y-119484590D02*
+X158583380Y-119579828D01*
+X158631000Y-119675066D01*
+X158678619Y-119722685D01*
+X158773857Y-119770304D01*
+X158964333Y-119817923D01*
+X159202428Y-119817923D01*
+X159392904Y-119770304D01*
+X159488142Y-119722685D01*
+X159535761Y-119675066D01*
+X159583380Y-119579828D01*
+X159583380Y-119484590D01*
+X159535761Y-119389352D01*
+X159488142Y-119341733D01*
+X159392904Y-119294114D01*
+X159202428Y-119246495D01*
+X158964333Y-119246495D01*
+X158773857Y-119294114D01*
+X158678619Y-119341733D01*
+X158631000Y-119389352D01*
+X158583380Y-119484590D01*
+X158678619Y-120198876D02*
+X158631000Y-120246495D01*
+X158583380Y-120341733D01*
+X158583380Y-120579828D01*
+X158631000Y-120675066D01*
+X158678619Y-120722685D01*
+X158773857Y-120770304D01*
+X158869095Y-120770304D01*
+X159011952Y-120722685D01*
+X159583380Y-120151257D01*
+X159583380Y-120770304D01*
+X158678619Y-121151257D02*
+X158631000Y-121198876D01*
+X158583380Y-121294114D01*
+X158583380Y-121532209D01*
+X158631000Y-121627447D01*
+X158678619Y-121675066D01*
+X158773857Y-121722685D01*
+X158869095Y-121722685D01*
+X159011952Y-121675066D01*
+X159583380Y-121103638D01*
+X159583380Y-121722685D01*
+D11*
+X153287185Y-115301485D02*
+X153334357Y-115254314D01*
+X153381528Y-115112800D01*
+X153381528Y-115018457D01*
+X153334357Y-114876942D01*
+X153240014Y-114782600D01*
+X153145671Y-114735428D01*
+X152956985Y-114688257D01*
+X152815471Y-114688257D01*
+X152626785Y-114735428D01*
+X152532442Y-114782600D01*
+X152438100Y-114876942D01*
+X152390928Y-115018457D01*
+X152390928Y-115112800D01*
+X152438100Y-115254314D01*
+X152485271Y-115301485D01*
+X153098500Y-115678857D02*
+X153098500Y-116150571D01*
+X153381528Y-115584514D02*
+X152390928Y-115914714D01*
+X153381528Y-116244914D01*
+X153381528Y-116575114D02*
+X152390928Y-116575114D01*
+X153381528Y-117141171D01*
+X152390928Y-117141171D01*
+X152390928Y-118226114D02*
+X152390928Y-118792171D01*
+X153381528Y-118509142D02*
+X152390928Y-118509142D01*
+X153334357Y-119499742D02*
+X153381528Y-119405400D01*
+X153381528Y-119216714D01*
+X153334357Y-119122371D01*
+X153240014Y-119075200D01*
+X152862642Y-119075200D01*
+X152768300Y-119122371D01*
+X152721128Y-119216714D01*
+X152721128Y-119405400D01*
+X152768300Y-119499742D01*
+X152862642Y-119546914D01*
+X152956985Y-119546914D01*
+X153051328Y-119075200D01*
+X153381528Y-119971457D02*
+X152721128Y-119971457D01*
+X152909814Y-119971457D02*
+X152815471Y-120018628D01*
+X152768300Y-120065800D01*
+X152721128Y-120160142D01*
+X152721128Y-120254485D01*
+X153381528Y-120584685D02*
+X152721128Y-120584685D01*
+X152815471Y-120584685D02*
+X152768300Y-120631857D01*
+X152721128Y-120726200D01*
+X152721128Y-120867714D01*
+X152768300Y-120962057D01*
+X152862642Y-121009228D01*
+X153381528Y-121009228D01*
+X152862642Y-121009228D02*
+X152768300Y-121056400D01*
+X152721128Y-121150742D01*
+X152721128Y-121292257D01*
+X152768300Y-121386600D01*
+X152862642Y-121433771D01*
+X153381528Y-121433771D01*
+X156997400Y-118588366D02*
+X156828066Y-118588366D01*
+X156743400Y-118546033D01*
+X156701066Y-118503700D01*
+X156616400Y-118376700D01*
+X156574066Y-118207366D01*
+X156574066Y-117868700D01*
+X156616400Y-117784033D01*
+X156658733Y-117741700D01*
+X156743400Y-117699366D01*
+X156912733Y-117699366D01*
+X156997400Y-117741700D01*
+X157039733Y-117784033D01*
+X157082066Y-117868700D01*
+X157082066Y-118080366D01*
+X157039733Y-118165033D01*
+X156997400Y-118207366D01*
+X156912733Y-118249700D01*
+X156743400Y-118249700D01*
+X156658733Y-118207366D01*
+X156616400Y-118165033D01*
+X156574066Y-118080366D01*
+X156277733Y-117699366D02*
+X155685066Y-117699366D01*
+X156066066Y-118588366D01*
+X154880733Y-118588366D02*
+X155388733Y-118588366D01*
+X155134733Y-118588366D02*
+X155134733Y-117699366D01*
+X155219400Y-117826366D01*
+X155304066Y-117911033D01*
+X155388733Y-117953366D01*
+D10*
+X150645619Y-115292142D02*
+X150693238Y-115339761D01*
+X150836095Y-115387380D01*
+X150931333Y-115387380D01*
+X151074190Y-115339761D01*
+X151169428Y-115244523D01*
+X151217047Y-115149285D01*
+X151264666Y-114958809D01*
+X151264666Y-114815952D01*
+X151217047Y-114625476D01*
+X151169428Y-114530238D01*
+X151074190Y-114435000D01*
+X150931333Y-114387380D01*
+X150836095Y-114387380D01*
+X150693238Y-114435000D01*
+X150645619Y-114482619D01*
+X150264666Y-115101666D02*
+X149788476Y-115101666D01*
+X150359904Y-115387380D02*
+X150026571Y-114387380D01*
+X149693238Y-115387380D01*
+X149359904Y-115387380D02*
+X149359904Y-114387380D01*
+X148788476Y-115387380D01*
+X148788476Y-114387380D01*
+X148550380Y-115482619D02*
+X147788476Y-115482619D01*
+X147550380Y-115387380D02*
+X147550380Y-114387380D01*
+X147550380Y-114863571D02*
+X146978952Y-114863571D01*
+X146978952Y-115387380D02*
+X146978952Y-114387380D01*
+X150526571Y-121642142D02*
+X150574190Y-121689761D01*
+X150717047Y-121737380D01*
+X150812285Y-121737380D01*
+X150955142Y-121689761D01*
+X151050380Y-121594523D01*
+X151098000Y-121499285D01*
+X151145619Y-121308809D01*
+X151145619Y-121165952D01*
+X151098000Y-120975476D01*
+X151050380Y-120880238D01*
+X150955142Y-120785000D01*
+X150812285Y-120737380D01*
+X150717047Y-120737380D01*
+X150574190Y-120785000D01*
+X150526571Y-120832619D01*
+X150145619Y-121451666D02*
+X149669428Y-121451666D01*
+X150240857Y-121737380D02*
+X149907523Y-120737380D01*
+X149574190Y-121737380D01*
+X149240857Y-121737380D02*
+X149240857Y-120737380D01*
+X148669428Y-121737380D01*
+X148669428Y-120737380D01*
+X148431333Y-121832619D02*
+X147669428Y-121832619D01*
+X146955142Y-121737380D02*
+X147431333Y-121737380D01*
+X147431333Y-120737380D01*
+D12*
+%TO.C,R1*%
+X150141000Y-117779436D02*
+X150141000Y-118233564D01*
+X151611000Y-117779436D02*
+X151611000Y-118233564D01*
+%TD*%
+M02*
diff --git a/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-Edge_Cuts.gbr b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-Edge_Cuts.gbr
new file mode 100644
index 0000000..21a8b1d
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-Edge_Cuts.gbr
@@ -0,0 +1,46 @@
+%TF.GenerationSoftware,KiCad,Pcbnew,6.0.7-f9a2dced07~116~ubuntu20.04.1*%
+%TF.CreationDate,2022-09-17T13:59:53-07:00*%
+%TF.ProjectId,CAN_terminator,43414e5f-7465-4726-9d69-6e61746f722e,rev?*%
+%TF.SameCoordinates,Original*%
+%TF.FileFunction,Profile,NP*%
+%FSLAX46Y46*%
+G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
+G04 Created by KiCad (PCBNEW 6.0.7-f9a2dced07~116~ubuntu20.04.1) date 2022-09-17 13:59:53*
+%MOMM*%
+%LPD*%
+G01*
+G04 APERTURE LIST*
+%TA.AperFunction,Profile*%
+%ADD10C,0.100000*%
+%TD*%
+G04 APERTURE END LIST*
+D10*
+X159893000Y-123825000D02*
+G75*
+G03*
+X160528000Y-123190000I0J635000D01*
+G01*
+X146050000Y-112395000D02*
+G75*
+G03*
+X145415000Y-113030000I0J-635000D01*
+G01*
+X145415000Y-113030000D02*
+X145415000Y-123190000D01*
+X160528000Y-113030000D02*
+X160528000Y-123190000D01*
+X146050000Y-123825000D02*
+X159893000Y-123825000D01*
+X145415000Y-123190000D02*
+G75*
+G03*
+X146050000Y-123825000I635000J0D01*
+G01*
+X146050000Y-112395000D02*
+X159893000Y-112395000D01*
+X160528000Y-113030000D02*
+G75*
+G03*
+X159893000Y-112395000I-635000J0D01*
+G01*
+M02*
diff --git a/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-F_Cu.gbr b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-F_Cu.gbr
new file mode 100644
index 0000000..8b1efbe
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-F_Cu.gbr
@@ -0,0 +1,39 @@
+%TF.GenerationSoftware,KiCad,Pcbnew,6.0.7-f9a2dced07~116~ubuntu20.04.1*%
+%TF.CreationDate,2022-09-17T13:59:53-07:00*%
+%TF.ProjectId,CAN_terminator,43414e5f-7465-4726-9d69-6e61746f722e,rev?*%
+%TF.SameCoordinates,Original*%
+%TF.FileFunction,Copper,L1,Top*%
+%TF.FilePolarity,Positive*%
+%FSLAX46Y46*%
+G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
+G04 Created by KiCad (PCBNEW 6.0.7-f9a2dced07~116~ubuntu20.04.1) date 2022-09-17 13:59:53*
+%MOMM*%
+%LPD*%
+G01*
+G04 APERTURE LIST*
+%TA.AperFunction,ComponentPad*%
+%ADD10R,1.524000X1.524000*%
+%TD*%
+%TA.AperFunction,ComponentPad*%
+%ADD11C,1.524000*%
+%TD*%
+%TA.AperFunction,ComponentPad*%
+%ADD12C,3.403600*%
+%TD*%
+G04 APERTURE END LIST*
+D10*
+%TO.P,J1,1,Pin_1*%
+%TO.N,/CAN_H*%
+X147320000Y-116840000D03*
+D11*
+%TO.P,J1,2,Pin_2*%
+%TO.N,/CAN_L*%
+X147320000Y-119380000D03*
+D12*
+%TO.P,J1,3*%
+%TO.N,N/C*%
+X155960001Y-120780000D03*
+%TO.P,J1,4*%
+X155960001Y-115440000D03*
+%TD*%
+M02*
diff --git a/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-F_Mask.gbr b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-F_Mask.gbr
new file mode 100644
index 0000000..21565ee
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-F_Mask.gbr
@@ -0,0 +1,27 @@
+%TF.GenerationSoftware,KiCad,Pcbnew,6.0.7-f9a2dced07~116~ubuntu20.04.1*%
+%TF.CreationDate,2022-09-17T13:59:53-07:00*%
+%TF.ProjectId,CAN_terminator,43414e5f-7465-4726-9d69-6e61746f722e,rev?*%
+%TF.SameCoordinates,Original*%
+%TF.FileFunction,Soldermask,Top*%
+%TF.FilePolarity,Negative*%
+%FSLAX46Y46*%
+G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
+G04 Created by KiCad (PCBNEW 6.0.7-f9a2dced07~116~ubuntu20.04.1) date 2022-09-17 13:59:53*
+%MOMM*%
+%LPD*%
+G01*
+G04 APERTURE LIST*
+%ADD10R,1.524000X1.524000*%
+%ADD11C,1.524000*%
+%ADD12C,3.403600*%
+G04 APERTURE END LIST*
+D10*
+%TO.C,J1*%
+X147320000Y-116840000D03*
+D11*
+X147320000Y-119380000D03*
+D12*
+X155960001Y-120780000D03*
+X155960001Y-115440000D03*
+%TD*%
+M02*
diff --git a/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-F_Paste.gbr b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-F_Paste.gbr
new file mode 100644
index 0000000..d2674b0
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-F_Paste.gbr
@@ -0,0 +1,15 @@
+%TF.GenerationSoftware,KiCad,Pcbnew,6.0.7-f9a2dced07~116~ubuntu20.04.1*%
+%TF.CreationDate,2022-09-17T13:59:53-07:00*%
+%TF.ProjectId,CAN_terminator,43414e5f-7465-4726-9d69-6e61746f722e,rev?*%
+%TF.SameCoordinates,Original*%
+%TF.FileFunction,Paste,Top*%
+%TF.FilePolarity,Positive*%
+%FSLAX46Y46*%
+G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
+G04 Created by KiCad (PCBNEW 6.0.7-f9a2dced07~116~ubuntu20.04.1) date 2022-09-17 13:59:53*
+%MOMM*%
+%LPD*%
+G01*
+G04 APERTURE LIST*
+G04 APERTURE END LIST*
+M02*
diff --git a/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-F_Silkscreen.gbr b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-F_Silkscreen.gbr
new file mode 100644
index 0000000..2fbc550
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-F_Silkscreen.gbr
@@ -0,0 +1,46 @@
+%TF.GenerationSoftware,KiCad,Pcbnew,6.0.7-f9a2dced07~116~ubuntu20.04.1*%
+%TF.CreationDate,2022-09-17T13:59:53-07:00*%
+%TF.ProjectId,CAN_terminator,43414e5f-7465-4726-9d69-6e61746f722e,rev?*%
+%TF.SameCoordinates,Original*%
+%TF.FileFunction,Legend,Top*%
+%TF.FilePolarity,Positive*%
+%FSLAX46Y46*%
+G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
+G04 Created by KiCad (PCBNEW 6.0.7-f9a2dced07~116~ubuntu20.04.1) date 2022-09-17 13:59:53*
+%MOMM*%
+%LPD*%
+G01*
+G04 APERTURE LIST*
+%ADD10C,0.150000*%
+%ADD11C,0.120000*%
+G04 APERTURE END LIST*
+D10*
+G36*
+X146177000Y-116840000D02*
+G01*
+X145542000Y-117475000D01*
+X145542000Y-116205000D01*
+X146177000Y-116840000D01*
+G37*
+X146177000Y-116840000D02*
+X145542000Y-117475000D01*
+X145542000Y-116205000D01*
+X146177000Y-116840000D01*
+D11*
+%TO.C,J1*%
+X156931330Y-122567700D02*
+X160386001Y-122567700D01*
+X154988671Y-113652300D02*
+X146543000Y-113652300D01*
+X146543000Y-120151185D02*
+X146543000Y-122567700D01*
+X146543000Y-122567700D02*
+X154988671Y-122567700D01*
+X160386001Y-113652300D02*
+X156931330Y-113652300D01*
+X146543000Y-113652300D02*
+X146543000Y-115570000D01*
+X160386001Y-122567700D02*
+X160386001Y-113652300D01*
+%TD*%
+M02*
diff --git a/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-NPTH-drl_map.gbr b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-NPTH-drl_map.gbr
new file mode 100644
index 0000000..839ba5c
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-NPTH-drl_map.gbr
@@ -0,0 +1,130 @@
+%TF.GenerationSoftware,KiCad,Pcbnew,6.0.7-f9a2dced07~116~ubuntu20.04.1*%
+%TF.CreationDate,2022-09-17T14:00:31-07:00*%
+%TF.ProjectId,CAN_terminator,43414e5f-7465-4726-9d69-6e61746f722e,rev?*%
+%TF.SameCoordinates,Original*%
+%TF.FileFunction,Drillmap*%
+%TF.FilePolarity,Positive*%
+%FSLAX45Y45*%
+G04 Gerber Fmt 4.5, Leading zero omitted, Abs format (unit mm)*
+G04 Created by KiCad (PCBNEW 6.0.7-f9a2dced07~116~ubuntu20.04.1) date 2022-09-17 14:00:31*
+%MOMM*%
+%LPD*%
+G01*
+G04 APERTURE LIST*
+%ADD10C,0.100000*%
+%ADD11C,0.200000*%
+G04 APERTURE END LIST*
+D10*
+X15989300Y-12382500D02*
+G75*
+G03*
+X16052800Y-12319000I0J63500D01*
+G01*
+X14605000Y-11239500D02*
+G75*
+G03*
+X14541500Y-11303000I0J-63500D01*
+G01*
+X14541500Y-11303000D02*
+X14541500Y-12319000D01*
+X16052800Y-11303000D02*
+X16052800Y-12319000D01*
+X14605000Y-12382500D02*
+X15989300Y-12382500D01*
+X14541500Y-12319000D02*
+G75*
+G03*
+X14605000Y-12382500I63500J0D01*
+G01*
+X14605000Y-11239500D02*
+X15989300Y-11239500D01*
+X16052800Y-11303000D02*
+G75*
+G03*
+X15989300Y-11239500I-63500J0D01*
+G01*
+D11*
+X14794119Y-12697976D02*
+X14794119Y-12497976D01*
+X14841738Y-12497976D01*
+X14870309Y-12507500D01*
+X14889357Y-12526548D01*
+X14898881Y-12545595D01*
+X14908405Y-12583690D01*
+X14908405Y-12612262D01*
+X14898881Y-12650357D01*
+X14889357Y-12669405D01*
+X14870309Y-12688452D01*
+X14841738Y-12697976D01*
+X14794119Y-12697976D01*
+X14994119Y-12697976D02*
+X14994119Y-12564643D01*
+X14994119Y-12602738D02*
+X15003643Y-12583690D01*
+X15013167Y-12574167D01*
+X15032214Y-12564643D01*
+X15051262Y-12564643D01*
+X15117928Y-12697976D02*
+X15117928Y-12564643D01*
+X15117928Y-12497976D02*
+X15108405Y-12507500D01*
+X15117928Y-12517024D01*
+X15127452Y-12507500D01*
+X15117928Y-12497976D01*
+X15117928Y-12517024D01*
+X15241738Y-12697976D02*
+X15222690Y-12688452D01*
+X15213167Y-12669405D01*
+X15213167Y-12497976D01*
+X15346500Y-12697976D02*
+X15327452Y-12688452D01*
+X15317928Y-12669405D01*
+X15317928Y-12497976D01*
+X15575071Y-12697976D02*
+X15575071Y-12497976D01*
+X15641738Y-12640833D01*
+X15708405Y-12497976D01*
+X15708405Y-12697976D01*
+X15889357Y-12697976D02*
+X15889357Y-12593214D01*
+X15879833Y-12574167D01*
+X15860786Y-12564643D01*
+X15822690Y-12564643D01*
+X15803643Y-12574167D01*
+X15889357Y-12688452D02*
+X15870309Y-12697976D01*
+X15822690Y-12697976D01*
+X15803643Y-12688452D01*
+X15794119Y-12669405D01*
+X15794119Y-12650357D01*
+X15803643Y-12631309D01*
+X15822690Y-12621786D01*
+X15870309Y-12621786D01*
+X15889357Y-12612262D01*
+X15984595Y-12564643D02*
+X15984595Y-12764643D01*
+X15984595Y-12574167D02*
+X16003643Y-12564643D01*
+X16041738Y-12564643D01*
+X16060786Y-12574167D01*
+X16070309Y-12583690D01*
+X16079833Y-12602738D01*
+X16079833Y-12659881D01*
+X16070309Y-12678928D01*
+X16060786Y-12688452D01*
+X16041738Y-12697976D01*
+X16003643Y-12697976D01*
+X15984595Y-12688452D01*
+X16165548Y-12678928D02*
+X16175071Y-12688452D01*
+X16165548Y-12697976D01*
+X16156024Y-12688452D01*
+X16165548Y-12678928D01*
+X16165548Y-12697976D01*
+X16165548Y-12574167D02*
+X16175071Y-12583690D01*
+X16165548Y-12593214D01*
+X16156024Y-12583690D01*
+X16165548Y-12574167D01*
+X16165548Y-12593214D01*
+M02*
diff --git a/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-NPTH-drl_map.pdf b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-NPTH-drl_map.pdf
new file mode 100644
index 0000000..7b6eddc
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-NPTH-drl_map.pdf
Binary files differ
diff --git a/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-NPTH.drl b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-NPTH.drl
new file mode 100644
index 0000000..f9b3ac6
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-NPTH.drl
@@ -0,0 +1,13 @@
+M48
+; DRILL file {KiCad 6.0.7-f9a2dced07~116~ubuntu20.04.1} date Sat 17 Sep 2022 02:00:29 PM PDT
+; FORMAT={-:-/ absolute / inch / decimal}
+; #@! TF.CreationDate,2022-09-17T14:00:29-07:00
+; #@! TF.GenerationSoftware,Kicad,Pcbnew,6.0.7-f9a2dced07~116~ubuntu20.04.1
+; #@! TF.FileFunction,NonPlated,1,2,NPTH
+FMAT,2
+INCH
+%
+G90
+G05
+T0
+M30
diff --git a/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-PTH-drl_map.gbr b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-PTH-drl_map.gbr
new file mode 100644
index 0000000..d3847cb
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-PTH-drl_map.gbr
@@ -0,0 +1,755 @@
+%TF.GenerationSoftware,KiCad,Pcbnew,6.0.7-f9a2dced07~116~ubuntu20.04.1*%
+%TF.CreationDate,2022-09-17T14:00:31-07:00*%
+%TF.ProjectId,CAN_terminator,43414e5f-7465-4726-9d69-6e61746f722e,rev?*%
+%TF.SameCoordinates,Original*%
+%TF.FileFunction,Drillmap*%
+%TF.FilePolarity,Positive*%
+%FSLAX45Y45*%
+G04 Gerber Fmt 4.5, Leading zero omitted, Abs format (unit mm)*
+G04 Created by KiCad (PCBNEW 6.0.7-f9a2dced07~116~ubuntu20.04.1) date 2022-09-17 14:00:31*
+%MOMM*%
+%LPD*%
+G01*
+G04 APERTURE LIST*
+%ADD10C,0.100000*%
+%ADD11C,0.200000*%
+%ADD12C,0.096520*%
+%ADD13C,0.340360*%
+G04 APERTURE END LIST*
+D10*
+X15989300Y-12382500D02*
+G75*
+G03*
+X16052800Y-12319000I0J63500D01*
+G01*
+X14605000Y-11239500D02*
+G75*
+G03*
+X14541500Y-11303000I0J-63500D01*
+G01*
+X14541500Y-11303000D02*
+X14541500Y-12319000D01*
+X16052800Y-11303000D02*
+X16052800Y-12319000D01*
+X14605000Y-12382500D02*
+X15989300Y-12382500D01*
+X14541500Y-12319000D02*
+G75*
+G03*
+X14605000Y-12382500I63500J0D01*
+G01*
+X14605000Y-11239500D02*
+X15989300Y-11239500D01*
+X16052800Y-11303000D02*
+G75*
+G03*
+X15989300Y-11239500I-63500J0D01*
+G01*
+D11*
+D12*
+X14683740Y-11635740D02*
+X14780260Y-11732260D01*
+X14780260Y-11635740D02*
+X14683740Y-11732260D01*
+X14683740Y-11889740D02*
+X14780260Y-11986260D01*
+X14780260Y-11889740D02*
+X14683740Y-11986260D01*
+D13*
+X15766180Y-11544000D02*
+G75*
+G03*
+X15766180Y-11544000I-170180J0D01*
+G01*
+X15766180Y-12078000D02*
+G75*
+G03*
+X15766180Y-12078000I-170180J0D01*
+G01*
+D11*
+X14794119Y-12697976D02*
+X14794119Y-12497976D01*
+X14841738Y-12497976D01*
+X14870309Y-12507500D01*
+X14889357Y-12526548D01*
+X14898881Y-12545595D01*
+X14908405Y-12583690D01*
+X14908405Y-12612262D01*
+X14898881Y-12650357D01*
+X14889357Y-12669405D01*
+X14870309Y-12688452D01*
+X14841738Y-12697976D01*
+X14794119Y-12697976D01*
+X14994119Y-12697976D02*
+X14994119Y-12564643D01*
+X14994119Y-12602738D02*
+X15003643Y-12583690D01*
+X15013167Y-12574167D01*
+X15032214Y-12564643D01*
+X15051262Y-12564643D01*
+X15117928Y-12697976D02*
+X15117928Y-12564643D01*
+X15117928Y-12497976D02*
+X15108405Y-12507500D01*
+X15117928Y-12517024D01*
+X15127452Y-12507500D01*
+X15117928Y-12497976D01*
+X15117928Y-12517024D01*
+X15241738Y-12697976D02*
+X15222690Y-12688452D01*
+X15213167Y-12669405D01*
+X15213167Y-12497976D01*
+X15346500Y-12697976D02*
+X15327452Y-12688452D01*
+X15317928Y-12669405D01*
+X15317928Y-12497976D01*
+X15575071Y-12697976D02*
+X15575071Y-12497976D01*
+X15641738Y-12640833D01*
+X15708405Y-12497976D01*
+X15708405Y-12697976D01*
+X15889357Y-12697976D02*
+X15889357Y-12593214D01*
+X15879833Y-12574167D01*
+X15860786Y-12564643D01*
+X15822690Y-12564643D01*
+X15803643Y-12574167D01*
+X15889357Y-12688452D02*
+X15870309Y-12697976D01*
+X15822690Y-12697976D01*
+X15803643Y-12688452D01*
+X15794119Y-12669405D01*
+X15794119Y-12650357D01*
+X15803643Y-12631309D01*
+X15822690Y-12621786D01*
+X15870309Y-12621786D01*
+X15889357Y-12612262D01*
+X15984595Y-12564643D02*
+X15984595Y-12764643D01*
+X15984595Y-12574167D02*
+X16003643Y-12564643D01*
+X16041738Y-12564643D01*
+X16060786Y-12574167D01*
+X16070309Y-12583690D01*
+X16079833Y-12602738D01*
+X16079833Y-12659881D01*
+X16070309Y-12678928D01*
+X16060786Y-12688452D01*
+X16041738Y-12697976D01*
+X16003643Y-12697976D01*
+X15984595Y-12688452D01*
+X16165548Y-12678928D02*
+X16175071Y-12688452D01*
+X16165548Y-12697976D01*
+X16156024Y-12688452D01*
+X16165548Y-12678928D01*
+X16165548Y-12697976D01*
+X16165548Y-12574167D02*
+X16175071Y-12583690D01*
+X16165548Y-12593214D01*
+X16156024Y-12583690D01*
+X16165548Y-12574167D01*
+X16165548Y-12593214D01*
+D12*
+X14439980Y-12979240D02*
+X14536500Y-13075760D01*
+X14536500Y-12979240D02*
+X14439980Y-13075760D01*
+D11*
+X14832214Y-12917976D02*
+X14851262Y-12917976D01*
+X14870309Y-12927500D01*
+X14879833Y-12937024D01*
+X14889357Y-12956071D01*
+X14898881Y-12994167D01*
+X14898881Y-13041786D01*
+X14889357Y-13079881D01*
+X14879833Y-13098928D01*
+X14870309Y-13108452D01*
+X14851262Y-13117976D01*
+X14832214Y-13117976D01*
+X14813167Y-13108452D01*
+X14803643Y-13098928D01*
+X14794119Y-13079881D01*
+X14784595Y-13041786D01*
+X14784595Y-12994167D01*
+X14794119Y-12956071D01*
+X14803643Y-12937024D01*
+X14813167Y-12927500D01*
+X14832214Y-12917976D01*
+X14984595Y-13098928D02*
+X14994119Y-13108452D01*
+X14984595Y-13117976D01*
+X14975071Y-13108452D01*
+X14984595Y-13098928D01*
+X14984595Y-13117976D01*
+X15089357Y-13117976D02*
+X15127452Y-13117976D01*
+X15146500Y-13108452D01*
+X15156024Y-13098928D01*
+X15175071Y-13070357D01*
+X15184595Y-13032262D01*
+X15184595Y-12956071D01*
+X15175071Y-12937024D01*
+X15165548Y-12927500D01*
+X15146500Y-12917976D01*
+X15108405Y-12917976D01*
+X15089357Y-12927500D01*
+X15079833Y-12937024D01*
+X15070309Y-12956071D01*
+X15070309Y-13003690D01*
+X15079833Y-13022738D01*
+X15089357Y-13032262D01*
+X15108405Y-13041786D01*
+X15146500Y-13041786D01*
+X15165548Y-13032262D01*
+X15175071Y-13022738D01*
+X15184595Y-13003690D01*
+X15356024Y-12917976D02*
+X15317928Y-12917976D01*
+X15298881Y-12927500D01*
+X15289357Y-12937024D01*
+X15270309Y-12965595D01*
+X15260786Y-13003690D01*
+X15260786Y-13079881D01*
+X15270309Y-13098928D01*
+X15279833Y-13108452D01*
+X15298881Y-13117976D01*
+X15336976Y-13117976D01*
+X15356024Y-13108452D01*
+X15365548Y-13098928D01*
+X15375071Y-13079881D01*
+X15375071Y-13032262D01*
+X15365548Y-13013214D01*
+X15356024Y-13003690D01*
+X15336976Y-12994167D01*
+X15298881Y-12994167D01*
+X15279833Y-13003690D01*
+X15270309Y-13013214D01*
+X15260786Y-13032262D01*
+X15556024Y-12917976D02*
+X15460786Y-12917976D01*
+X15451262Y-13013214D01*
+X15460786Y-13003690D01*
+X15479833Y-12994167D01*
+X15527452Y-12994167D01*
+X15546500Y-13003690D01*
+X15556024Y-13013214D01*
+X15565548Y-13032262D01*
+X15565548Y-13079881D01*
+X15556024Y-13098928D01*
+X15546500Y-13108452D01*
+X15527452Y-13117976D01*
+X15479833Y-13117976D01*
+X15460786Y-13108452D01*
+X15451262Y-13098928D01*
+X15651262Y-13117976D02*
+X15651262Y-12984643D01*
+X15651262Y-13003690D02*
+X15660786Y-12994167D01*
+X15679833Y-12984643D01*
+X15708405Y-12984643D01*
+X15727452Y-12994167D01*
+X15736976Y-13013214D01*
+X15736976Y-13117976D01*
+X15736976Y-13013214D02*
+X15746500Y-12994167D01*
+X15765548Y-12984643D01*
+X15794119Y-12984643D01*
+X15813167Y-12994167D01*
+X15822690Y-13013214D01*
+X15822690Y-13117976D01*
+X15917928Y-13117976D02*
+X15917928Y-12984643D01*
+X15917928Y-13003690D02*
+X15927452Y-12994167D01*
+X15946500Y-12984643D01*
+X15975071Y-12984643D01*
+X15994119Y-12994167D01*
+X16003643Y-13013214D01*
+X16003643Y-13117976D01*
+X16003643Y-13013214D02*
+X16013167Y-12994167D01*
+X16032214Y-12984643D01*
+X16060786Y-12984643D01*
+X16079833Y-12994167D01*
+X16089357Y-13013214D01*
+X16089357Y-13117976D01*
+X16479833Y-12908452D02*
+X16308405Y-13165595D01*
+X16736976Y-12917976D02*
+X16756024Y-12917976D01*
+X16775071Y-12927500D01*
+X16784595Y-12937024D01*
+X16794119Y-12956071D01*
+X16803643Y-12994167D01*
+X16803643Y-13041786D01*
+X16794119Y-13079881D01*
+X16784595Y-13098928D01*
+X16775071Y-13108452D01*
+X16756024Y-13117976D01*
+X16736976Y-13117976D01*
+X16717928Y-13108452D01*
+X16708405Y-13098928D01*
+X16698881Y-13079881D01*
+X16689357Y-13041786D01*
+X16689357Y-12994167D01*
+X16698881Y-12956071D01*
+X16708405Y-12937024D01*
+X16717928Y-12927500D01*
+X16736976Y-12917976D01*
+X16889357Y-13098928D02*
+X16898881Y-13108452D01*
+X16889357Y-13117976D01*
+X16879833Y-13108452D01*
+X16889357Y-13098928D01*
+X16889357Y-13117976D01*
+X17022690Y-12917976D02*
+X17041738Y-12917976D01*
+X17060786Y-12927500D01*
+X17070310Y-12937024D01*
+X17079833Y-12956071D01*
+X17089357Y-12994167D01*
+X17089357Y-13041786D01*
+X17079833Y-13079881D01*
+X17070310Y-13098928D01*
+X17060786Y-13108452D01*
+X17041738Y-13117976D01*
+X17022690Y-13117976D01*
+X17003643Y-13108452D01*
+X16994119Y-13098928D01*
+X16984595Y-13079881D01*
+X16975071Y-13041786D01*
+X16975071Y-12994167D01*
+X16984595Y-12956071D01*
+X16994119Y-12937024D01*
+X17003643Y-12927500D01*
+X17022690Y-12917976D01*
+X17156024Y-12917976D02*
+X17279833Y-12917976D01*
+X17213167Y-12994167D01*
+X17241738Y-12994167D01*
+X17260786Y-13003690D01*
+X17270310Y-13013214D01*
+X17279833Y-13032262D01*
+X17279833Y-13079881D01*
+X17270310Y-13098928D01*
+X17260786Y-13108452D01*
+X17241738Y-13117976D01*
+X17184595Y-13117976D01*
+X17165548Y-13108452D01*
+X17156024Y-13098928D01*
+X17394119Y-13003690D02*
+X17375071Y-12994167D01*
+X17365548Y-12984643D01*
+X17356024Y-12965595D01*
+X17356024Y-12956071D01*
+X17365548Y-12937024D01*
+X17375071Y-12927500D01*
+X17394119Y-12917976D01*
+X17432214Y-12917976D01*
+X17451262Y-12927500D01*
+X17460786Y-12937024D01*
+X17470310Y-12956071D01*
+X17470310Y-12965595D01*
+X17460786Y-12984643D01*
+X17451262Y-12994167D01*
+X17432214Y-13003690D01*
+X17394119Y-13003690D01*
+X17375071Y-13013214D01*
+X17365548Y-13022738D01*
+X17356024Y-13041786D01*
+X17356024Y-13079881D01*
+X17365548Y-13098928D01*
+X17375071Y-13108452D01*
+X17394119Y-13117976D01*
+X17432214Y-13117976D01*
+X17451262Y-13108452D01*
+X17460786Y-13098928D01*
+X17470310Y-13079881D01*
+X17470310Y-13041786D01*
+X17460786Y-13022738D01*
+X17451262Y-13013214D01*
+X17432214Y-13003690D01*
+X17594119Y-12917976D02*
+X17613167Y-12917976D01*
+X17632214Y-12927500D01*
+X17641738Y-12937024D01*
+X17651262Y-12956071D01*
+X17660786Y-12994167D01*
+X17660786Y-13041786D01*
+X17651262Y-13079881D01*
+X17641738Y-13098928D01*
+X17632214Y-13108452D01*
+X17613167Y-13117976D01*
+X17594119Y-13117976D01*
+X17575071Y-13108452D01*
+X17565548Y-13098928D01*
+X17556024Y-13079881D01*
+X17546500Y-13041786D01*
+X17546500Y-12994167D01*
+X17556024Y-12956071D01*
+X17565548Y-12937024D01*
+X17575071Y-12927500D01*
+X17594119Y-12917976D01*
+X17736976Y-12917976D02*
+X17736976Y-12956071D01*
+X17813167Y-12917976D02*
+X17813167Y-12956071D01*
+X18108405Y-13194167D02*
+X18098881Y-13184643D01*
+X18079833Y-13156071D01*
+X18070310Y-13137024D01*
+X18060786Y-13108452D01*
+X18051262Y-13060833D01*
+X18051262Y-13022738D01*
+X18060786Y-12975119D01*
+X18070310Y-12946548D01*
+X18079833Y-12927500D01*
+X18098881Y-12898928D01*
+X18108405Y-12889405D01*
+X18175071Y-12937024D02*
+X18184595Y-12927500D01*
+X18203643Y-12917976D01*
+X18251262Y-12917976D01*
+X18270310Y-12927500D01*
+X18279833Y-12937024D01*
+X18289357Y-12956071D01*
+X18289357Y-12975119D01*
+X18279833Y-13003690D01*
+X18165548Y-13117976D01*
+X18289357Y-13117976D01*
+X18527452Y-13117976D02*
+X18527452Y-12917976D01*
+X18613167Y-13117976D02*
+X18613167Y-13013214D01*
+X18603643Y-12994167D01*
+X18584595Y-12984643D01*
+X18556024Y-12984643D01*
+X18536976Y-12994167D01*
+X18527452Y-13003690D01*
+X18736976Y-13117976D02*
+X18717929Y-13108452D01*
+X18708405Y-13098928D01*
+X18698881Y-13079881D01*
+X18698881Y-13022738D01*
+X18708405Y-13003690D01*
+X18717929Y-12994167D01*
+X18736976Y-12984643D01*
+X18765548Y-12984643D01*
+X18784595Y-12994167D01*
+X18794119Y-13003690D01*
+X18803643Y-13022738D01*
+X18803643Y-13079881D01*
+X18794119Y-13098928D01*
+X18784595Y-13108452D01*
+X18765548Y-13117976D01*
+X18736976Y-13117976D01*
+X18917929Y-13117976D02*
+X18898881Y-13108452D01*
+X18889357Y-13089405D01*
+X18889357Y-12917976D01*
+X19070310Y-13108452D02*
+X19051262Y-13117976D01*
+X19013167Y-13117976D01*
+X18994119Y-13108452D01*
+X18984595Y-13089405D01*
+X18984595Y-13013214D01*
+X18994119Y-12994167D01*
+X19013167Y-12984643D01*
+X19051262Y-12984643D01*
+X19070310Y-12994167D01*
+X19079833Y-13013214D01*
+X19079833Y-13032262D01*
+X18984595Y-13051309D01*
+X19156024Y-13108452D02*
+X19175071Y-13117976D01*
+X19213167Y-13117976D01*
+X19232214Y-13108452D01*
+X19241738Y-13089405D01*
+X19241738Y-13079881D01*
+X19232214Y-13060833D01*
+X19213167Y-13051309D01*
+X19184595Y-13051309D01*
+X19165548Y-13041786D01*
+X19156024Y-13022738D01*
+X19156024Y-13013214D01*
+X19165548Y-12994167D01*
+X19184595Y-12984643D01*
+X19213167Y-12984643D01*
+X19232214Y-12994167D01*
+X19308405Y-13194167D02*
+X19317929Y-13184643D01*
+X19336976Y-13156071D01*
+X19346500Y-13137024D01*
+X19356024Y-13108452D01*
+X19365548Y-13060833D01*
+X19365548Y-13022738D01*
+X19356024Y-12975119D01*
+X19346500Y-12946548D01*
+X19336976Y-12927500D01*
+X19317929Y-12898928D01*
+X19308405Y-12889405D01*
+X14536500Y-13291500D02*
+G75*
+G03*
+X14536500Y-13291500I-100000J0D01*
+G01*
+X14775071Y-13181976D02*
+X14898881Y-13181976D01*
+X14832214Y-13258167D01*
+X14860786Y-13258167D01*
+X14879833Y-13267690D01*
+X14889357Y-13277214D01*
+X14898881Y-13296262D01*
+X14898881Y-13343881D01*
+X14889357Y-13362928D01*
+X14879833Y-13372452D01*
+X14860786Y-13381976D01*
+X14803643Y-13381976D01*
+X14784595Y-13372452D01*
+X14775071Y-13362928D01*
+X14984595Y-13362928D02*
+X14994119Y-13372452D01*
+X14984595Y-13381976D01*
+X14975071Y-13372452D01*
+X14984595Y-13362928D01*
+X14984595Y-13381976D01*
+X15165548Y-13248643D02*
+X15165548Y-13381976D01*
+X15117928Y-13172452D02*
+X15070309Y-13315309D01*
+X15194119Y-13315309D01*
+X15308405Y-13181976D02*
+X15327452Y-13181976D01*
+X15346500Y-13191500D01*
+X15356024Y-13201024D01*
+X15365548Y-13220071D01*
+X15375071Y-13258167D01*
+X15375071Y-13305786D01*
+X15365548Y-13343881D01*
+X15356024Y-13362928D01*
+X15346500Y-13372452D01*
+X15327452Y-13381976D01*
+X15308405Y-13381976D01*
+X15289357Y-13372452D01*
+X15279833Y-13362928D01*
+X15270309Y-13343881D01*
+X15260786Y-13305786D01*
+X15260786Y-13258167D01*
+X15270309Y-13220071D01*
+X15279833Y-13201024D01*
+X15289357Y-13191500D01*
+X15308405Y-13181976D01*
+X15546500Y-13248643D02*
+X15546500Y-13381976D01*
+X15498881Y-13172452D02*
+X15451262Y-13315309D01*
+X15575071Y-13315309D01*
+X15651262Y-13381976D02*
+X15651262Y-13248643D01*
+X15651262Y-13267690D02*
+X15660786Y-13258167D01*
+X15679833Y-13248643D01*
+X15708405Y-13248643D01*
+X15727452Y-13258167D01*
+X15736976Y-13277214D01*
+X15736976Y-13381976D01*
+X15736976Y-13277214D02*
+X15746500Y-13258167D01*
+X15765548Y-13248643D01*
+X15794119Y-13248643D01*
+X15813167Y-13258167D01*
+X15822690Y-13277214D01*
+X15822690Y-13381976D01*
+X15917928Y-13381976D02*
+X15917928Y-13248643D01*
+X15917928Y-13267690D02*
+X15927452Y-13258167D01*
+X15946500Y-13248643D01*
+X15975071Y-13248643D01*
+X15994119Y-13258167D01*
+X16003643Y-13277214D01*
+X16003643Y-13381976D01*
+X16003643Y-13277214D02*
+X16013167Y-13258167D01*
+X16032214Y-13248643D01*
+X16060786Y-13248643D01*
+X16079833Y-13258167D01*
+X16089357Y-13277214D01*
+X16089357Y-13381976D01*
+X16479833Y-13172452D02*
+X16308405Y-13429595D01*
+X16736976Y-13181976D02*
+X16756024Y-13181976D01*
+X16775071Y-13191500D01*
+X16784595Y-13201024D01*
+X16794119Y-13220071D01*
+X16803643Y-13258167D01*
+X16803643Y-13305786D01*
+X16794119Y-13343881D01*
+X16784595Y-13362928D01*
+X16775071Y-13372452D01*
+X16756024Y-13381976D01*
+X16736976Y-13381976D01*
+X16717928Y-13372452D01*
+X16708405Y-13362928D01*
+X16698881Y-13343881D01*
+X16689357Y-13305786D01*
+X16689357Y-13258167D01*
+X16698881Y-13220071D01*
+X16708405Y-13201024D01*
+X16717928Y-13191500D01*
+X16736976Y-13181976D01*
+X16889357Y-13362928D02*
+X16898881Y-13372452D01*
+X16889357Y-13381976D01*
+X16879833Y-13372452D01*
+X16889357Y-13362928D01*
+X16889357Y-13381976D01*
+X17089357Y-13381976D02*
+X16975071Y-13381976D01*
+X17032214Y-13381976D02*
+X17032214Y-13181976D01*
+X17013167Y-13210548D01*
+X16994119Y-13229595D01*
+X16975071Y-13239119D01*
+X17156024Y-13181976D02*
+X17279833Y-13181976D01*
+X17213167Y-13258167D01*
+X17241738Y-13258167D01*
+X17260786Y-13267690D01*
+X17270310Y-13277214D01*
+X17279833Y-13296262D01*
+X17279833Y-13343881D01*
+X17270310Y-13362928D01*
+X17260786Y-13372452D01*
+X17241738Y-13381976D01*
+X17184595Y-13381976D01*
+X17165548Y-13372452D01*
+X17156024Y-13362928D01*
+X17451262Y-13248643D02*
+X17451262Y-13381976D01*
+X17403643Y-13172452D02*
+X17356024Y-13315309D01*
+X17479833Y-13315309D01*
+X17594119Y-13181976D02*
+X17613167Y-13181976D01*
+X17632214Y-13191500D01*
+X17641738Y-13201024D01*
+X17651262Y-13220071D01*
+X17660786Y-13258167D01*
+X17660786Y-13305786D01*
+X17651262Y-13343881D01*
+X17641738Y-13362928D01*
+X17632214Y-13372452D01*
+X17613167Y-13381976D01*
+X17594119Y-13381976D01*
+X17575071Y-13372452D01*
+X17565548Y-13362928D01*
+X17556024Y-13343881D01*
+X17546500Y-13305786D01*
+X17546500Y-13258167D01*
+X17556024Y-13220071D01*
+X17565548Y-13201024D01*
+X17575071Y-13191500D01*
+X17594119Y-13181976D01*
+X17736976Y-13181976D02*
+X17736976Y-13220071D01*
+X17813167Y-13181976D02*
+X17813167Y-13220071D01*
+X18108405Y-13458167D02*
+X18098881Y-13448643D01*
+X18079833Y-13420071D01*
+X18070310Y-13401024D01*
+X18060786Y-13372452D01*
+X18051262Y-13324833D01*
+X18051262Y-13286738D01*
+X18060786Y-13239119D01*
+X18070310Y-13210548D01*
+X18079833Y-13191500D01*
+X18098881Y-13162928D01*
+X18108405Y-13153405D01*
+X18175071Y-13201024D02*
+X18184595Y-13191500D01*
+X18203643Y-13181976D01*
+X18251262Y-13181976D01*
+X18270310Y-13191500D01*
+X18279833Y-13201024D01*
+X18289357Y-13220071D01*
+X18289357Y-13239119D01*
+X18279833Y-13267690D01*
+X18165548Y-13381976D01*
+X18289357Y-13381976D01*
+X18527452Y-13381976D02*
+X18527452Y-13181976D01*
+X18613167Y-13381976D02*
+X18613167Y-13277214D01*
+X18603643Y-13258167D01*
+X18584595Y-13248643D01*
+X18556024Y-13248643D01*
+X18536976Y-13258167D01*
+X18527452Y-13267690D01*
+X18736976Y-13381976D02*
+X18717929Y-13372452D01*
+X18708405Y-13362928D01*
+X18698881Y-13343881D01*
+X18698881Y-13286738D01*
+X18708405Y-13267690D01*
+X18717929Y-13258167D01*
+X18736976Y-13248643D01*
+X18765548Y-13248643D01*
+X18784595Y-13258167D01*
+X18794119Y-13267690D01*
+X18803643Y-13286738D01*
+X18803643Y-13343881D01*
+X18794119Y-13362928D01*
+X18784595Y-13372452D01*
+X18765548Y-13381976D01*
+X18736976Y-13381976D01*
+X18917929Y-13381976D02*
+X18898881Y-13372452D01*
+X18889357Y-13353405D01*
+X18889357Y-13181976D01*
+X19070310Y-13372452D02*
+X19051262Y-13381976D01*
+X19013167Y-13381976D01*
+X18994119Y-13372452D01*
+X18984595Y-13353405D01*
+X18984595Y-13277214D01*
+X18994119Y-13258167D01*
+X19013167Y-13248643D01*
+X19051262Y-13248643D01*
+X19070310Y-13258167D01*
+X19079833Y-13277214D01*
+X19079833Y-13296262D01*
+X18984595Y-13315309D01*
+X19156024Y-13372452D02*
+X19175071Y-13381976D01*
+X19213167Y-13381976D01*
+X19232214Y-13372452D01*
+X19241738Y-13353405D01*
+X19241738Y-13343881D01*
+X19232214Y-13324833D01*
+X19213167Y-13315309D01*
+X19184595Y-13315309D01*
+X19165548Y-13305786D01*
+X19156024Y-13286738D01*
+X19156024Y-13277214D01*
+X19165548Y-13258167D01*
+X19184595Y-13248643D01*
+X19213167Y-13248643D01*
+X19232214Y-13258167D01*
+X19308405Y-13458167D02*
+X19317929Y-13448643D01*
+X19336976Y-13420071D01*
+X19346500Y-13401024D01*
+X19356024Y-13372452D01*
+X19365548Y-13324833D01*
+X19365548Y-13286738D01*
+X19356024Y-13239119D01*
+X19346500Y-13210548D01*
+X19336976Y-13191500D01*
+X19317929Y-13162928D01*
+X19308405Y-13153405D01*
+M02*
diff --git a/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-PTH-drl_map.pdf b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-PTH-drl_map.pdf
new file mode 100644
index 0000000..11056fe
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-PTH-drl_map.pdf
Binary files differ
diff --git a/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-PTH.drl b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-PTH.drl
new file mode 100644
index 0000000..2ae858f
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-PTH.drl
@@ -0,0 +1,23 @@
+M48
+; DRILL file {KiCad 6.0.7-f9a2dced07~116~ubuntu20.04.1} date Sat 17 Sep 2022 02:00:29 PM PDT
+; FORMAT={-:-/ absolute / inch / decimal}
+; #@! TF.CreationDate,2022-09-17T14:00:29-07:00
+; #@! TF.GenerationSoftware,Kicad,Pcbnew,6.0.7-f9a2dced07~116~ubuntu20.04.1
+; #@! TF.FileFunction,Plated,1,2,PTH
+FMAT,2
+INCH
+; #@! TA.AperFunction,Plated,PTH,ComponentDrill
+T1C0.0380
+; #@! TA.AperFunction,Plated,PTH,ComponentDrill
+T2C0.1340
+%
+G90
+G05
+T1
+X5.8Y-4.6
+X5.8Y-4.7
+T2
+X6.1402Y-4.5449
+X6.1402Y-4.7551
+T0
+M30
diff --git a/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-job.gbrjob b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-job.gbrjob
new file mode 100644
index 0000000..5fb6bcb
--- /dev/null
+++ b/circuit_boards/CAN_boards/CAN_Terminator/gerbers/CAN_terminator-job.gbrjob
@@ -0,0 +1,120 @@
+{
+  "Header": {
+    "GenerationSoftware": {
+      "Vendor": "KiCad",
+      "Application": "Pcbnew",
+      "Version": "6.0.7-f9a2dced07~116~ubuntu20.04.1"
+    },
+    "CreationDate": "2022-09-17T13:59:53-07:00"
+  },
+  "GeneralSpecs": {
+    "ProjectId": {
+      "Name": "CAN_terminator",
+      "GUID": "43414e5f-7465-4726-9d69-6e61746f722e",
+      "Revision": "rev?"
+    },
+    "Size": {
+      "X": 15.213,
+      "Y": 11.53
+    },
+    "LayerNumber": 2,
+    "BoardThickness": 1.6,
+    "Finish": "None"
+  },
+  "DesignRules": [
+    {
+      "Layers": "Outer",
+      "PadToPad": 0.0,
+      "PadToTrack": 0.0,
+      "TrackToTrack": 0.2,
+      "MinLineWidth": 0.508
+    }
+  ],
+  "FilesAttributes": [
+    {
+      "Path": "CAN_terminator-F_Cu.gbr",
+      "FileFunction": "Copper,L1,Top",
+      "FilePolarity": "Positive"
+    },
+    {
+      "Path": "CAN_terminator-B_Cu.gbr",
+      "FileFunction": "Copper,L2,Bot",
+      "FilePolarity": "Positive"
+    },
+    {
+      "Path": "CAN_terminator-F_Paste.gbr",
+      "FileFunction": "SolderPaste,Top",
+      "FilePolarity": "Positive"
+    },
+    {
+      "Path": "CAN_terminator-B_Paste.gbr",
+      "FileFunction": "SolderPaste,Bot",
+      "FilePolarity": "Positive"
+    },
+    {
+      "Path": "CAN_terminator-F_Silkscreen.gbr",
+      "FileFunction": "Legend,Top",
+      "FilePolarity": "Positive"
+    },
+    {
+      "Path": "CAN_terminator-B_Silkscreen.gbr",
+      "FileFunction": "Legend,Bot",
+      "FilePolarity": "Positive"
+    },
+    {
+      "Path": "CAN_terminator-F_Mask.gbr",
+      "FileFunction": "SolderMask,Top",
+      "FilePolarity": "Negative"
+    },
+    {
+      "Path": "CAN_terminator-B_Mask.gbr",
+      "FileFunction": "SolderMask,Bot",
+      "FilePolarity": "Negative"
+    },
+    {
+      "Path": "CAN_terminator-Edge_Cuts.gbr",
+      "FileFunction": "Profile",
+      "FilePolarity": "Positive"
+    }
+  ],
+  "MaterialStackup": [
+    {
+      "Type": "Legend",
+      "Name": "Top Silk Screen"
+    },
+    {
+      "Type": "SolderPaste",
+      "Name": "Top Solder Paste"
+    },
+    {
+      "Type": "SolderMask",
+      "Name": "Top Solder Mask"
+    },
+    {
+      "Type": "Copper",
+      "Name": "F.Cu"
+    },
+    {
+      "Type": "Dielectric",
+      "Material": "FR4",
+      "Name": "F.Cu/B.Cu",
+      "Notes": "Type: dielectric layer 1 (from F.Cu to B.Cu)"
+    },
+    {
+      "Type": "Copper",
+      "Name": "B.Cu"
+    },
+    {
+      "Type": "SolderMask",
+      "Name": "Bottom Solder Mask"
+    },
+    {
+      "Type": "SolderPaste",
+      "Name": "Bottom Solder Paste"
+    },
+    {
+      "Type": "Legend",
+      "Name": "Bottom Silk Screen"
+    }
+  ]
+}
diff --git a/debian/BUILD.nghttp2.bazel b/debian/BUILD.nghttp2.bazel
new file mode 100644
index 0000000..cd055cd
--- /dev/null
+++ b/debian/BUILD.nghttp2.bazel
@@ -0,0 +1,71 @@
+load("@bazel_skylib//rules:write_file.bzl", "write_file")
+
+licenses(["notice"])  # MIT/X derivative license
+
+exports_files(["COPYING"])
+
+write_file(
+    name = "nghttp2ver.h",
+    out = "lib/includes/nghttp2/nghttp2ver.h",
+    content = """
+/*
+ * nghttp2 - HTTP/2 C Library
+ *
+ * Copyright (c) 2012, 2013 Tatsuhiro Tsujikawa
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+#ifndef NGHTTP2VER_H
+#define NGHTTP2VER_H
+
+/**
+ * @macro
+ * Version number of the nghttp2 library release
+ */
+#define NGHTTP2_VERSION "1.58.0"
+
+/**
+ * @macro
+ * Numerical representation of the version number of the nghttp2 library
+ * release. This is a 24 bit number with 8 bits for major number, 8 bits
+ * for minor and 8 bits for patch. Version 1.2.3 becomes 0x010203.
+ */
+#define NGHTTP2_VERSION_NUM 0x013a00
+
+#endif /* NGHTTP2VER_H */
+""".splitlines(),
+)
+
+cc_library(
+    name = "nghttp2",
+    srcs = glob(["lib/*.c"]),
+    hdrs = glob(["lib/*.h"]) + [
+        "lib/includes/nghttp2/nghttp2.h",
+        ":nghttp2ver.h",
+    ],
+    defines = [
+        "HAVE_TIME_H",
+        "HAVE_ARPA_INET_H",
+    ],
+    includes = [
+        "lib/includes/",
+    ],
+    visibility = ["//visibility:public"],
+)
diff --git a/debian/curl.BUILD b/debian/curl.BUILD
index a4f6526..1292c5e 100644
--- a/debian/curl.BUILD
+++ b/debian/curl.BUILD
@@ -422,6 +422,7 @@
             "-DCURL_DISABLE_NTLM",  # turning it off in configure is not enough
             "-DHAVE_LIBZ",
             "-DHAVE_ZLIB_H",
+            "-DUSE_NGHTTP2",
             "-Wno-string-plus-int",
             "-Wno-cast-qual",
             "-Wno-format-nonliteral",
@@ -466,6 +467,7 @@
         # Use the same version of zlib that gRPC does.
         "@zlib",
         ":define-ca-bundle-location",
+        "@com_github_nghttp2_nghttp2//:nghttp2",
     ] + select({
         ":windows": [],
         "//conditions:default": [
diff --git a/documentation/aos/docs/flatbuffers.md b/documentation/aos/docs/flatbuffers.md
index 132348d..9c82ee3 100644
--- a/documentation/aos/docs/flatbuffers.md
+++ b/documentation/aos/docs/flatbuffers.md
@@ -157,13 +157,30 @@
 type using the regular generated flatbuffer API and a `FromFlatbuffer()` method
 which attempts to copy the specified flatbuffer into the current object.
 
+The `FromFlatbuffer()` method works on both the "raw" flatbuffer type, as well
+as on the [Flatbuffer Object
+API](https://flatbuffers.dev/flatbuffers_guide_use_cpp.html) (i.e. the
+`FlatbufferT` types). When copying
+flatbuffers from the object-based API, we apply the same semantics that that the
+`Pack()` method does in the raw flatbuffer type. Namely, all non-table fields
+will be set:
+
+ * Scalar fields are always populated, even if their value is equal to the
+   default.
+ * Vectors are set to zero-length vectors if there is no data in the vector.
+ * Strings are set to the empty string if there is no data in the string.
+
+These limitations are a consequence of how flatbuffers are represented in the
+object API, and is not an issue when copying from regular flatbuffer types.
+For copying from raw flatbuffer objects (which is what most existing code
+uses), these caveats do not apply, and there is no loss of information.
+
 ### Sample Usage
 
 The below example constructs a table of the above example `TestTable`:
 
 ```cpp
-aos::FixedAllocator allocator(TestTableStatic::kUnalignedBufferSize);
-Builder<TestTableStatic> builder(&allocator);
+Builder<TestTableStatic> builder;
 TestTableStatic *object = builder.get();
 object->set_scalar(123);
 {
@@ -436,10 +453,24 @@
   space allocated for the vector; returns false on failure (e.g., if you are in
   a fixed-size allocator that does not support increasing the size past a
   certain point).
-* `bool FromFlatbuffer(const flatbuffers::Vector<>*)`: Attempts to copy an
+* `bool FromFlatbuffer(const flatbuffers::Vector<>&)`: Attempts to copy an
   existing vector into this `Vector`. This may attempt to call `reserve()`
   if the new vector is longer than `capacity()`. If the copy fails for
   any reason, returns `false`.
+* `bool FromFlatbuffer(const std::vector<>&)`: Attempts to copy an
+  existing vector into this `Vector`. This may attempt to call `reserve()`
+  if the new vector is longer than `capacity()`. If the copy fails for
+  any reason, returns `false`. This is called "`FromFlatbuffer`" because
+  the [Flatbuffer Object
+  API](https://flatbuffers.dev/flatbuffers_guide_use_cpp.html) uses
+  `std::vector<>` to represent vectors.
+* `bool FromData(const T*, size_t)`: Attempts to copy a contiguous set of data
+  from the provided pointer. Only applies to inline types. This may attempt to
+  call `reserve()`, and if the call fails, it returns `false`.
+* `bool FromIterator(It begin, It end)`: Attempts to copy data from [begin, end)
+  into the vector. Does not assume that the data is stored contiguously in
+  memory. Only applies to inline types. This may attempt to
+  call `reserve()`, and if the call fails, it returns `false`.
 
 #### Managing Resizing of Vectors
 
@@ -510,12 +541,25 @@
 # upgraded to static_flatbuffer rules.
 static_flatbuffer(
     name = "test_message_fbs",
-    src = "test_message.fbs",
+    srcs = ["test_message.fbs"],
 )
 ```
 
+Then you must update the `#include` to use a `test_message_static.h` instead of
+the standard `test_message_generated.h` (the `_static.h` will include the
+`_generated.h` itself). Any C++ code can then be updated, noting that any
+AOS Senders that need to use the new API need to have their definitions updated
+to use the `TestMessageStatic` and must call `MakeStaticBuilder` instead of
+`MakeBuilder`. There is currently no support for using static flatbuffers with
+the AOS Fetcher or Watcher interfaces, as these only present immutable objects
+and so do not benefit from the API additions (and attempting to convert them to
+the new API would require additional overhead associated with copying the
+serialized flatbuffer into the correct format).
+
 Before:
 ```cpp
+#include "aos/events/test_message_generated.h"
+...
   aos::Sender<TestMessage> sender = loop1->MakeSender<TestMessage>("/test");
 
   loop->OnRun([&]() {
@@ -528,6 +572,8 @@
 
 After:
 ```cpp
+#include "aos/events/test_message_static.h"
+...
   aos::Sender<TestMessageStatic> sender =
       loop1->MakeSender<TestMessageStatic>("/test");
 
diff --git a/documentation/tutorials/rio-gdb.md b/documentation/tutorials/rio-gdb.md
new file mode 100644
index 0000000..eb7bc16
--- /dev/null
+++ b/documentation/tutorials/rio-gdb.md
@@ -0,0 +1,29 @@
+# Roborio GDB
+
+The roborio has a gdb-server command so we need to set the rio as our server and connect to it with some host device.
+
+## Steps
+
+### Rio
+This command will start the gdb-server program on the rio.
+```bash
+gdbserver localhost:8898 program
+```
+
+### Host
+```bash
+# this is the gdb-multiarch package on debian. Its needed because your host machine
+# likely isn't using the architecture the rio uses.
+gdb-multiarch
+# This connects us using an extended remote to the roborio, which lets us use the run command.
+target extended-remote ip:8898
+# Here we specify the name of program from the rio side
+set remote exec-file program
+# This makes it so we don't stop when getting realtime signals
+handle SIG33 pass nostop noprint
+# From here on you can use gdb like you normally would
+# So we can run our program with some args
+r args
+# To exit the gdb-server program on the rio side we need to run this command.
+monitor exit
+```
diff --git a/frc971/analysis/BUILD b/frc971/analysis/BUILD
index b179ad9..9c236c8 100644
--- a/frc971/analysis/BUILD
+++ b/frc971/analysis/BUILD
@@ -31,6 +31,9 @@
         "//y2023/control_loops/superstructure:superstructure_plotter",
         "//y2023/localizer:corrections_plotter",
         "//y2023/localizer:localizer_plotter",
+        "//y2024/control_loops/superstructure:superstructure_plotter",
+        "//y2024/localizer:corrections_plotter",
+        "//y2024/localizer:localizer_plotter",
     ],
 )
 
diff --git a/frc971/analysis/plot_index.ts b/frc971/analysis/plot_index.ts
index 1ee85d1..610ac4b 100644
--- a/frc971/analysis/plot_index.ts
+++ b/frc971/analysis/plot_index.ts
@@ -21,46 +21,32 @@
 // using JSON rather than requiring people to write a script just to create
 // a plot.
 import {Configuration} from '../../aos/configuration_generated';
-import {Connection} from '../../aos/network/www/proxy';
-import {plotImu} from '../wpilib/imu_plotter';
-import {plotDrivetrain} from '../control_loops/drivetrain/drivetrain_plotter';
-import {plotSpline} from '../control_loops/drivetrain/spline_plotter';
-import {plotDownEstimator} from '../control_loops/drivetrain/down_estimator_plotter';
-import {plotRobotState} from
-    '../control_loops/drivetrain/robot_state_plotter'
-import {plotFinisher as plot2020Finisher} from
-    '../../y2020/control_loops/superstructure/finisher_plotter'
-import {plotTurret as plot2020Turret} from
-    '../../y2020/control_loops/superstructure/turret_plotter'
-import {plotLocalizer as plot2020Localizer} from
-    '../../y2020/control_loops/drivetrain/localizer_plotter'
-import {plotAccelerator as plot2020Accelerator} from
-    '../../y2020/control_loops/superstructure/accelerator_plotter'
-import {plotHood as plot2020Hood} from
-    '../../y2020/control_loops/superstructure/hood_plotter'
-import {plotSuperstructure as plot2021Superstructure} from
-    '../../y2021_bot3/control_loops/superstructure/superstructure_plotter';
-import {plotTurret as plot2022Turret} from
-    '../../y2022/control_loops/superstructure/turret_plotter'
-import {plotSuperstructure as plot2022Superstructure} from
-    '../../y2022/control_loops/superstructure/superstructure_plotter'
-import {plotSuperstructure as plot2023Superstructure} from
-    '../../y2023/control_loops/superstructure/superstructure_plotter'
-import {plotCatapult as plot2022Catapult} from
-    '../../y2022/control_loops/superstructure/catapult_plotter'
-import {plotIntakeFront as plot2022IntakeFront, plotIntakeBack as plot2022IntakeBack} from
-    '../../y2022/control_loops/superstructure/intake_plotter'
-import {plotClimber as plot2022Climber} from
-    '../../y2022/control_loops/superstructure/climber_plotter'
-import {plotLocalizer as plot2022Localizer} from
-    '../../y2022/localizer/localizer_plotter'
-import {plotLocalizer as plot2023Localizer} from
-    '../../y2023/localizer/localizer_plotter'
-import {plotVision as plot2022Vision} from
-    '../../y2022/vision/vision_plotter'
-import {plotVision as plot2023Corrections} from
-    '../../y2023/localizer/corrections_plotter'
 import {plotDemo} from '../../aos/network/www/demo_plot';
+import {Connection} from '../../aos/network/www/proxy';
+import {plotLocalizer as plot2020Localizer} from '../../y2020/control_loops/drivetrain/localizer_plotter'
+import {plotAccelerator as plot2020Accelerator} from '../../y2020/control_loops/superstructure/accelerator_plotter'
+import {plotFinisher as plot2020Finisher} from '../../y2020/control_loops/superstructure/finisher_plotter'
+import {plotHood as plot2020Hood} from '../../y2020/control_loops/superstructure/hood_plotter'
+import {plotTurret as plot2020Turret} from '../../y2020/control_loops/superstructure/turret_plotter'
+import {plotSuperstructure as plot2021Superstructure} from '../../y2021_bot3/control_loops/superstructure/superstructure_plotter';
+import {plotCatapult as plot2022Catapult} from '../../y2022/control_loops/superstructure/catapult_plotter'
+import {plotClimber as plot2022Climber} from '../../y2022/control_loops/superstructure/climber_plotter'
+import {plotIntakeBack as plot2022IntakeBack, plotIntakeFront as plot2022IntakeFront} from '../../y2022/control_loops/superstructure/intake_plotter'
+import {plotSuperstructure as plot2022Superstructure} from '../../y2022/control_loops/superstructure/superstructure_plotter'
+import {plotTurret as plot2022Turret} from '../../y2022/control_loops/superstructure/turret_plotter'
+import {plotLocalizer as plot2022Localizer} from '../../y2022/localizer/localizer_plotter'
+import {plotVision as plot2022Vision} from '../../y2022/vision/vision_plotter'
+import {plotSuperstructure as plot2023Superstructure} from '../../y2023/control_loops/superstructure/superstructure_plotter'
+import {plotVision as plot2023Corrections} from '../../y2023/localizer/corrections_plotter'
+import {plotLocalizer as plot2023Localizer} from '../../y2023/localizer/localizer_plotter'
+import {plotClimber as plot2024Climber, plotIntake as plot2024Intake, plotSuperstructure as plot2024Superstructure} from '../../y2024/control_loops/superstructure/superstructure_plotter'
+import {plotVision as plot2024Corrections} from '../../y2024/localizer/corrections_plotter'
+import {plotLocalizer as plot2024Localizer} from '../../y2024/localizer/localizer_plotter'
+import {plotDownEstimator} from '../control_loops/drivetrain/down_estimator_plotter';
+import {plotDrivetrain} from '../control_loops/drivetrain/drivetrain_plotter';
+import {plotRobotState} from '../control_loops/drivetrain/robot_state_plotter'
+import {plotSpline} from '../control_loops/drivetrain/spline_plotter';
+import {plotImu} from '../wpilib/imu_plotter';
 
 const rootDiv = document.createElement('div');
 rootDiv.style.width = '100%';
@@ -118,6 +104,11 @@
   ['Spline Debug', new PlotState(plotDiv, plotSpline)],
   ['Down Estimator', new PlotState(plotDiv, plotDownEstimator)],
   ['Robot State', new PlotState(plotDiv, plotRobotState)],
+  ['2024 Vision', new PlotState(plotDiv, plot2024Corrections)],
+  ['2024 Localizer', new PlotState(plotDiv, plot2024Localizer)],
+  ['2024 Superstructure', new PlotState(plotDiv, plot2024Superstructure)],
+  ['2024 Climber', new PlotState(plotDiv, plot2024Climber)],
+  ['2024 Intake', new PlotState(plotDiv, plot2024Intake)],
   ['2023 Vision', new PlotState(plotDiv, plot2023Corrections)],
   ['2023 Localizer', new PlotState(plotDiv, plot2023Localizer)],
   ['2023 Superstructure', new PlotState(plotDiv, plot2023Superstructure)],
diff --git a/frc971/can_logger/asc_logger.cc b/frc971/can_logger/asc_logger.cc
index 6848938..5282d13 100644
--- a/frc971/can_logger/asc_logger.cc
+++ b/frc971/can_logger/asc_logger.cc
@@ -12,11 +12,11 @@
 }
 
 void AscLogger::HandleFrame(const CanFrame &frame) {
-  if (!first_frame_monotonic_) {
-    aos::monotonic_clock::time_point time(
-        std::chrono::nanoseconds(frame.monotonic_timestamp_ns()));
+  if (!first_frame_realtime_) {
+    aos::realtime_clock::time_point time(
+        std::chrono::nanoseconds(frame.realtime_timestamp_ns()));
 
-    first_frame_monotonic_ = time;
+    first_frame_realtime_ = time;
 
     WriteHeader(output_, event_loop_->realtime_now());
   }
@@ -60,11 +60,11 @@
 }  // namespace
 
 void AscLogger::WriteFrame(std::ostream &file, const CanFrame &frame) {
-  aos::monotonic_clock::time_point frame_timestamp(
-      std::chrono::nanoseconds(frame.monotonic_timestamp_ns()));
+  aos::realtime_clock::time_point frame_timestamp(
+      std::chrono::nanoseconds(frame.realtime_timestamp_ns()));
 
   std::chrono::duration<double> time(frame_timestamp -
-                                     first_frame_monotonic_.value());
+                                     first_frame_realtime_.value());
 
   // TODO: maybe this should not be hardcoded
   const int device_id = 1;
diff --git a/frc971/can_logger/asc_logger.h b/frc971/can_logger/asc_logger.h
index 2be2eb2..e7d4e74 100644
--- a/frc971/can_logger/asc_logger.h
+++ b/frc971/can_logger/asc_logger.h
@@ -25,7 +25,7 @@
   static void WriteHeader(std::ostream &file,
                           aos::realtime_clock::time_point start_time);
 
-  std::optional<aos::monotonic_clock::time_point> first_frame_monotonic_;
+  std::optional<aos::realtime_clock::time_point> first_frame_realtime_;
 
   std::ofstream output_;
 
diff --git a/frc971/can_logger/can_logger.cc b/frc971/can_logger/can_logger.cc
index cfc3dd8..380410f 100644
--- a/frc971/can_logger/can_logger.cc
+++ b/frc971/can_logger/can_logger.cc
@@ -1,5 +1,9 @@
 #include "frc971/can_logger/can_logger.h"
 
+DEFINE_bool(poll, false,
+            "If true, poll the CAN bus every 100ms.  If false, wake up for "
+            "each frame and publish it.");
+
 namespace frc971::can_logger {
 
 CanLogger::CanLogger(aos::ShmEventLoop *event_loop,
@@ -8,8 +12,10 @@
     : shm_event_loop_(event_loop),
       fd_(socket(PF_CAN, SOCK_RAW | SOCK_NONBLOCK, CAN_RAW)),
       frames_sender_(shm_event_loop_->MakeSender<CanFrame>(channel_name)) {
-  // TOOD(max): Figure out a proper priority
-  shm_event_loop_->SetRuntimeRealtimePriority(10);
+  // TODO(max): Figure out a proper priority
+  if (!FLAGS_poll) {
+    shm_event_loop_->SetRuntimeRealtimePriority(10);
+  }
   struct ifreq ifr;
   strcpy(ifr.ifr_name, interface_name.data());
   PCHECK(ioctl(fd_.get(), SIOCGIFINDEX, &ifr) == 0)
@@ -35,7 +41,14 @@
   CHECK_EQ(opt_size, sizeof(recieve_buffer_size));
   VLOG(0) << "CAN recieve bufffer is " << recieve_buffer_size << " bytes large";
 
-  shm_event_loop_->epoll()->OnReadable(fd_.get(), [this]() { Poll(); });
+  if (FLAGS_poll) {
+    aos::TimerHandler *timer_handler =
+        shm_event_loop_->AddTimer([this]() { Poll(); });
+    timer_handler->set_name("CAN logging Loop");
+    timer_handler->Schedule(event_loop->monotonic_now(), kPollPeriod);
+  } else {
+    shm_event_loop_->epoll()->OnReadable(fd_.get(), [this]() { Poll(); });
+  }
 }
 
 void CanLogger::Poll() {
@@ -75,8 +88,8 @@
   can_frame_builder.add_can_id(frame.can_id);
   can_frame_builder.add_flags(frame.flags);
   can_frame_builder.add_data(frame_data);
-  can_frame_builder.add_monotonic_timestamp_ns(
-      static_cast<std::chrono::nanoseconds>(
+  can_frame_builder.add_realtime_timestamp_ns(
+      std::chrono::duration_cast<std::chrono::nanoseconds>(
           std::chrono::seconds(tv.tv_sec) +
           std::chrono::microseconds(tv.tv_usec))
           .count());
diff --git a/frc971/can_logger/can_logging.fbs b/frc971/can_logger/can_logging.fbs
index ba60241..c7a82f2 100644
--- a/frc971/can_logger/can_logging.fbs
+++ b/frc971/can_logger/can_logging.fbs
@@ -7,7 +7,7 @@
   // The body of the CAN frame up to 64 bytes long.
   data:[ubyte] (id: 1);
   // The hardware timestamp of when the frame came in
-  monotonic_timestamp_ns:uint64 (id: 2);
+  realtime_timestamp_ns:uint64 (id: 2);
   // Additional flags for CAN FD
   flags: ubyte (id: 3);
 }
diff --git a/frc971/control_loops/aiming/aiming.cc b/frc971/control_loops/aiming/aiming.cc
index 4cf9255..572ed1c 100644
--- a/frc971/control_loops/aiming/aiming.cc
+++ b/frc971/control_loops/aiming/aiming.cc
@@ -54,6 +54,8 @@
 }  // namespace
 
 TurretGoal AimerGoal(const ShotConfig &config, const RobotState &state) {
+  CHECK(config.ball_speed_over_ground > 0.0)
+      << ": Ball speed must be positive.";
   TurretGoal result;
   // This code manages compensating the goal turret heading for the robot's
   // current velocity, to allow for shooting on-the-fly.
diff --git a/frc971/control_loops/aiming/aiming_test.cc b/frc971/control_loops/aiming/aiming_test.cc
index 4ec7eb9..a4f7bf2 100644
--- a/frc971/control_loops/aiming/aiming_test.cc
+++ b/frc971/control_loops/aiming/aiming_test.cc
@@ -158,4 +158,17 @@
   EXPECT_FLOAT_EQ(0.0, goal.velocity);
 }
 
+// Tests that we fail if the ballspeed is zero.  Can't hit anything if the ball
+// isn't moving.
+TEST(AimerDeathTest, ZeroBallSpeed) {
+  const Pose target({0.0, 0.0, 0.0}, 0.0);
+  Pose robot_pose({1.0, 1.0, 1.0}, 0.0);
+  const constants::Range range{-2.36, 2.36, -2.16, 2.16};
+  const double kBallSpeed = 0.0;
+  EXPECT_DEATH(AimerGoal(ShotConfig{target, ShotMode::kShootOnTheFly, range,
+                                    kBallSpeed, 0.0, 0.0},
+                         RobotState{robot_pose, {0.0, 0.0}, 0.0, 0.0}),
+               "Ball speed must be positive.");
+}
+
 }  // namespace frc971::control_loops::aiming::testing
diff --git a/frc971/control_loops/catapult/catapult.cc b/frc971/control_loops/catapult/catapult.cc
index 2e1867c..a827dc9 100644
--- a/frc971/control_loops/catapult/catapult.cc
+++ b/frc971/control_loops/catapult/catapult.cc
@@ -102,9 +102,11 @@
 
     case CatapultState::RESETTING:
       if (catapult_.controller().R(1, 0) > 7.0) {
-        catapult_.AdjustProfile(7.0, 2000.0);
+        catapult_.mutable_profile()->set_maximum_velocity(7.0);
+        catapult_.mutable_profile()->set_maximum_acceleration(2000.0);
       } else if (catapult_.controller().R(1, 0) > 0.0) {
-        catapult_.AdjustProfile(7.0, 1000.0);
+        catapult_.mutable_profile()->set_maximum_velocity(7.0);
+        catapult_.mutable_profile()->set_maximum_acceleration(1000.0);
       } else {
         catapult_state_ = CatapultState::PROFILE;
       }
@@ -135,4 +137,4 @@
   return catapult_.MakeStatus(fbb);
 }
 
-}  // namespace frc971::control_loops::catapult
\ No newline at end of file
+}  // namespace frc971::control_loops::catapult
diff --git a/frc971/control_loops/drivetrain/BUILD b/frc971/control_loops/drivetrain/BUILD
index afbce9b..789de2b 100644
--- a/frc971/control_loops/drivetrain/BUILD
+++ b/frc971/control_loops/drivetrain/BUILD
@@ -30,6 +30,11 @@
 )
 
 static_flatbuffer(
+    name = "rio_localizer_inputs_fbs",
+    srcs = ["rio_localizer_inputs.fbs"],
+)
+
+static_flatbuffer(
     name = "drivetrain_output_fbs",
     srcs = ["drivetrain_output.fbs"],
 )
@@ -42,7 +47,10 @@
 static_flatbuffer(
     name = "drivetrain_status_fbs",
     srcs = ["drivetrain_status.fbs"],
-    deps = ["//frc971/control_loops:control_loops_fbs"],
+    deps = [
+        "//frc971/control_loops:control_loops_fbs",
+        "//frc971/control_loops:encoder_fault_status_fbs",
+    ],
 )
 
 static_flatbuffer(
@@ -62,7 +70,10 @@
     name = "drivetrain_status_ts_fbs",
     srcs = ["drivetrain_status.fbs"],
     target_compatible_with = ["@platforms//os:linux"],
-    deps = ["//frc971/control_loops:control_loops_ts_fbs"],
+    deps = [
+        "//frc971/control_loops:control_loops_ts_fbs",
+        "//frc971/control_loops:encoder_fault_status_ts_fbs",
+    ],
 )
 
 flatbuffer_ts_library(
@@ -128,7 +139,10 @@
 static_flatbuffer(
     name = "drivetrain_status_float_fbs",
     srcs = ["drivetrain_status_float.fbs"],
-    deps = ["//frc971/control_loops:control_loops_fbs"],
+    deps = [
+        "//frc971/control_loops:control_loops_fbs",
+        "//frc971/control_loops:encoder_fault_status_fbs",
+    ],
 )
 
 aos_config(
@@ -136,6 +150,7 @@
     src = "drivetrain_simulation_channels.json",
     flatbuffers = [
         ":drivetrain_status_fbs",
+        "//frc971/control_loops:encoder_fault_status_fbs",
     ],
     target_compatible_with = ["@platforms//os:linux"],
     visibility = ["//visibility:public"],
@@ -162,6 +177,7 @@
         ":drivetrain_output_fbs",
         ":drivetrain_status_fbs",
         ":drivetrain_position_fbs",
+        ":drivetrain_can_position_fbs",
         ":localizer_fbs",
         "//frc971/queues:gyro_fbs",
         "//frc971/queues:gyro_uid_fbs",
@@ -444,6 +460,7 @@
         ":localizer",
         ":localizer_fbs",
         ":polydrivetrain",
+        ":rio_localizer_inputs_fbs",
         ":spline_goal_fbs",
         ":splinedrivetrain",
         ":ssdrivetrain",
@@ -789,6 +806,34 @@
     ],
 )
 
+cc_library(
+    name = "drivetrain_encoder_fault_detector",
+    srcs = ["drivetrain_encoder_fault_detector.cc"],
+    hdrs = ["drivetrain_encoder_fault_detector.h"],
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = [
+        "//aos/events:event_loop",
+        "//frc971/control_loops:encoder_fault_detector",
+        "//frc971/control_loops:encoder_fault_status_fbs",
+        "//frc971/control_loops/drivetrain:drivetrain_can_position_fbs",
+        "//frc971/control_loops/drivetrain:drivetrain_position_fbs",
+        "//frc971/control_loops/drivetrain:drivetrain_status_fbs",
+    ],
+)
+
+cc_test(
+    name = "drivetrain_encoder_fault_detector_test",
+    srcs = ["drivetrain_encoder_fault_detector_test.cc"],
+    data = [":simulation_config"],
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = [
+        ":drivetrain_encoder_fault_detector",
+        "//aos:json_to_flatbuffer",
+        "//aos/events:simulated_event_loop",
+        "//aos/testing:googletest",
+    ],
+)
+
 cc_test(
     name = "camera_test",
     srcs = ["camera_test.cc"],
diff --git a/frc971/control_loops/drivetrain/drivetrain.cc b/frc971/control_loops/drivetrain/drivetrain.cc
index 2b84a9d..95fab0a2 100644
--- a/frc971/control_loops/drivetrain/drivetrain.cc
+++ b/frc971/control_loops/drivetrain/drivetrain.cc
@@ -350,7 +350,11 @@
       dt_openloop_(dt_config_, filters_.kf()),
       dt_closedloop_(dt_config_, filters_.kf(), localizer),
       dt_spline_(dt_config_),
-      dt_line_follow_(dt_config_, localizer->target_selector()) {
+      dt_line_follow_(dt_config_, localizer->target_selector()),
+      localizer_input_sender_(
+          event_loop->TryMakeSender<
+              frc971::control_loops::drivetrain::RioLocalizerInputsStatic>(
+              "/drivetrain")) {
   event_loop->SetRuntimeRealtimePriority(30);
   for (size_t ii = 0; ii < trajectory_fetchers_.size(); ++ii) {
     trajectory_fetchers_[ii].fetcher =
@@ -613,6 +617,17 @@
   if (output) {
     output->CheckOk(output->Send(Output::Pack(*output->fbb(), &output_struct)));
   }
+
+  if (localizer_input_sender_.valid()) {
+    auto localizer_input_builder = localizer_input_sender_.MakeStaticBuilder();
+    localizer_input_builder->set_left_encoder(position->left_encoder());
+    localizer_input_builder->set_right_encoder(position->right_encoder());
+    if (output) {
+      localizer_input_builder->set_left_voltage(output_struct.left_voltage);
+      localizer_input_builder->set_right_voltage(output_struct.right_voltage);
+    }
+    localizer_input_builder.CheckOk(localizer_input_builder.Send());
+  }
 }
 
 flatbuffers::Offset<Output> DrivetrainLoop::Zero(
diff --git a/frc971/control_loops/drivetrain/drivetrain.h b/frc971/control_loops/drivetrain/drivetrain.h
index 87266f1..332b117 100644
--- a/frc971/control_loops/drivetrain/drivetrain.h
+++ b/frc971/control_loops/drivetrain/drivetrain.h
@@ -18,6 +18,7 @@
 #include "frc971/control_loops/drivetrain/localizer.h"
 #include "frc971/control_loops/drivetrain/localizer_generated.h"
 #include "frc971/control_loops/drivetrain/polydrivetrain.h"
+#include "frc971/control_loops/drivetrain/rio_localizer_inputs_static.h"
 #include "frc971/control_loops/drivetrain/splinedrivetrain.h"
 #include "frc971/control_loops/drivetrain/ssdrivetrain.h"
 #include "frc971/control_loops/polytope.h"
@@ -191,6 +192,8 @@
   bool has_been_enabled_ = false;
 
   aos::SendFailureCounter status_failure_counter_;
+  aos::Sender<::frc971::control_loops::drivetrain::RioLocalizerInputsStatic>
+      localizer_input_sender_;
 };
 
 }  // namespace frc971::control_loops::drivetrain
diff --git a/frc971/control_loops/drivetrain/drivetrain_can_position.fbs b/frc971/control_loops/drivetrain/drivetrain_can_position.fbs
index 29c0b85..539b18e 100644
--- a/frc971/control_loops/drivetrain/drivetrain_can_position.fbs
+++ b/frc971/control_loops/drivetrain/drivetrain_can_position.fbs
@@ -14,6 +14,11 @@
   // The ctre::phoenix::StatusCode of the measurement
   // Should be OK = 0
   status:int (id: 2);
+
+  // These values aren't used yet but are
+  // arrays that represent falcons on left/right sides of the drivetrain
+  left_falcons: [CANTalonFX] (id: 3);
+  right_falcons: [CANTalonFX] (id: 4);
 }
 
 root_type CANPosition;
diff --git a/frc971/control_loops/drivetrain/drivetrain_config.json b/frc971/control_loops/drivetrain/drivetrain_config.json
index a74d416..71c7628 100644
--- a/frc971/control_loops/drivetrain/drivetrain_config.json
+++ b/frc971/control_loops/drivetrain/drivetrain_config.json
@@ -43,6 +43,11 @@
     },
     {
       "name": "/drivetrain",
+      "type": "frc971.control_loops.drivetrain.CANPosition",
+      "frequency": 200
+    },
+    {
+      "name": "/drivetrain",
       "type": "frc971.control_loops.drivetrain.Status",
       "frequency": 200,
       "max_size": 2000
diff --git a/frc971/control_loops/drivetrain/drivetrain_encoder_fault_detector.cc b/frc971/control_loops/drivetrain/drivetrain_encoder_fault_detector.cc
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/frc971/control_loops/drivetrain/drivetrain_encoder_fault_detector.cc
diff --git a/frc971/control_loops/drivetrain/drivetrain_encoder_fault_detector.h b/frc971/control_loops/drivetrain/drivetrain_encoder_fault_detector.h
new file mode 100644
index 0000000..7513033
--- /dev/null
+++ b/frc971/control_loops/drivetrain/drivetrain_encoder_fault_detector.h
@@ -0,0 +1,115 @@
+#ifndef FRC971_CONTROL_LOOPS_DRIVETRAIN_DRIVETRAIN_ENCODER_FAULT_DETECTOR_H_
+#define FRC971_CONTROL_LOOPS_DRIVETRAIN_DRIVETRAIN_ENCODER_FAULT_DETECTOR_H_
+
+#include "aos/events/event_loop.h"
+#include "frc971/control_loops/drivetrain/drivetrain_can_position_generated.h"
+#include "frc971/control_loops/drivetrain/drivetrain_position_generated.h"
+#include "frc971/control_loops/drivetrain/drivetrain_status_generated.h"
+#include "frc971/control_loops/encoder_fault_detector.h"
+
+namespace frc971 {
+namespace control_loops {
+namespace drivetrain {
+
+// This class will be used to detect any encoder faults within the drivetrain
+// using values from the left/right encoders and motors
+
+template <uint8_t NumberofDrivetrainMotors>
+class DrivetrainEncoderFaultDetector {
+ public:
+  DrivetrainEncoderFaultDetector(aos::EventLoop *event_loop);
+
+  void UpdateDetectors();
+
+  void SendMessage();
+
+  flatbuffers::Offset<drivetrain::Faults> PopulateFaults(
+      flatbuffers::FlatBufferBuilder *fbb);
+
+ private:
+  aos::EventLoop *event_loop_;
+  EncoderFaultDetector<NumberofDrivetrainMotors> left_encoder_fault_detector_;
+  EncoderFaultDetector<NumberofDrivetrainMotors> right_encoder_fault_detector_;
+  aos::Sender<frc971::control_loops::drivetrain::Status>
+      drivetrain_status_sender_;
+  aos::Fetcher<control_loops::drivetrain::Position>
+      drivetrain_position_fetcher_;
+  aos::Fetcher<control_loops::drivetrain::CANPosition>
+      drivetrain_can_position_fetcher_;
+};
+
+template <uint8_t NumberofDrivetrainMotors>
+DrivetrainEncoderFaultDetector<NumberofDrivetrainMotors>::
+    DrivetrainEncoderFaultDetector(aos::EventLoop *event_loop)
+    : event_loop_(event_loop),
+      drivetrain_status_sender_(
+          event_loop->MakeSender<drivetrain::Status>("/drivetrain")),
+      drivetrain_position_fetcher_(
+          event_loop->MakeFetcher<drivetrain::Position>("/drivetrain")),
+      drivetrain_can_position_fetcher_(
+          event_loop->MakeFetcher<drivetrain::CANPosition>("/drivetrain")) {
+  event_loop
+      ->AddTimer([this]() {
+        UpdateDetectors();
+        SendMessage();
+      })
+      ->Schedule(event_loop->monotonic_now(),
+                 std::chrono::duration_cast<aos::monotonic_clock::duration>(
+                     std::chrono::milliseconds(5)));
+}
+
+template <uint8_t NumberofDrivetrainMotors>
+void DrivetrainEncoderFaultDetector<
+    NumberofDrivetrainMotors>::UpdateDetectors() {
+  drivetrain_position_fetcher_.Fetch();
+  drivetrain_can_position_fetcher_.Fetch();
+
+  if (drivetrain_position_fetcher_.get() &&
+      drivetrain_can_position_fetcher_.get()) {
+    left_encoder_fault_detector_.Iterate(
+        drivetrain_position_fetcher_->left_encoder(),
+        drivetrain_can_position_fetcher_->left_falcons(),
+        event_loop_->monotonic_now());
+    right_encoder_fault_detector_.Iterate(
+        drivetrain_position_fetcher_->right_encoder(),
+        drivetrain_can_position_fetcher_->right_falcons(),
+        event_loop_->monotonic_now());
+  }
+}
+
+template <uint8_t NumberofDrivetrainMotors>
+void DrivetrainEncoderFaultDetector<NumberofDrivetrainMotors>::SendMessage() {
+  auto builder = drivetrain_status_sender_.MakeBuilder();
+  auto offset = PopulateFaults(builder.fbb());
+
+  drivetrain::Status::Builder drivetrain_status_builder =
+      builder.MakeBuilder<drivetrain::Status>();
+
+  drivetrain_status_builder.add_encoder_faults(offset);
+  builder.CheckOk(builder.Send(drivetrain_status_builder.Finish()));
+}
+
+template <uint8_t NumberofDrivetrainMotors>
+flatbuffers::Offset<drivetrain::Faults>
+DrivetrainEncoderFaultDetector<NumberofDrivetrainMotors>::PopulateFaults(
+    flatbuffers::FlatBufferBuilder *fbb) {
+  flatbuffers::Offset<EncoderFaultStatus> left_encoder_fault_detector_offset =
+      left_encoder_fault_detector_.PopulateStatus(fbb);
+  flatbuffers::Offset<EncoderFaultStatus> right_encoder_fault_detector_offset =
+      right_encoder_fault_detector_.PopulateStatus(fbb);
+
+  drivetrain::Faults::Builder drivetrain_faults_builder(*fbb);
+
+  drivetrain_faults_builder.add_left_faulted(
+      left_encoder_fault_detector_offset);
+  drivetrain_faults_builder.add_right_faulted(
+      right_encoder_fault_detector_offset);
+
+  return drivetrain_faults_builder.Finish();
+}
+
+}  // namespace drivetrain
+}  // namespace control_loops
+}  // namespace frc971
+
+#endif
\ No newline at end of file
diff --git a/frc971/control_loops/drivetrain/drivetrain_encoder_fault_detector_test.cc b/frc971/control_loops/drivetrain/drivetrain_encoder_fault_detector_test.cc
new file mode 100644
index 0000000..adc8972
--- /dev/null
+++ b/frc971/control_loops/drivetrain/drivetrain_encoder_fault_detector_test.cc
@@ -0,0 +1,416 @@
+#include "frc971/control_loops/drivetrain/drivetrain_encoder_fault_detector.h"
+
+#include "gtest/gtest.h"
+
+#include "aos/events/simulated_event_loop.h"
+#include "aos/json_to_flatbuffer.h"
+
+namespace frc971::control_loops::drivetrain::testing {
+
+class EncoderFaultDetectorTest : public ::testing::Test {
+ public:
+  EncoderFaultDetectorTest()
+      : config_(aos::configuration::ReadConfig(
+            "frc971/control_loops/drivetrain/simulation_config.json")),
+        event_loop_factory_(&config_.message()),
+        event_loop_(event_loop_factory_.MakeEventLoop(
+            "drivetrain_encoder_fault_detector", nullptr)),
+        drivetrain_status_fetcher_(
+            event_loop_->MakeFetcher<frc971::control_loops::drivetrain::Status>(
+                "/drivetrain")),
+        drivetrain_position_sender_(
+            event_loop_->MakeSender<drivetrain::Position>("/drivetrain")),
+        drivetrain_can_position_sender_(
+            event_loop_->MakeSender<drivetrain::CANPosition>("/drivetrain")),
+        fault_detector_(event_loop_.get()) {}
+  void ResetEncoders() {
+    left_encoder_ = 0.0;
+    right_encoder_ = 0.0;
+    right_falcons_ = {0.0, 0.0};
+    left_falcons_ = {0.0, 0.0};
+  }
+  aos::FlatbufferDetachedBuffer<control_loops::drivetrain::Position>
+  ConstructPositionFlatBuffer(double left_encoder, double right_encoder) {
+    return aos::JsonToFlatbuffer<control_loops::drivetrain::Position>(
+        absl::StrFormat("{ \"left_encoder\": %f, \"right_encoder\": %f }",
+                        left_encoder, right_encoder));
+  }
+
+  aos::FlatbufferDetachedBuffer<control_loops::drivetrain::CANPosition>
+  ConstructCANPositionFlatBuffer(const std::vector<double> &left_falcons,
+                                 const std::vector<double> &right_falcons) {
+    if (left_falcons.size() == right_falcons.size()) {
+      const size_t num_falcons = left_falcons.size();
+      std::string json = "{ \"left_falcons\":[";
+
+      for (size_t i = 0; i < num_falcons; ++i) {
+        json += absl::StrFormat("{ \"position\": %f }", left_falcons[i]);
+        if (i + 1 < num_falcons) {
+          json += ", ";
+        }
+      }
+
+      json += "], \"right_falcons\":[";
+
+      for (size_t i = 0; i < num_falcons; ++i) {
+        json += absl::StrFormat("{ \"position\": %f }", right_falcons[i]);
+        if (i + 1 < num_falcons) {
+          json += ", ";
+        }
+      }
+
+      json += "]}";
+      return aos::JsonToFlatbuffer<control_loops::drivetrain::CANPosition>(
+          json);
+    }
+    LOG(FATAL) << "You must provide two falcon arrays of equal length";
+    return aos::JsonToFlatbuffer<control_loops::drivetrain::CANPosition>("");
+  }
+
+  void SendPositionMessages() {
+    drivetrain_position_sender_.CheckOk(drivetrain_position_sender_.Send(
+        ConstructPositionFlatBuffer(left_encoder_, right_encoder_)));
+    drivetrain_can_position_sender_.CheckOk(
+        drivetrain_can_position_sender_.Send(
+            ConstructCANPositionFlatBuffer(left_falcons_, right_falcons_)));
+  }
+
+ protected:
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config_;
+  aos::SimulatedEventLoopFactory event_loop_factory_;
+  std::unique_ptr<aos::EventLoop> event_loop_;
+
+  aos::Fetcher<frc971::control_loops::drivetrain::Status>
+      drivetrain_status_fetcher_;
+  aos::Sender<control_loops::drivetrain::Position> drivetrain_position_sender_;
+  aos::Sender<control_loops::drivetrain::CANPosition>
+      drivetrain_can_position_sender_;
+
+  DrivetrainEncoderFaultDetector<2> fault_detector_;
+  double left_encoder_ = 0.0;
+  double right_encoder_ = 0.0;
+  std::vector<double> right_falcons_;
+  std::vector<double> left_falcons_;
+};
+
+// Test simulates if drivetrain encoders are idle
+TEST_F(EncoderFaultDetectorTest, Idle) {
+  ResetEncoders();
+
+  event_loop_->AddTimer([this]() { SendPositionMessages(); })
+      ->Schedule(event_loop_->monotonic_now(),
+                 std::chrono::duration_cast<aos::monotonic_clock::duration>(
+                     std::chrono::milliseconds(5)));
+  event_loop_factory_.RunFor(std::chrono::seconds(1));
+
+  CHECK(drivetrain_status_fetcher_.Fetch());
+
+  EXPECT_EQ(
+      false,
+      drivetrain_status_fetcher_->encoder_faults()->right_faulted()->faulted());
+  EXPECT_EQ(
+      false,
+      drivetrain_status_fetcher_->encoder_faults()->left_faulted()->faulted());
+}
+
+// Test simulates if drivetrain encoders are increasing
+TEST_F(EncoderFaultDetectorTest, Increasing) {
+  ResetEncoders();
+
+  event_loop_
+      ->AddTimer([this]() {
+        SendPositionMessages();
+        left_encoder_ += 0.1;
+        right_encoder_ += 0.1;
+        for (double &falcon : left_falcons_) {
+          falcon += 0.1;
+        }
+        for (double &falcon : right_falcons_) {
+          falcon += 0.1;
+        }
+      })
+      ->Schedule(event_loop_->monotonic_now(),
+                 std::chrono::duration_cast<aos::monotonic_clock::duration>(
+                     std::chrono::milliseconds(5)));
+  event_loop_factory_.RunFor(std::chrono::seconds(5));
+
+  CHECK(drivetrain_status_fetcher_.Fetch());
+
+  EXPECT_EQ(
+      false,
+      drivetrain_status_fetcher_->encoder_faults()->right_faulted()->faulted());
+  EXPECT_EQ(
+      false,
+      drivetrain_status_fetcher_->encoder_faults()->left_faulted()->faulted());
+}
+
+// Test simulates if drivetrain encoders are decreasing
+TEST_F(EncoderFaultDetectorTest, Decreasing) {
+  ResetEncoders();
+
+  event_loop_
+      ->AddTimer([this]() {
+        SendPositionMessages();
+        left_encoder_ -= 0.1;
+        right_encoder_ -= 0.1;
+        for (double &falcon : left_falcons_) {
+          falcon -= 0.1;
+        }
+        for (double &falcon : right_falcons_) {
+          falcon -= 0.1;
+        }
+      })
+      ->Schedule(event_loop_->monotonic_now(),
+                 std::chrono::duration_cast<aos::monotonic_clock::duration>(
+                     std::chrono::milliseconds(5)));
+  event_loop_factory_.RunFor(std::chrono::seconds(5));
+
+  CHECK(drivetrain_status_fetcher_.Fetch());
+
+  EXPECT_EQ(
+      false,
+      drivetrain_status_fetcher_->encoder_faults()->right_faulted()->faulted());
+  EXPECT_EQ(
+      false,
+      drivetrain_status_fetcher_->encoder_faults()->left_faulted()->faulted());
+}
+
+// Test simulates if only the right drivetrain encoders are increasing
+TEST_F(EncoderFaultDetectorTest, OnlyIncreaseRightSide) {
+  ResetEncoders();
+
+  event_loop_
+      ->AddTimer([this]() {
+        SendPositionMessages();
+        right_encoder_ += 0.1;
+        for (double &falcon : right_falcons_) {
+          falcon += 0.1;
+        }
+      })
+      ->Schedule(event_loop_->monotonic_now(),
+                 std::chrono::duration_cast<aos::monotonic_clock::duration>(
+                     std::chrono::milliseconds(5)));
+  event_loop_factory_.RunFor(std::chrono::seconds(5));
+
+  CHECK(drivetrain_status_fetcher_.Fetch());
+
+  EXPECT_EQ(
+      false,
+      drivetrain_status_fetcher_->encoder_faults()->right_faulted()->faulted());
+  EXPECT_EQ(
+      false,
+      drivetrain_status_fetcher_->encoder_faults()->left_faulted()->faulted());
+}
+
+// Test simulates if only the left drivetrain encoders are increasing
+TEST_F(EncoderFaultDetectorTest, OnlyIncreaseLeftSide) {
+  ResetEncoders();
+
+  event_loop_
+      ->AddTimer([this]() {
+        SendPositionMessages();
+        left_encoder_ += 0.1;
+        for (double &falcon : left_falcons_) {
+          falcon += 0.1;
+        }
+      })
+      ->Schedule(event_loop_->monotonic_now(),
+                 std::chrono::duration_cast<aos::monotonic_clock::duration>(
+                     std::chrono::milliseconds(5)));
+  event_loop_factory_.RunFor(std::chrono::seconds(5));
+
+  CHECK(drivetrain_status_fetcher_.Fetch());
+
+  EXPECT_EQ(
+      false,
+      drivetrain_status_fetcher_->encoder_faults()->right_faulted()->faulted());
+  EXPECT_EQ(
+      false,
+      drivetrain_status_fetcher_->encoder_faults()->left_faulted()->faulted());
+}
+
+// Test simulates if only the right drivetrain encoders are decreasing
+TEST_F(EncoderFaultDetectorTest, OnlyDecreaseRightSide) {
+  ResetEncoders();
+
+  event_loop_
+      ->AddTimer([this]() {
+        SendPositionMessages();
+        right_encoder_ -= 0.1;
+        for (double &falcon : right_falcons_) {
+          falcon -= 0.1;
+        }
+      })
+      ->Schedule(event_loop_->monotonic_now(),
+                 std::chrono::duration_cast<aos::monotonic_clock::duration>(
+                     std::chrono::milliseconds(5)));
+  event_loop_factory_.RunFor(std::chrono::seconds(5));
+
+  CHECK(drivetrain_status_fetcher_.Fetch());
+
+  EXPECT_EQ(
+      false,
+      drivetrain_status_fetcher_->encoder_faults()->right_faulted()->faulted());
+  EXPECT_EQ(
+      false,
+      drivetrain_status_fetcher_->encoder_faults()->left_faulted()->faulted());
+}
+// Test simulates if only the left drivetrain encoders are decreasing
+TEST_F(EncoderFaultDetectorTest, OnlyDecreaseLeftSide) {
+  ResetEncoders();
+
+  event_loop_
+      ->AddTimer([this]() {
+        SendPositionMessages();
+        left_encoder_ -= 0.1;
+        for (double &falcon : left_falcons_) {
+          falcon -= 0.1;
+        }
+      })
+      ->Schedule(event_loop_->monotonic_now(),
+                 std::chrono::duration_cast<aos::monotonic_clock::duration>(
+                     std::chrono::milliseconds(5)));
+  event_loop_factory_.RunFor(std::chrono::seconds(5));
+
+  CHECK(drivetrain_status_fetcher_.Fetch());
+
+  EXPECT_EQ(
+      false,
+      drivetrain_status_fetcher_->encoder_faults()->right_faulted()->faulted());
+  EXPECT_EQ(
+      false,
+      drivetrain_status_fetcher_->encoder_faults()->left_faulted()->faulted());
+}
+
+// Test simulates that if there is no data for one second that their will be no
+// faults
+TEST_F(EncoderFaultDetectorTest, NoDataForOneSecond) {
+  ResetEncoders();
+
+  SendPositionMessages();
+
+  event_loop_factory_.RunFor(std::chrono::seconds(1));
+
+  left_encoder_ = 1.0;
+  right_encoder_ = 2.0;
+  for (double &falcon : left_falcons_) {
+    falcon = 3.0;
+  }
+  for (double &falcon : right_falcons_) {
+    falcon = 4.0;
+  }
+
+  SendPositionMessages();
+
+  event_loop_factory_.RunFor(std::chrono::seconds(1));
+
+  CHECK(drivetrain_status_fetcher_.Fetch());
+
+  EXPECT_EQ(
+      false,
+      drivetrain_status_fetcher_->encoder_faults()->right_faulted()->faulted());
+  EXPECT_EQ(
+      false,
+      drivetrain_status_fetcher_->encoder_faults()->left_faulted()->faulted());
+}
+
+// Test simulates that only the left encoder is increasing
+TEST_F(EncoderFaultDetectorTest, LeftEncoderFaulted) {
+  ResetEncoders();
+
+  event_loop_
+      ->AddTimer([this]() {
+        SendPositionMessages();
+        left_encoder_ += 0.1;
+      })
+      ->Schedule(event_loop_->monotonic_now(),
+                 std::chrono::duration_cast<aos::monotonic_clock::duration>(
+                     std::chrono::milliseconds(5)));
+  event_loop_factory_.RunFor(std::chrono::seconds(5));
+
+  CHECK(drivetrain_status_fetcher_.Fetch());
+
+  EXPECT_EQ(
+      false,
+      drivetrain_status_fetcher_->encoder_faults()->right_faulted()->faulted());
+  EXPECT_EQ(
+      true,
+      drivetrain_status_fetcher_->encoder_faults()->left_faulted()->faulted());
+}
+// Test simulates that only the right encoder is increasing
+TEST_F(EncoderFaultDetectorTest, RightEncoderFaulted) {
+  ResetEncoders();
+
+  event_loop_
+      ->AddTimer([this]() {
+        SendPositionMessages();
+        right_encoder_ += 0.1;
+      })
+      ->Schedule(event_loop_->monotonic_now(),
+                 std::chrono::duration_cast<aos::monotonic_clock::duration>(
+                     std::chrono::milliseconds(5)));
+  event_loop_factory_.RunFor(std::chrono::seconds(5));
+
+  CHECK(drivetrain_status_fetcher_.Fetch());
+
+  EXPECT_EQ(
+      true,
+      drivetrain_status_fetcher_->encoder_faults()->right_faulted()->faulted());
+  EXPECT_EQ(
+      false,
+      drivetrain_status_fetcher_->encoder_faults()->left_faulted()->faulted());
+}
+
+// Test simulates that only the left falcons are increasing
+TEST_F(EncoderFaultDetectorTest, LeftFalconsFaulted) {
+  ResetEncoders();
+
+  event_loop_
+      ->AddTimer([this]() {
+        SendPositionMessages();
+        for (double &falcon : left_falcons_) {
+          falcon += 0.1;
+        }
+      })
+      ->Schedule(event_loop_->monotonic_now(),
+                 std::chrono::duration_cast<aos::monotonic_clock::duration>(
+                     std::chrono::milliseconds(5)));
+  event_loop_factory_.RunFor(std::chrono::seconds(5));
+
+  CHECK(drivetrain_status_fetcher_.Fetch());
+
+  EXPECT_EQ(
+      false,
+      drivetrain_status_fetcher_->encoder_faults()->right_faulted()->faulted());
+  EXPECT_EQ(
+      true,
+      drivetrain_status_fetcher_->encoder_faults()->left_faulted()->faulted());
+}
+
+// Test simulates that only the right falcons are increasing
+TEST_F(EncoderFaultDetectorTest, RightFalconsFaulted) {
+  ResetEncoders();
+
+  event_loop_
+      ->AddTimer([this]() {
+        SendPositionMessages();
+        for (double &falcon : right_falcons_) {
+          falcon += 0.1;
+        }
+      })
+      ->Schedule(event_loop_->monotonic_now(),
+                 std::chrono::duration_cast<aos::monotonic_clock::duration>(
+                     std::chrono::milliseconds(5)));
+  event_loop_factory_.RunFor(std::chrono::seconds(5));
+
+  CHECK(drivetrain_status_fetcher_.Fetch());
+
+  EXPECT_EQ(
+      true,
+      drivetrain_status_fetcher_->encoder_faults()->right_faulted()->faulted());
+  EXPECT_EQ(
+      false,
+      drivetrain_status_fetcher_->encoder_faults()->left_faulted()->faulted());
+}
+
+}  // namespace frc971::control_loops::drivetrain::testing
\ No newline at end of file
diff --git a/frc971/control_loops/drivetrain/drivetrain_status.fbs b/frc971/control_loops/drivetrain/drivetrain_status.fbs
index 8158e75..8e18915 100644
--- a/frc971/control_loops/drivetrain/drivetrain_status.fbs
+++ b/frc971/control_loops/drivetrain/drivetrain_status.fbs
@@ -1,4 +1,5 @@
 include "frc971/control_loops/control_loops.fbs";
+include "frc971/control_loops/encoder_fault_status.fbs";
 
 namespace frc971.control_loops.drivetrain;
 
@@ -210,6 +211,11 @@
   accel_z_average:float (id: 8);
 }
 
+table Faults {
+  right_faulted:EncoderFaultStatus (id: 0);
+  left_faulted:EncoderFaultStatus (id: 1);
+}
+
 table Status {
   // Estimated speed of the center of the robot in m/s (positive forwards).
   robot_speed:double (id: 0);
@@ -271,6 +277,8 @@
 
   // Total number of status send failures.
   send_failures:uint64 (id: 28);
+
+  encoder_faults:Faults (id: 29);
 }
 
 root_type Status;
diff --git a/frc971/control_loops/drivetrain/hybrid_ekf.h b/frc971/control_loops/drivetrain/hybrid_ekf.h
index f784f2e..2f89a0c 100644
--- a/frc971/control_loops/drivetrain/hybrid_ekf.h
+++ b/frc971/control_loops/drivetrain/hybrid_ekf.h
@@ -757,8 +757,9 @@
     aos::monotonic_clock::time_point t) {
   CHECK(!observations_.empty());
   if (!observations_.full() && t < observations_.begin()->t) {
-    LOG(ERROR) << "Dropped an observation that was received before we "
-                  "initialized.\n";
+    AOS_LOG(ERROR,
+            "Dropped an observation that was received before we "
+            "initialized.\n");
     return;
   }
   auto cur_it = observations_.PushFromBottom(
diff --git a/frc971/control_loops/drivetrain/localization/BUILD b/frc971/control_loops/drivetrain/localization/BUILD
index 7b86f09..1cf83d4 100644
--- a/frc971/control_loops/drivetrain/localization/BUILD
+++ b/frc971/control_loops/drivetrain/localization/BUILD
@@ -9,7 +9,11 @@
     deps = [
         "//aos/events:event_loop",
         "//aos/network:message_bridge_server_fbs",
+        "//frc971/control_loops:pose",
         "//frc971/control_loops/drivetrain:drivetrain_output_fbs",
+        "//frc971/control_loops/drivetrain:drivetrain_position_fbs",
+        "//frc971/control_loops/drivetrain:hybrid_ekf",
+        "//frc971/control_loops/drivetrain:rio_localizer_inputs_fbs",
         "//frc971/input:joystick_state_fbs",
         "//frc971/vision:calibration_fbs",
         "@org_tuxfamily_eigen//:eigen",
diff --git a/frc971/control_loops/drivetrain/localization/utils.cc b/frc971/control_loops/drivetrain/localization/utils.cc
index 10ee445..0986037 100644
--- a/frc971/control_loops/drivetrain/localization/utils.cc
+++ b/frc971/control_loops/drivetrain/localization/utils.cc
@@ -3,24 +3,63 @@
 namespace frc971::control_loops::drivetrain {
 
 LocalizationUtils::LocalizationUtils(aos::EventLoop *event_loop)
-    : output_fetcher_(event_loop->MakeFetcher<Output>("/drivetrain")),
+    : event_loop_(event_loop),
+      output_fetcher_(event_loop->TryMakeFetcher<Output>("/drivetrain")),
+      position_fetcher_(event_loop->TryMakeFetcher<Position>("/drivetrain")),
+      combined_fetcher_(
+          event_loop->TryMakeFetcher<RioLocalizerInputs>("/drivetrain")),
       clock_offset_fetcher_(
           event_loop->MakeFetcher<aos::message_bridge::ServerStatistics>(
               "/aos")),
       joystick_state_fetcher_(
           event_loop->MakeFetcher<aos::JoystickState>("/roborio/aos")) {}
 
+namespace {
+template <typename T>
+Eigen::Vector2d GetVoltage(T &fetcher, aos::monotonic_clock::time_point now) {
+  fetcher.Fetch();
+  // Determine if the robot is likely to be disabled currently.
+  const bool disabled =
+      (fetcher.get() == nullptr) ||
+      (fetcher.context().monotonic_event_time + std::chrono::milliseconds(10) <
+       now);
+  return disabled ? Eigen::Vector2d::Zero()
+                  : Eigen::Vector2d{fetcher->left_voltage(),
+                                    fetcher->right_voltage()};
+}
+}  // namespace
 Eigen::Vector2d LocalizationUtils::VoltageOrZero(
     aos::monotonic_clock::time_point now) {
-  output_fetcher_.Fetch();
-  // Determine if the robot is likely to be disabled currently.
-  const bool disabled = (output_fetcher_.get() == nullptr) ||
-                        (output_fetcher_.context().monotonic_event_time +
-                             std::chrono::milliseconds(10) <
-                         now);
-  return disabled ? Eigen::Vector2d::Zero()
-                  : Eigen::Vector2d{output_fetcher_->left_voltage(),
-                                    output_fetcher_->right_voltage()};
+  if (output_fetcher_.valid()) {
+    return GetVoltage(output_fetcher_, now);
+  } else {
+    CHECK(combined_fetcher_.valid());
+    return GetVoltage(combined_fetcher_, now);
+  }
+}
+namespace {
+template <typename T>
+std::optional<Eigen::Vector2d> GetPosition(
+    T &fetcher, aos::monotonic_clock::time_point now) {
+  fetcher.Fetch();
+  const bool stale =
+      (fetcher.get() == nullptr) ||
+      (fetcher.context().monotonic_event_time + std::chrono::milliseconds(10) <
+       now);
+  return stale ? std::nullopt
+               : std::make_optional<Eigen::Vector2d>(fetcher->left_encoder(),
+                                                     fetcher->right_encoder());
+}
+}  // namespace
+
+std::optional<Eigen::Vector2d> LocalizationUtils::Encoders(
+    aos::monotonic_clock::time_point now) {
+  if (position_fetcher_.valid()) {
+    return GetPosition(position_fetcher_, now);
+  } else {
+    CHECK(combined_fetcher_.valid());
+    return GetPosition(combined_fetcher_, now);
+  }
 }
 
 bool LocalizationUtils::MaybeInAutonomous() {
@@ -39,6 +78,9 @@
 
 std::optional<aos::monotonic_clock::duration> LocalizationUtils::ClockOffset(
     std::string_view node) {
+  if (node == event_loop_->node()->name()->string_view()) {
+    return std::chrono::seconds(0);
+  }
   std::optional<aos::monotonic_clock::duration> monotonic_offset;
   clock_offset_fetcher_.Fetch();
   if (clock_offset_fetcher_.get() != nullptr) {
diff --git a/frc971/control_loops/drivetrain/localization/utils.h b/frc971/control_loops/drivetrain/localization/utils.h
index cfd443a..4d21e3b 100644
--- a/frc971/control_loops/drivetrain/localization/utils.h
+++ b/frc971/control_loops/drivetrain/localization/utils.h
@@ -5,6 +5,10 @@
 #include "aos/events/event_loop.h"
 #include "aos/network/message_bridge_server_generated.h"
 #include "frc971/control_loops/drivetrain/drivetrain_output_generated.h"
+#include "frc971/control_loops/drivetrain/drivetrain_position_generated.h"
+#include "frc971/control_loops/drivetrain/hybrid_ekf.h"
+#include "frc971/control_loops/drivetrain/rio_localizer_inputs_generated.h"
+#include "frc971/control_loops/pose.h"
 #include "frc971/input/joystick_state_generated.h"
 #include "frc971/vision/calibration_generated.h"
 
@@ -16,6 +20,7 @@
 //   has timed out.
 // * Offsets between monotonic clocks on different devices.
 // * Whether we are in autonomous mode.
+// * Drivetrain encoder voltages, as reported by the roborio.
 class LocalizationUtils {
  public:
   LocalizationUtils(aos::EventLoop *event_loop);
@@ -26,6 +31,11 @@
   // [left_voltage, right_voltage]
   Eigen::Vector2d VoltageOrZero(aos::monotonic_clock::time_point now);
 
+  // Returns the latest drivetrain encoder values (in meters), or nullopt if no
+  // position message is available (or if the message is stale).
+  // Returns encoders as [left_position, right_position]
+  std::optional<Eigen::Vector2d> Encoders(aos::monotonic_clock::time_point now);
+
   // Returns true if either there is no JoystickState message available or if
   // we are currently in autonomous mode.
   bool MaybeInAutonomous();
@@ -38,7 +48,11 @@
       std::string_view node);
 
  private:
+  aos::EventLoop *const event_loop_;
   aos::Fetcher<frc971::control_loops::drivetrain::Output> output_fetcher_;
+  aos::Fetcher<frc971::control_loops::drivetrain::Position> position_fetcher_;
+  aos::Fetcher<frc971::control_loops::drivetrain::RioLocalizerInputs>
+      combined_fetcher_;
   aos::Fetcher<aos::message_bridge::ServerStatistics> clock_offset_fetcher_;
   aos::Fetcher<aos::JoystickState> joystick_state_fetcher_;
 };
@@ -47,6 +61,63 @@
 Eigen::Matrix<double, 4, 4> FlatbufferToTransformationMatrix(
     const frc971::vision::calibration::TransformationMatrix &flatbuffer);
 
+// This approximates the Jacobian of a vector of [heading, distance, skew]
+// of a target with respect to the full state of a drivetrain EKF.
+// Note that the only nonzero values in the returned matrix will be in the
+// columns corresponding to the X, Y, and Theta components of the state.
+// This is suitable for use as the H matrix in the kalman updates of the EKF,
+// although due to the approximation it should not be used to actually
+// calculate the expected measurement.
+// target_pose is the global pose of the target that we have identified.
+// camera_pose is the current estimate of the global pose of
+//   the camera that can see the target.
+template <typename Scalar>
+Eigen::Matrix<double, 3, HybridEkf<Scalar>::kNStates>
+HMatrixForCameraHeadingDistanceSkew(const TypedPose<Scalar> &target_pose,
+                                    const TypedPose<Scalar> &camera_pose) {
+  // For all of the below calculations, we will assume to a first
+  // approximation that:
+  //
+  // dcamera_theta / dtheta ~= 1
+  // dcamera_x / dx ~= 1
+  // dcamera_y / dy ~= 1
+  //
+  // For cameras sufficiently far from the robot's origin, or if the robot were
+  // spinning extremely rapidly, this would not hold.
+
+  // To calculate dheading/d{x,y,theta}:
+  // heading = arctan2(target_pos - camera_pos) - camera_theta
+  Eigen::Matrix<Scalar, 3, 1> target_pos = target_pose.abs_pos();
+  Eigen::Matrix<Scalar, 3, 1> camera_pos = camera_pose.abs_pos();
+  Scalar diffx = target_pos.x() - camera_pos.x();
+  Scalar diffy = target_pos.y() - camera_pos.y();
+  Scalar norm2 = diffx * diffx + diffy * diffy;
+  Scalar dheadingdx = diffy / norm2;
+  Scalar dheadingdy = -diffx / norm2;
+  Scalar dheadingdtheta = -1.0;
+
+  // To calculate ddistance/d{x,y}:
+  // distance = sqrt(diffx^2 + diffy^2)
+  Scalar distance = ::std::sqrt(norm2);
+  Scalar ddistdx = -diffx / distance;
+  Scalar ddistdy = -diffy / distance;
+
+  // Skew = target.theta - camera.theta - heading
+  //      = target.theta - arctan2(target_pos - camera_pos)
+  Scalar dskewdx = -dheadingdx;
+  Scalar dskewdy = -dheadingdy;
+  Eigen::Matrix<Scalar, 3, HybridEkf<Scalar>::kNStates> H;
+  H.setZero();
+  H(0, HybridEkf<Scalar>::kX) = dheadingdx;
+  H(0, HybridEkf<Scalar>::kY) = dheadingdy;
+  H(0, HybridEkf<Scalar>::kTheta) = dheadingdtheta;
+  H(1, HybridEkf<Scalar>::kX) = ddistdx;
+  H(1, HybridEkf<Scalar>::kY) = ddistdy;
+  H(2, HybridEkf<Scalar>::kX) = dskewdx;
+  H(2, HybridEkf<Scalar>::kY) = dskewdy;
+  return H;
+}
+
 }  // namespace frc971::control_loops::drivetrain
 
 #endif  // FRC971_CONTROL_LOOPS_DRIVETRAIN_LOCALIZATION_UTILS_H_
diff --git a/frc971/control_loops/drivetrain/rio_localizer_inputs.fbs b/frc971/control_loops/drivetrain/rio_localizer_inputs.fbs
new file mode 100644
index 0000000..b9dc359
--- /dev/null
+++ b/frc971/control_loops/drivetrain/rio_localizer_inputs.fbs
@@ -0,0 +1,12 @@
+namespace frc971.control_loops.drivetrain;
+
+// This combines the drivetrian Position and Output messages
+// to be forwarded to the imu orin.
+table RioLocalizerInputs {
+  left_voltage:double (id: 0);
+  right_voltage:double (id: 1);
+  left_encoder:double (id: 2);
+  right_encoder:double (id: 3);
+}
+
+root_type RioLocalizerInputs;
diff --git a/frc971/control_loops/profiled_subsystem.h b/frc971/control_loops/profiled_subsystem.h
index 7a0b7fa..599c4da 100644
--- a/frc971/control_loops/profiled_subsystem.h
+++ b/frc971/control_loops/profiled_subsystem.h
@@ -148,7 +148,8 @@
 };
 
 template <typename ZeroingEstimator =
-              ::frc971::zeroing::PotAndIndexPulseZeroingEstimator>
+              ::frc971::zeroing::PotAndIndexPulseZeroingEstimator,
+          class Profile = aos::util::TrapezoidProfile>
 class SingleDOFProfiledSubsystem
     : public ::frc971::control_loops::ProfiledSubsystem<3, 1,
                                                         ZeroingEstimator> {
@@ -209,6 +210,9 @@
   double default_velocity() const { return default_velocity_; }
   double default_acceleration() const { return default_acceleration_; }
 
+  // Returns a pointer to the profile in use.
+  Profile *mutable_profile() { return &profile_; }
+
  protected:
   // Limits the provided goal to the soft limits.  Prints "name" when it fails
   // to aid debugging.
@@ -218,7 +222,7 @@
  private:
   void UpdateOffset(double offset);
 
-  aos::util::TrapezoidProfile profile_;
+  Profile profile_;
   bool enable_profile_ = true;
 
   // Current measurement.
@@ -240,12 +244,13 @@
 
 }  // namespace internal
 
-template <class ZeroingEstimator>
-SingleDOFProfiledSubsystem<ZeroingEstimator>::SingleDOFProfiledSubsystem(
-    ::std::unique_ptr<SimpleCappedStateFeedbackLoop<3, 1, 1>> loop,
-    const typename ZeroingEstimator::ZeroingConstants &zeroing_constants,
-    const ::frc971::constants::Range &range, double default_velocity,
-    double default_acceleration)
+template <class ZeroingEstimator, class Profile>
+SingleDOFProfiledSubsystem<ZeroingEstimator, Profile>::
+    SingleDOFProfiledSubsystem(
+        ::std::unique_ptr<SimpleCappedStateFeedbackLoop<3, 1, 1>> loop,
+        const typename ZeroingEstimator::ZeroingConstants &zeroing_constants,
+        const ::frc971::constants::Range &range, double default_velocity,
+        double default_acceleration)
     : ProfiledSubsystem<3, 1, ZeroingEstimator>(
           ::std::move(loop), {{ZeroingEstimator(zeroing_constants)}}),
       profile_(this->loop_->plant().coefficients().dt),
@@ -254,11 +259,11 @@
       default_acceleration_(default_acceleration) {
   Y_.setZero();
   offset_.setZero();
-  AdjustProfile(0.0, 0.0);
 }
 
-template <class ZeroingEstimator>
-void SingleDOFProfiledSubsystem<ZeroingEstimator>::UpdateOffset(double offset) {
+template <class ZeroingEstimator, class Profile>
+void SingleDOFProfiledSubsystem<ZeroingEstimator, Profile>::UpdateOffset(
+    double offset) {
   const double doffset = offset - offset_(0, 0);
   AOS_LOG(INFO, "Adjusting offset from %f to %f\n", offset_(0, 0), offset);
 
@@ -273,9 +278,10 @@
   CapGoal("R", &this->loop_->mutable_R());
 }
 
-template <class ZeroingEstimator>
+template <class ZeroingEstimator, class Profile>
 template <class StatusTypeBuilder>
-StatusTypeBuilder SingleDOFProfiledSubsystem<ZeroingEstimator>::BuildStatus(
+StatusTypeBuilder
+SingleDOFProfiledSubsystem<ZeroingEstimator, Profile>::BuildStatus(
     flatbuffers::FlatBufferBuilder *fbb) {
   flatbuffers::Offset<typename ZeroingEstimator::State> estimator_state =
       this->EstimatorState(fbb, 0);
@@ -306,8 +312,8 @@
   return builder;
 }
 
-template <class ZeroingEstimator>
-void SingleDOFProfiledSubsystem<ZeroingEstimator>::Correct(
+template <class ZeroingEstimator, class Profile>
+void SingleDOFProfiledSubsystem<ZeroingEstimator, Profile>::Correct(
     const typename ZeroingEstimator::Position &new_position) {
   this->estimators_[0].UpdateEstimate(new_position);
 
@@ -336,8 +342,8 @@
   this->X_hat_ = this->loop_->X_hat();
 }
 
-template <class ZeroingEstimator>
-void SingleDOFProfiledSubsystem<ZeroingEstimator>::CapGoal(
+template <class ZeroingEstimator, class Profile>
+void SingleDOFProfiledSubsystem<ZeroingEstimator, Profile>::CapGoal(
     const char *name, Eigen::Matrix<double, 3, 1> *goal, bool print) {
   // Limit the goal to min/max allowable positions.
   if ((*goal)(0, 0) > range_.upper) {
@@ -356,8 +362,8 @@
   }
 }
 
-template <class ZeroingEstimator>
-void SingleDOFProfiledSubsystem<ZeroingEstimator>::ForceGoal(
+template <class ZeroingEstimator, class Profile>
+void SingleDOFProfiledSubsystem<ZeroingEstimator, Profile>::ForceGoal(
     double goal, double goal_velocity) {
   set_unprofiled_goal(goal, goal_velocity, false);
   this->loop_->mutable_R() = this->unprofiled_goal_;
@@ -367,8 +373,8 @@
   this->profile_.MoveCurrentState(R.block<2, 1>(0, 0));
 }
 
-template <class ZeroingEstimator>
-void SingleDOFProfiledSubsystem<ZeroingEstimator>::set_unprofiled_goal(
+template <class ZeroingEstimator, class Profile>
+void SingleDOFProfiledSubsystem<ZeroingEstimator, Profile>::set_unprofiled_goal(
     double unprofiled_goal, double unprofiled_goal_velocity, bool print) {
   this->unprofiled_goal_(0, 0) = unprofiled_goal;
   this->unprofiled_goal_(1, 0) = unprofiled_goal_velocity;
@@ -376,8 +382,8 @@
   CapGoal("unprofiled R", &this->unprofiled_goal_, print);
 }
 
-template <class ZeroingEstimator>
-double SingleDOFProfiledSubsystem<ZeroingEstimator>::UpdateController(
+template <class ZeroingEstimator, class Profile>
+double SingleDOFProfiledSubsystem<ZeroingEstimator, Profile>::UpdateController(
     bool disable) {
   // TODO(austin): What do we want to do with the profile on reset?  Also, we
   // should probably reset R, the offset, the profile, etc.
@@ -418,22 +424,23 @@
   return this->loop_->U(0, 0);
 }
 
-template <class ZeroingEstimator>
-void SingleDOFProfiledSubsystem<ZeroingEstimator>::UpdateObserver(
+template <class ZeroingEstimator, class Profile>
+void SingleDOFProfiledSubsystem<ZeroingEstimator, Profile>::UpdateObserver(
     double voltage) {
   this->loop_->mutable_U(0, 0) = voltage;
   this->loop_->UpdateObserver(this->loop_->U(), this->loop_->plant().dt());
 }
 
-template <class ZeroingEstimator>
-double SingleDOFProfiledSubsystem<ZeroingEstimator>::Update(bool disable) {
+template <class ZeroingEstimator, class Profile>
+double SingleDOFProfiledSubsystem<ZeroingEstimator, Profile>::Update(
+    bool disable) {
   const double voltage = UpdateController(disable);
   UpdateObserver(voltage);
   return voltage;
 }
 
-template <class ZeroingEstimator>
-bool SingleDOFProfiledSubsystem<ZeroingEstimator>::CheckHardLimits() {
+template <class ZeroingEstimator, class Profile>
+bool SingleDOFProfiledSubsystem<ZeroingEstimator, Profile>::CheckHardLimits() {
   // Returns whether hard limits have been exceeded.
 
   if (position() > range_.upper_hard || position() < range_.lower_hard) {
@@ -447,8 +454,8 @@
   return false;
 }
 
-template <class ZeroingEstimator>
-void SingleDOFProfiledSubsystem<ZeroingEstimator>::AdjustProfile(
+template <class ZeroingEstimator, class Profile>
+void SingleDOFProfiledSubsystem<ZeroingEstimator, Profile>::AdjustProfile(
     const ::frc971::ProfileParameters *profile_parameters) {
   AdjustProfile(
       profile_parameters != nullptr ? profile_parameters->max_velocity() : 0.0,
@@ -456,8 +463,8 @@
                                     : 0.0);
 }
 
-template <class ZeroingEstimator>
-void SingleDOFProfiledSubsystem<ZeroingEstimator>::AdjustProfile(
+template <class ZeroingEstimator, class Profile>
+void SingleDOFProfiledSubsystem<ZeroingEstimator, Profile>::AdjustProfile(
     double max_angular_velocity, double max_angular_acceleration) {
   profile_.set_maximum_velocity(
       internal::UseUnlessZero(max_angular_velocity, default_velocity_));
diff --git a/frc971/control_loops/python/angular_system.py b/frc971/control_loops/python/angular_system.py
index 551f3d0..16773fa 100755
--- a/frc971/control_loops/python/angular_system.py
+++ b/frc971/control_loops/python/angular_system.py
@@ -22,7 +22,9 @@
                  kalman_q_voltage,
                  kalman_r_position,
                  radius=None,
-                 dt=0.00505):
+                 dt=0.00505,
+                 enable_voltage_error=True,
+                 delayed_u=0):
         """Constructs an AngularSystemParams object.
 
         Args:
@@ -42,6 +44,8 @@
         self.kalman_r_position = kalman_r_position
         self.radius = radius
         self.dt = dt
+        self.enable_voltage_error = enable_voltage_error
+        self.delayed_u = delayed_u
 
 
 class AngularSystem(control_loop.ControlLoop):
@@ -56,7 +60,8 @@
         self.G = params.G
 
         # Moment of inertia in kg m^2
-        self.J = params.J + self.motor.motor_inertia / (self.G**2.0)
+        self.J_motor = self.motor.motor_inertia / (self.G**2.0)
+        self.J = params.J + self.J_motor
 
         # Control loop time step
         self.dt = params.dt
@@ -182,7 +187,8 @@
         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
+        if params.enable_voltage_error:
+            self.K[0, 2] = 1
 
         self.Kff = numpy.concatenate(
             (self.Kff, numpy.matrix(numpy.zeros((1, 1)))), axis=1)
@@ -225,6 +231,10 @@
     v_goal_plot = []
     x_hat_plot = []
     u_plot = []
+    power_rotor_plot = []
+    power_mechanism_plot = []
+    power_overall_plot = []
+    power_electrical_plot = []
     offset_plot = []
 
     if controller is None:
@@ -273,8 +283,18 @@
         motor_current = (U[0, 0] - plant.X[1, 0] / plant.G /
                          plant.motor.Kv) / plant.motor.resistance
         motor_current_plot.append(motor_current)
-        battery_current = U[0, 0] * motor_current / 12.0
+        battery_current = U[0, 0] * motor_current / vbat
+        power_electrical_plot.append(battery_current * vbat)
         battery_current_plot.append(battery_current)
+
+        # Instantaneous acceleration.
+        X_dot = plant.A_continuous * plant.X + plant.B_continuous * U
+        # Torque = J * alpha (accel).
+        power_rotor_plot.append(X_dot[1, 0] * plant.J_motor * plant.X[1, 0])
+        power_mechanism_plot.append(X_dot[1, 0] * plant.params.J *
+                                    plant.X[1, 0])
+        power_overall_plot.append(X_dot[1, 0] * plant.J * plant.X[1, 0])
+
         x_plot.append(plant.X[0, 0])
 
         if v_plot:
@@ -306,21 +326,34 @@
     glog.debug('goal_error %s', repr(end_goal - goal))
     glog.debug('error %s', repr(observer.X_hat - end_goal))
 
-    pylab.subplot(3, 1, 1)
-    pylab.plot(t_plot, x_plot, label='x')
-    pylab.plot(t_plot, x_hat_plot, label='x_hat')
-    pylab.plot(t_plot, x_goal_plot, label='x_goal')
-    pylab.legend()
+    pylab.suptitle(f'Gear ratio {plant.G}')
+    position_ax1 = pylab.subplot(3, 1, 1)
+    position_ax1.plot(t_plot, x_plot, label='x')
+    position_ax1.plot(t_plot, x_hat_plot, label='x_hat')
+    position_ax1.plot(t_plot, x_goal_plot, label='x_goal')
 
-    pylab.subplot(3, 1, 2)
-    pylab.plot(t_plot, u_plot, label='u')
-    pylab.plot(t_plot, offset_plot, label='voltage_offset')
-    pylab.legend()
+    power_ax2 = position_ax1.twinx()
+    power_ax2.set_xlabel("time(s)")
+    power_ax2.set_ylabel("Power (W)")
+    power_ax2.plot(t_plot, power_rotor_plot, label='Rotor power')
+    power_ax2.plot(t_plot, power_mechanism_plot, label='Mechanism power')
+    power_ax2.plot(t_plot,
+                   power_overall_plot,
+                   label='Overall mechanical power')
+    power_ax2.plot(t_plot, power_electrical_plot, label='Electrical power')
+
+    position_ax1.legend()
+    power_ax2.legend(loc='lower right')
+
+    voltage_ax1 = pylab.subplot(3, 1, 2)
+    voltage_ax1.plot(t_plot, u_plot, label='u')
+    voltage_ax1.plot(t_plot, offset_plot, label='voltage_offset')
+    voltage_ax1.legend()
 
     ax1 = pylab.subplot(3, 1, 3)
     ax1.set_xlabel("time(s)")
     ax1.set_ylabel("rad/s^2")
-    ax1.plot(t_plot, a_plot, label='a')
+    ax1.plot(t_plot, a_plot, label='acceleration')
 
     ax2 = ax1.twinx()
     ax2.set_xlabel("time(s)")
diff --git a/frc971/control_loops/python/constants.py b/frc971/control_loops/python/constants.py
index e4ff6b7..260a6ad 100644
--- a/frc971/control_loops/python/constants.py
+++ b/frc971/control_loops/python/constants.py
@@ -37,8 +37,7 @@
 Robot2021 = Robot2020
 Robot2022 = RobotType(width=0.8763, length=0.96647)
 Robot2023 = RobotType(width=0.6061, length=0.77581)
-#TODO (Nathan): Update 2024 robot dimensions when CAD is done
-Robot2024 = RobotType(width=0.9017, length=0.9525)  # 35.5 in x 37.5 in
+Robot2024 = RobotType(width=0.85979, length=0.9906)  # 33.85 in x 39.0 in
 
 FIELDS = {
     "2019 Field":
diff --git a/frc971/control_loops/python/linear_system.py b/frc971/control_loops/python/linear_system.py
index e28c422..aa8251e 100755
--- a/frc971/control_loops/python/linear_system.py
+++ b/frc971/control_loops/python/linear_system.py
@@ -50,8 +50,9 @@
         self.radius = params.radius
 
         # Mass in kg
-        self.mass = params.mass + self.motor.motor_inertia / (
+        self.mass_motor = self.motor.motor_inertia / (
             (self.G * self.radius)**2.0)
+        self.mass = params.mass + self.mass_motor
 
         # Control loop time step
         self.dt = params.dt
@@ -80,13 +81,22 @@
         controllability = controls.ctrb(self.A, self.B)
         glog.debug('Controllability of %d',
                    numpy.linalg.matrix_rank(controllability))
-        glog.debug('Mass: %f', self.mass)
-        glog.debug('Stall force: %f',
+        glog.debug('Mass: %f kg', self.mass)
+        glog.debug('Stall force: %f N',
                    self.motor.stall_torque / self.G / self.radius)
-        glog.debug('Stall acceleration: %f',
+        glog.debug('Stall acceleration: %f m/s^2',
                    self.motor.stall_torque / self.G / self.radius / self.mass)
 
-        glog.debug('Free speed is %f',
+        holding_force = 500
+
+        holding_current = holding_force * self.radius * self.G / self.motor.Kt
+        glog.debug('Motor current to hold %f N: %f A', holding_force,
+                   holding_current)
+        glog.debug(
+            'Battery current to hold %f N: %f A', holding_force,
+            holding_current * holding_current * self.motor.resistance / 12.0)
+
+        glog.debug('Free speed is %f m/s',
                    -self.B_continuous[1, 0] / self.A_continuous[1, 1] * 12.0)
 
         self.Q = numpy.matrix([[(1.0 / (self.params.q_pos**2.0)), 0.0],
@@ -202,10 +212,16 @@
     x_plot = []
     v_plot = []
     a_plot = []
+    motor_current_plot = []
+    battery_current_plot = []
     x_goal_plot = []
     v_goal_plot = []
     x_hat_plot = []
     u_plot = []
+    power_rotor_plot = []
+    power_mechanism_plot = []
+    power_overall_plot = []
+    power_electrical_plot = []
     offset_plot = []
 
     if controller is None:
@@ -249,6 +265,22 @@
 
         U = U_uncapped.copy()
         U[0, 0] = numpy.clip(U[0, 0], -vbat, vbat)
+
+        motor_current = (U[0, 0] - plant.X[1, 0] / plant.radius / plant.G /
+                         plant.motor.Kv) / plant.motor.resistance
+        motor_current_plot.append(motor_current)
+        battery_current = U[0, 0] * motor_current / vbat
+        power_electrical_plot.append(battery_current * vbat)
+        battery_current_plot.append(battery_current)
+
+        # Instantaneous acceleration.
+        X_dot = plant.A_continuous * plant.X + plant.B_continuous * U
+        # Torque = J * alpha (accel).
+        power_rotor_plot.append(X_dot[1, 0] * plant.mass_motor * plant.X[1, 0])
+        power_mechanism_plot.append(X_dot[1, 0] * plant.params.mass *
+                                    plant.X[1, 0])
+        power_overall_plot.append(X_dot[1, 0] * plant.mass * plant.X[1, 0])
+
         x_plot.append(plant.X[0, 0])
 
         if v_plot:
@@ -280,20 +312,42 @@
     glog.debug('goal_error %s', repr(end_goal - goal))
     glog.debug('error %s', repr(observer.X_hat - end_goal))
 
-    pylab.subplot(3, 1, 1)
-    pylab.plot(t_plot, x_plot, label='x')
-    pylab.plot(t_plot, x_hat_plot, label='x_hat')
-    pylab.plot(t_plot, x_goal_plot, label='x_goal')
-    pylab.legend()
+    pylab.suptitle(f'Gear ratio {plant.G}')
+    position_ax1 = pylab.subplot(3, 1, 1)
+    position_ax1.plot(t_plot, x_plot, label='x')
+    position_ax1.plot(t_plot, x_hat_plot, label='x_hat')
+    position_ax1.plot(t_plot, x_goal_plot, label='x_goal')
 
-    pylab.subplot(3, 1, 2)
-    pylab.plot(t_plot, u_plot, label='u')
-    pylab.plot(t_plot, offset_plot, label='voltage_offset')
-    pylab.legend()
+    power_ax2 = position_ax1.twinx()
+    power_ax2.set_xlabel("time(s)")
+    power_ax2.set_ylabel("Power (W)")
+    power_ax2.plot(t_plot, power_rotor_plot, label='Rotor power')
+    power_ax2.plot(t_plot, power_mechanism_plot, label='Mechanism power')
+    power_ax2.plot(t_plot,
+                   power_overall_plot,
+                   label='Overall mechanical power')
+    power_ax2.plot(t_plot, power_electrical_plot, label='Electrical power')
 
-    pylab.subplot(3, 1, 3)
-    pylab.plot(t_plot, a_plot, label='a')
-    pylab.legend()
+    position_ax1.legend()
+    power_ax2.legend(loc='lower right')
+
+    voltage_ax1 = pylab.subplot(3, 1, 2)
+    voltage_ax1.plot(t_plot, u_plot, label='u')
+    voltage_ax1.plot(t_plot, offset_plot, label='voltage_offset')
+    voltage_ax1.legend()
+
+    ax1 = pylab.subplot(3, 1, 3)
+    ax1.set_xlabel("time(s)")
+    ax1.set_ylabel("m/s^2")
+    ax1.plot(t_plot, a_plot, label='acceleration')
+
+    ax2 = ax1.twinx()
+    ax2.set_xlabel("time(s)")
+    ax2.set_ylabel("Amps")
+    ax2.plot(t_plot, battery_current_plot, 'g', label='battery')
+    ax2.plot(t_plot, motor_current_plot, 'r', label='motor')
+    ax2.legend()
+    ax1.legend(loc='lower left')
 
     pylab.show()
 
@@ -377,7 +431,7 @@
             end_goal=augmented_R,
             controller=controller,
             observer=observer,
-            duration=2.0,
+            duration=1.0,
             use_profile=True,
             max_velocity=max_velocity,
             max_acceleration=max_acceleration)
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 069914e..c084047 100644
--- a/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem.h
+++ b/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem.h
@@ -96,7 +96,8 @@
 // Class for controlling and motion profiling a single degree of freedom
 // subsystem with a zeroing strategy of not moving.
 template <typename TZeroingEstimator, typename TProfiledJointStatus,
-          typename TSubsystemParams = TZeroingEstimator>
+          typename TSubsystemParams = TZeroingEstimator,
+          typename TProfile = aos::util::TrapezoidProfile>
 class StaticZeroingSingleDOFProfiledSubsystem {
  public:
   // Constructs the subsystem from flatbuffer types (appropriate when using the
@@ -147,8 +148,10 @@
 
   // Sets the unprofiled goal which UpdateController will go to.
   void set_unprofiled_goal(double position, double velocity);
-  // Changes the profile parameters for UpdateController to track.
-  void AdjustProfile(double velocity, double acceleration);
+
+  double unprofiled_goal(int row, int col) const {
+    return profiled_subsystem_.unprofiled_goal(row, col);
+  }
 
   // Returns the current position
   double position() const { return profiled_subsystem_.position(); }
@@ -178,6 +181,12 @@
   flatbuffers::Offset<ProfiledJointStatus> MakeStatus(
       flatbuffers::FlatBufferBuilder *status_fbb);
 
+  // Sets whether to use the trapezoidal profiler or whether to just bypass it
+  // and pass the unprofiled goal through directly.
+  void set_enable_profile(bool enable) {
+    profiled_subsystem_.set_enable_profile(enable);
+  }
+
   // Iterates the controller with the provided goal.
   flatbuffers::Offset<ProfiledJointStatus> Iterate(
       const StaticZeroingSingleDOFProfiledSubsystemGoal *goal,
@@ -220,6 +229,9 @@
     return profiled_subsystem_.controller();
   }
 
+  // Returns a pointer to the profile in use.
+  TProfile *mutable_profile() { return profiled_subsystem_.mutable_profile(); }
+
  private:
   State state_ = State::UNINITIALIZED;
   double min_position_, max_position_;
@@ -227,14 +239,15 @@
 
   const StaticZeroingSingleDOFProfiledSubsystemParams<TSubsystemParams> params_;
 
-  ::frc971::control_loops::SingleDOFProfiledSubsystem<ZeroingEstimator>
+  ::frc971::control_loops::SingleDOFProfiledSubsystem<ZeroingEstimator,
+                                                      TProfile>
       profiled_subsystem_;
 };
 
 template <typename ZeroingEstimator, typename ProfiledJointStatus,
-          typename SubsystemParams>
+          typename SubsystemParams, typename Profile>
 StaticZeroingSingleDOFProfiledSubsystem<ZeroingEstimator, ProfiledJointStatus,
-                                        SubsystemParams>::
+                                        SubsystemParams, Profile>::
     StaticZeroingSingleDOFProfiledSubsystem(
         const StaticZeroingSingleDOFProfiledSubsystemParams<SubsystemParams>
             &params)
@@ -251,9 +264,9 @@
 };
 
 template <typename ZeroingEstimator, typename ProfiledJointStatus,
-          typename SubsystemParams>
+          typename SubsystemParams, typename Profile>
 void StaticZeroingSingleDOFProfiledSubsystem<
-    ZeroingEstimator, ProfiledJointStatus, SubsystemParams>::Reset() {
+    ZeroingEstimator, ProfiledJointStatus, SubsystemParams, Profile>::Reset() {
   state_ = State::UNINITIALIZED;
   clear_min_position();
   clear_max_position();
@@ -261,12 +274,12 @@
 }
 
 template <typename ZeroingEstimator, typename ProfiledJointStatus,
-          typename SubsystemParams>
+          typename SubsystemParams, typename Profile>
 bool StaticZeroingSingleDOFProfiledSubsystem<
-    ZeroingEstimator, ProfiledJointStatus, SubsystemParams>::
-    Correct(const StaticZeroingSingleDOFProfiledSubsystemGoal *goal,
-            const typename ZeroingEstimator::Position *position,
-            bool disabled) {
+    ZeroingEstimator, ProfiledJointStatus, SubsystemParams,
+    Profile>::Correct(const StaticZeroingSingleDOFProfiledSubsystemGoal *goal,
+                      const typename ZeroingEstimator::Position *position,
+                      bool disabled) {
   CHECK_NOTNULL(position);
   profiled_subsystem_.Correct(*position);
 
@@ -296,8 +309,9 @@
       // jump.
       profiled_subsystem_.ForceGoal(profiled_subsystem_.position());
       // Set up the profile to be the zeroing profile.
-      profiled_subsystem_.AdjustProfile(
-          params_.zeroing_profile_params.max_velocity,
+      mutable_profile()->set_maximum_velocity(
+          params_.zeroing_profile_params.max_velocity);
+      mutable_profile()->set_maximum_acceleration(
           params_.zeroing_profile_params.max_acceleration);
 
       // We are not ready to start doing anything yet.
@@ -321,11 +335,20 @@
 
       if (goal) {
         if (goal->profile_params()) {
-          AdjustProfile(goal->profile_params()->max_velocity(),
-                        goal->profile_params()->max_acceleration());
+          mutable_profile()->set_maximum_velocity(
+              internal::UseUnlessZero(goal->profile_params()->max_velocity(),
+                                      profiled_subsystem_.default_velocity()));
+          mutable_profile()->set_maximum_acceleration(
+              std::min(static_cast<double>(max_acceleration_),
+                       internal::UseUnlessZero(
+                           goal->profile_params()->max_acceleration(),
+                           profiled_subsystem_.default_acceleration())));
         } else {
-          AdjustProfile(profiled_subsystem_.default_velocity(),
-                        profiled_subsystem_.default_acceleration());
+          mutable_profile()->set_maximum_velocity(
+              profiled_subsystem_.default_velocity());
+          mutable_profile()->set_maximum_acceleration(
+              std::min(static_cast<double>(max_acceleration_),
+                       profiled_subsystem_.default_acceleration()));
         }
 
         if (goal->has_ignore_profile()) {
@@ -352,10 +375,10 @@
 }
 
 template <typename ZeroingEstimator, typename ProfiledJointStatus,
-          typename SubsystemParams>
+          typename SubsystemParams, typename Profile>
 void StaticZeroingSingleDOFProfiledSubsystem<
-    ZeroingEstimator, ProfiledJointStatus,
-    SubsystemParams>::set_unprofiled_goal(double goal, double goal_velocity) {
+    ZeroingEstimator, ProfiledJointStatus, SubsystemParams,
+    Profile>::set_unprofiled_goal(double goal, double goal_velocity) {
   if (goal < min_position_) {
     VLOG(1) << "Limiting to " << min_position_ << " from " << goal;
     goal = min_position_;
@@ -368,22 +391,14 @@
 }
 
 template <typename ZeroingEstimator, typename ProfiledJointStatus,
-          typename SubsystemParams>
-void StaticZeroingSingleDOFProfiledSubsystem<
-    ZeroingEstimator, ProfiledJointStatus,
-    SubsystemParams>::AdjustProfile(double velocity, double acceleration) {
-  profiled_subsystem_.AdjustProfile(
-      velocity, std::min(acceleration, static_cast<double>(max_acceleration_)));
-}
-
-template <typename ZeroingEstimator, typename ProfiledJointStatus,
-          typename SubsystemParams>
+          typename SubsystemParams, typename Profile>
 flatbuffers::Offset<ProfiledJointStatus>
-StaticZeroingSingleDOFProfiledSubsystem<ZeroingEstimator, ProfiledJointStatus,
-                                        SubsystemParams>::
-    Iterate(const StaticZeroingSingleDOFProfiledSubsystemGoal *goal,
-            const typename ZeroingEstimator::Position *position, double *output,
-            flatbuffers::FlatBufferBuilder *status_fbb) {
+StaticZeroingSingleDOFProfiledSubsystem<
+    ZeroingEstimator, ProfiledJointStatus, SubsystemParams,
+    Profile>::Iterate(const StaticZeroingSingleDOFProfiledSubsystemGoal *goal,
+                      const typename ZeroingEstimator::Position *position,
+                      double *output,
+                      flatbuffers::FlatBufferBuilder *status_fbb) {
   const bool disabled = Correct(goal, position, output == nullptr);
 
   // Calculate the loops for a cycle.
@@ -400,35 +415,35 @@
 }
 
 template <typename ZeroingEstimator, typename ProfiledJointStatus,
-          typename SubsystemParams>
+          typename SubsystemParams, typename Profile>
 double StaticZeroingSingleDOFProfiledSubsystem<
-    ZeroingEstimator, ProfiledJointStatus,
-    SubsystemParams>::UpdateController(bool disabled) {
+    ZeroingEstimator, ProfiledJointStatus, SubsystemParams,
+    Profile>::UpdateController(bool disabled) {
   return profiled_subsystem_.UpdateController(disabled);
 }
 
 template <typename ZeroingEstimator, typename ProfiledJointStatus,
-          typename SubsystemParams>
+          typename SubsystemParams, typename Profile>
 void StaticZeroingSingleDOFProfiledSubsystem<
-    ZeroingEstimator, ProfiledJointStatus,
-    SubsystemParams>::UpdateObserver(double voltage) {
+    ZeroingEstimator, ProfiledJointStatus, SubsystemParams,
+    Profile>::UpdateObserver(double voltage) {
   profiled_subsystem_.UpdateObserver(voltage);
 }
 
 template <typename ZeroingEstimator, typename ProfiledJointStatus,
-          typename SubsystemParams>
+          typename SubsystemParams, typename Profile>
 void StaticZeroingSingleDOFProfiledSubsystem<
-    ZeroingEstimator, ProfiledJointStatus,
-    SubsystemParams>::ForceGoal(double goal, double goal_velocity) {
+    ZeroingEstimator, ProfiledJointStatus, SubsystemParams,
+    Profile>::ForceGoal(double goal, double goal_velocity) {
   profiled_subsystem_.ForceGoal(goal, goal_velocity);
 }
 
 template <typename ZeroingEstimator, typename ProfiledJointStatus,
-          typename SubsystemParams>
+          typename SubsystemParams, typename Profile>
 flatbuffers::Offset<ProfiledJointStatus>
 StaticZeroingSingleDOFProfiledSubsystem<
-    ZeroingEstimator, ProfiledJointStatus,
-    SubsystemParams>::MakeStatus(flatbuffers::FlatBufferBuilder *status_fbb) {
+    ZeroingEstimator, ProfiledJointStatus, SubsystemParams,
+    Profile>::MakeStatus(flatbuffers::FlatBufferBuilder *status_fbb) {
   CHECK_NOTNULL(status_fbb);
 
   typename ProfiledJointStatus::Builder status_builder =
diff --git a/frc971/control_loops/subsystem_simulator.h b/frc971/control_loops/subsystem_simulator.h
index 0a23a38..3dc44f7 100644
--- a/frc971/control_loops/subsystem_simulator.h
+++ b/frc971/control_loops/subsystem_simulator.h
@@ -79,6 +79,9 @@
 
   PositionSensorSimulator *encoder() { return &encoder_; }
 
+  double position() const { return plant_->X(0, 0); }
+  double velocity() const { return plant_->X(1, 0); }
+
  private:
   std::unique_ptr<CappedTestPlant> plant_;
   PositionSensorSimulator encoder_;
diff --git a/frc971/imu/BUILD b/frc971/imu/BUILD
index b90f844..3bccf20 100644
--- a/frc971/imu/BUILD
+++ b/frc971/imu/BUILD
@@ -61,3 +61,46 @@
 uf2_from_elf(
     name = "ADIS16505",
 )
+
+cc_library(
+    name = "imu_calibrator",
+    srcs = ["imu_calibrator.cc"],
+    hdrs = [
+        "imu_calibrator.h",
+        "imu_calibrator-tmpl.h",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = [
+        "//aos/time",
+        "//frc971/math:interpolate",
+        "@com_google_absl//absl/strings:str_format",
+        "@com_google_ceres_solver//:ceres",
+        "@org_tuxfamily_eigen//:eigen",
+    ],
+)
+
+cc_library(
+    name = "imu_calibrator_solver",
+    srcs = [
+        "imu_calibrator_solver.cc",
+    ],
+    hdrs = [
+        "imu_calibrator_solver.h",
+    ],
+    deps = [
+        ":imu_calibrator",
+        "@com_google_ceres_solver//:ceres",
+        "@org_tuxfamily_eigen//:eigen",
+    ],
+)
+
+cc_test(
+    name = "imu_calibrator_test",
+    srcs = ["imu_calibrator_test.cc"],
+    shard_count = 3,
+    deps = [
+        ":imu_calibrator",
+        ":imu_calibrator_solver",
+        "//aos/testing:googletest",
+    ],
+)
diff --git a/frc971/imu/imu_calibrator-tmpl.h b/frc971/imu/imu_calibrator-tmpl.h
new file mode 100644
index 0000000..6743106
--- /dev/null
+++ b/frc971/imu/imu_calibrator-tmpl.h
@@ -0,0 +1,248 @@
+#include "frc971/imu/imu_calibrator.h"
+#include "frc971/math/interpolate.h"
+
+DECLARE_int32(imu_zeroing_buffer);
+
+namespace frc971::imu {
+
+inline constexpr double kGravityGs = 1.0;
+// rad / sec
+inline constexpr double kGyroMaxZeroingValue = 0.1;
+
+template <typename Scalar>
+void ImuCalibrator<Scalar>::InsertImu(size_t imu_index,
+                                      const RawImuReading &reading) {
+  CHECK_LT(imu_index, imu_readings_.size());
+  std::vector<ImuReading> &readings = imu_readings_[imu_index];
+  if (readings.size() > 0u) {
+    CHECK_LT(readings.back().capture_time_raw, reading.capture_time)
+        << ": Readings must be inserted in time order per IMU.";
+  }
+  // Execute the stationary logic. We identify if this reading is plausibly
+  // stationary, then if it is not stationary, we go back in time to any
+  // potentially relevant readings and mark them as not stationary. Finally, we
+  // go through and as values exit the FLAGS_imu_zeroing_buffer window we do any
+  // processing that we can given that we know it must be stationary.
+  const bool plausibly_stationary =
+      reading.gyro.squaredNorm() < kGyroMaxZeroingValue * kGyroMaxZeroingValue;
+  bool stationary = plausibly_stationary;
+  int earliest_affected_index = readings.size() - FLAGS_imu_zeroing_buffer;
+  for (size_t index = std::max(0, earliest_affected_index);
+       index < readings.size(); ++index) {
+    if (!plausibly_stationary) {
+      readings[index].stationary = false;
+    }
+    if (!readings[index].plausibly_stationary) {
+      stationary = false;
+    }
+  }
+
+  // Since we don't have data from before the start, assume that we may have
+  // been moving.
+  if (earliest_affected_index < 0) {
+    stationary = false;
+  }
+
+  if (earliest_affected_index >= 0) {
+    ImuReading &earliest_reading = readings[earliest_affected_index];
+    // The stationary flag for this reading can no longer change, so we can
+    // start to do things based on it.
+    earliest_reading.stationary_is_locked = true;
+    if (earliest_reading.stationary) {
+      earliest_reading.parameter_residuals.gravity =
+          earliest_reading.accel.norm() - kGravityGs;
+      earliest_reading.parameter_residuals.gyro_zero = earliest_reading.gyro;
+      LOG(INFO) << earliest_reading.gyro.transpose();
+    }
+  }
+
+  const ImuConfig<Scalar> &config = imu_configs_[imu_index];
+  Scalar capture_time_adjusted =
+      static_cast<Scalar>(aos::time::DurationInSeconds(
+          reading.capture_time.time_since_epoch())) -
+      (config.parameters.has_value() ? config.parameters->time_offset
+                                     : static_cast<Scalar>(0.0));
+
+  imu_readings_[imu_index].emplace_back(
+      reading.capture_time, capture_time_adjusted,
+      reading.gyro - config.dynamic_params.gyro_zero,
+      reading.accel / config.dynamic_params.gravity,
+      DynamicImuParameters<Scalar>{static_cast<Scalar>(0.0),
+                                   Eigen::Matrix<Scalar, 3, 1>::Zero()},
+      plausibly_stationary, stationary, false, std::nullopt, std::nullopt);
+}
+
+template <typename Scalar>
+void ImuCalibrator<Scalar>::EvaluateRelativeResiduals() {
+  for (const auto &readings : imu_readings_) {
+    CHECK_LT(static_cast<size_t>(FLAGS_imu_zeroing_buffer * 2), readings.size())
+        << ": Insufficient readings to perform calibration.";
+  }
+  Scalar base_clock = imu_readings_[origin_index_][0].capture_time_adjusted;
+  // Current index corresponding to the base_clock time.
+  std::vector<size_t> reading_indices(imu_configs_.size(), 0);
+  // The for loops are set up so that we:
+  // 1. Iterate over every pair of readings from the origin/base IMU.
+  // 2. For each other IMU, we identify 0 or 1 readings that fall between those
+  //    two readings of the origin IMU. We then calculate the residuals for
+  //    that IMU relative to the origin IMU, linearly interpolating between
+  //    the pair of readings from (1) (by doing a linear interpolation, we can
+  //    get sub-cycle accuracy on time offsets).
+  for (;
+       reading_indices[origin_index_] < imu_readings_[origin_index_].size() - 1;
+       ++reading_indices[origin_index_]) {
+    const ImuReading &base_reading =
+        imu_readings_[origin_index_][reading_indices[origin_index_]];
+    const ImuReading &next_base_reading =
+        imu_readings_[origin_index_][reading_indices[origin_index_] + 1];
+    base_clock = base_reading.capture_time_adjusted;
+    const Scalar next_base_clock = next_base_reading.capture_time_adjusted;
+    for (size_t imu_index = 0; imu_index < imu_configs_.size(); ++imu_index) {
+      const ImuConfig<Scalar> &imu_config = imu_configs_[imu_index];
+      // We don't care about calculating the offsets from the origin IMU to
+      // itself...
+      if (imu_config.is_origin) {
+        continue;
+      }
+      auto &readings = imu_readings_[imu_index];
+      bool done_with_imu = false;
+      // This will put the index for the current IMU just past the base_clock
+      // timepoint, allowing us to interpolate between
+      // reading_indices[origin_index_] and reading_indices[origin_index_] + 1.
+      while (readings[reading_indices[imu_index]].capture_time_adjusted <
+             base_clock) {
+        if (reading_indices[imu_index] == imu_readings_[imu_index].size() - 1) {
+          done_with_imu = true;
+          break;
+        }
+        ++reading_indices[imu_index];
+      }
+      // If we've run out of readings on this imu, stop doing anything.
+      if (done_with_imu) {
+        continue;
+      }
+      ImuReading &reading = readings[reading_indices[imu_index]];
+      const Scalar reading_time = reading.capture_time_adjusted;
+      if (reading_time >= next_base_clock) {
+        // There is a gap in readings for this imu; we can't meaningfully
+        // populate the residuals.
+        continue;
+      }
+      // Sanity check the above logic.
+      CHECK_LE(base_clock, reading_time);
+      CHECK_LT(reading_time, next_base_clock);
+      CHECK(imu_config.parameters.has_value());
+      reading.gyro_residual =
+          imu_config.parameters.value().rotation * reading.gyro -
+          frc971::math::Interpolate<Eigen::Matrix<Scalar, 3, 1>, Scalar>(
+              base_clock, next_base_clock, base_reading.gyro,
+              next_base_reading.gyro, reading_time);
+      if (!reading.stationary_is_locked || !reading.stationary) {
+        continue;
+      }
+      // In order to calculate the accelerometer residual, we are assuming that
+      // the two IMUs "should" produce identical accelerations. This is only
+      // true when not rotating. Future changes may account for coriolis
+      // effects.
+      reading.accel_residual =
+          imu_config.parameters.value().rotation * reading.accel -
+          frc971::math::Interpolate(base_clock, next_base_clock,
+                                    base_reading.accel, next_base_reading.accel,
+                                    reading_time);
+    }
+  }
+}
+
+// Helpers to accommodate serializing residuals into the ceres buffer. These
+// helpers all return a buffer that points to the next value to be populated.
+namespace internal {
+template <typename Scalar>
+std::span<Scalar> SerializeScalar(Scalar value, std::span<Scalar> out) {
+  DCHECK(!out.empty());
+  out[0] = value;
+  return out.subspan(1);
+}
+template <typename Scalar, int kSize>
+std::span<Scalar> SerializeVector(const Eigen::Matrix<Scalar, kSize, 1> &value,
+                                  std::span<Scalar> out) {
+  DCHECK_LE(static_cast<size_t>(value.size()), out.size());
+  for (int index = 0; index < kSize; ++index) {
+    out[index] = value(index);
+  }
+  return out.subspan(kSize);
+}
+template <typename Scalar>
+std::span<Scalar> SerializeParams(const DynamicImuParameters<Scalar> &params,
+                                  std::span<Scalar> out) {
+  return SerializeVector(params.gyro_zero,
+                         SerializeScalar(params.gravity, out));
+}
+inline constexpr int kResidualsPerReading = 10u;
+}  // namespace internal
+
+template <typename Scalar>
+void ImuCalibrator<Scalar>::CalculateResiduals(std::span<Scalar> residuals) {
+  EvaluateRelativeResiduals();
+  for (size_t imu_index = 0; imu_index < imu_configs_.size(); ++imu_index) {
+    const auto &readings = imu_readings_[imu_index];
+    double valid_gyro_reading_count = 0;
+    double valid_accel_reading_count = 0;
+    for (size_t reading_index = 0; reading_index < readings.size();
+         ++reading_index) {
+      const auto &reading = readings[reading_index];
+      if (reading.gyro_residual.has_value()) {
+        ++valid_gyro_reading_count;
+      }
+      if (reading.accel_residual.has_value()) {
+        ++valid_accel_reading_count;
+      }
+    }
+    if (!imu_configs_[imu_index].is_origin) {
+      CHECK_LT(0, valid_gyro_reading_count);
+      CHECK_LT(0, valid_accel_reading_count);
+    } else {
+      valid_gyro_reading_count = readings.size();
+      valid_accel_reading_count = readings.size();
+    }
+    // Adjust the residuals of the readings to ensure that the solver doesn't
+    // cheat by just making it so that the time-offsets are completely
+    // misaligned and we can say that all the residuals are "zero".
+    const Scalar gyro_reading_scalar =
+        static_cast<Scalar>(readings.size() / valid_gyro_reading_count);
+    const Scalar accel_reading_scalar =
+        static_cast<Scalar>(readings.size() / valid_accel_reading_count);
+    for (size_t reading_index = 0; reading_index < readings.size();
+         ++reading_index) {
+      const auto &reading = readings[reading_index];
+      const Scalar *const start_residual = residuals.data();
+      // 4 residuals (gravity scalar; gyro zeroes)
+      residuals =
+          internal::SerializeParams(reading.parameter_residuals, residuals);
+      const Eigen::Matrix<Scalar, 3, 1> gyro_residual =
+          reading.gyro_residual.value_or(Eigen::Matrix<Scalar, 3, 1>::Zero()) *
+          gyro_reading_scalar;
+      // 3 residuals
+      residuals = internal::SerializeVector(gyro_residual, residuals);
+      const Eigen::Matrix<Scalar, 3, 1> accel_residual =
+          reading.accel_residual.value_or(Eigen::Matrix<Scalar, 3, 1>::Zero()) *
+          accel_reading_scalar;
+      // 3 residuals
+      residuals = internal::SerializeVector(accel_residual, residuals);
+      CHECK_EQ(internal::kResidualsPerReading,
+               residuals.data() - start_residual)
+          << ": Need to update kResidualsPerReading.";
+    }
+  }
+}
+
+template <typename Scalar>
+size_t ImuCalibrator<Scalar>::CalculateNumResiduals(
+    const std::vector<size_t> &num_readings) {
+  size_t num_residuals = 0;
+  for (const size_t count : num_readings) {
+    num_residuals += internal::kResidualsPerReading * count;
+  }
+  return num_residuals;
+}
+
+}  // namespace frc971::imu
diff --git a/frc971/imu/imu_calibrator.cc b/frc971/imu/imu_calibrator.cc
new file mode 100644
index 0000000..03fa377
--- /dev/null
+++ b/frc971/imu/imu_calibrator.cc
@@ -0,0 +1,6 @@
+#include "frc971/imu/imu_calibrator.h"
+
+DEFINE_int32(
+    imu_zeroing_buffer, 100,
+    "We will only consider readings stationary for purposes if calibration if "
+    "this many readings to either side appear to be stationary.");
diff --git a/frc971/imu/imu_calibrator.h b/frc971/imu/imu_calibrator.h
new file mode 100644
index 0000000..be4c4d4
--- /dev/null
+++ b/frc971/imu/imu_calibrator.h
@@ -0,0 +1,331 @@
+#ifndef FRC971_IMU_IMU_CALIBRATOR_H_
+#define FRC971_IMU_IMU_CALIBRATOR_H_
+
+#include <optional>
+#include <span>
+#include <tuple>
+#include <vector>
+
+#include "absl/strings/str_format.h"
+#include "absl/strings/str_join.h"
+#include "ceres/ceres.h"
+#include "glog/logging.h"
+#include <Eigen/Core>
+#include <Eigen/Geometry>
+
+#include "aos/time/time.h"
+
+namespace frc971::imu {
+
+// Contains a reading that comes directly from an IMU.
+// These should not be zeroed or corrected for yet.
+struct RawImuReading {
+  aos::monotonic_clock::time_point capture_time;
+  // gyro readings are in radians / sec; accelerometer readings are in g's.
+  const Eigen::Vector3d gyro;
+  const Eigen::Vector3d accel;
+};
+
+// Note on how we manage populating ceres parameters:
+// * We use dynamically-sized parameter lists for convenience.
+// * For every struct that we have that corresponds to problem parameters,
+//   there is a PopulateParameters() method that takes a:
+//   * ceres::Problem* that is used for setting parameter constraints.
+//   * ceres::DynamicCostFunction* that is used to mark that we have added
+//     a parameter block.
+//   * A std::vector<double*> that will later be passed to AddResidualBlock.
+//     We add any parameter blocks which we add to the problem.
+//   * post_populate_methods is a list of std::function's that will get called
+//     after everything is populated and added (this is necessary because
+//     we cannot add constraints to the Problem until after everything
+//     has been added).
+// * Additionally, there is a PopulateFromParameters() method which is used
+//   in the ceres cost functor and:
+//   * Takes a parameters double-pointer where parameters[X] is the
+//     parameter block X.
+//   * Returns a pair where the first value is a populated instance of the
+//     struct and an updated parameters pointer which points to the next
+//     set of pointers.
+// * All the Populate* methods must be called in the same order every
+//   time so that they result in the raw pointers getting populated
+//   consistently.
+
+// These are the parameters corresponding to things which will vary at every
+// power-on of the IMU.
+template <typename Scalar>
+struct DynamicImuParameters {
+  // A scalar to represent the current magnitude of gravity, in g's.
+  // This currently compensates both for any variations in local gravity as well
+  // as for some amount of variations in the IMU itself. In the future we may
+  // expand this to have a local gravity number that is global to all IMUs while
+  // separately calibrating per-axis information.
+  Scalar gravity;
+  // Current gyro zero, in radians / sec.
+  // These are the per-axis values that the gyro will read when it is sitting
+  // still. Technically these zeroes will drift over time; however, the
+  // time-windows that we expect to use for calibration are short enough that
+  // this should be a non-issue.
+  Eigen::Matrix<Scalar, 3, 1> gyro_zero;
+  void PopulateParameters(
+      ceres::Problem *problem, ceres::DynamicCostFunction *cost_function,
+      std::vector<double *> *parameters,
+      std::vector<std::function<void()>> *post_populate_methods) {
+    cost_function->AddParameterBlock(1);
+    parameters->push_back(&gravity);
+    cost_function->AddParameterBlock(3);
+    parameters->push_back(gyro_zero.data());
+    post_populate_methods->emplace_back([this, problem]() {
+      // Gravity shouldn't vary by much (these bounds are significantly larger
+      // than any real variations which we will experience).
+      problem->SetParameterLowerBound(&gravity, 0, 0.95);
+      problem->SetParameterUpperBound(&gravity, 0, 1.05);
+      for (int i = 0; i < 3; ++i) {
+        problem->SetParameterLowerBound(gyro_zero.data(), i, -0.05);
+        problem->SetParameterUpperBound(gyro_zero.data(), i, 0.05);
+      }
+    });
+  }
+  static std::tuple<DynamicImuParameters<Scalar>, const Scalar *const *>
+  PopulateFromParameters(const Scalar *const *parameters) {
+    const Scalar *const gravity = parameters[0];
+    ++parameters;
+    const Scalar *const gyro = parameters[0];
+    ++parameters;
+    return std::make_tuple(
+        DynamicImuParameters<Scalar>{
+            *gravity, Eigen::Matrix<Scalar, 3, 1>(gyro[0], gyro[1], gyro[2])},
+        parameters);
+  }
+  std::string ToString() const {
+    std::stringstream out;
+    out << "gravity: " << gravity << " gyro_zero: " << gyro_zero.transpose();
+    return out.str();
+  }
+};
+
+// These parameters correspond to IMU parameters which will not vary between
+// boots. Namely, the relative positions and time offsets of the IMU(s).
+template <typename Scalar>
+struct StaticImuParameters {
+  // If you have a point p_imu in this IMU's frame then (rotation *
+  // p_imu + position) will give you that point's position in the board frame
+  // (the "board" frame refers to the frame associated with the entire PCB,
+  // where the PCB itself contains multiple IMUs. The board frame will be
+  // attached to whichever IMU is being treated as the origin).
+  Eigen::Quaternion<Scalar> rotation;
+  // position is currently unused because it is only observeable when there
+  // are coriolis effects, and we currently only make use of accelerometer
+  // readings from when the IMU is sat still.
+  // As such, we currently assume that all position offsets are zero.
+  // Eigen::Matrix<Scalar, 3, 1> position;
+  // The "true" time at which the event occurred is equal to the capture time -
+  // time_offset. I.e., a more positive time offset implies that the there is a
+  // large delay between the imu readings being taken and us observing them.
+  Scalar time_offset;
+
+  void PopulateParameters(
+      ceres::EigenQuaternionParameterization *quaternion_local_parameterization,
+      ceres::Problem *problem, ceres::DynamicCostFunction *cost_function,
+      std::vector<double *> *parameters,
+      std::vector<std::function<void()>> *post_populate_methods) {
+    cost_function->AddParameterBlock(4);
+    parameters->push_back(rotation.coeffs().data());
+    cost_function->AddParameterBlock(1);
+    parameters->push_back(&time_offset);
+    post_populate_methods->emplace_back(
+        [this, problem, quaternion_local_parameterization]() {
+          problem->SetParameterization(rotation.coeffs().data(),
+                                       quaternion_local_parameterization);
+          problem->SetParameterLowerBound(&time_offset, 0, -0.03);
+          problem->SetParameterUpperBound(&time_offset, 0, 0.03);
+        });
+  }
+  static std::tuple<StaticImuParameters, const Scalar *const *>
+  PopulateFromParameters(const Scalar *const *parameters) {
+    const Scalar *const quat = parameters[0];
+    ++parameters;
+    const Scalar *const time = parameters[0];
+    ++parameters;
+    return std::make_tuple(
+        StaticImuParameters{
+            Eigen::Quaternion<Scalar>(quat[3], quat[0], quat[1], quat[2]),
+            *time},
+        parameters);
+  }
+  std::string ToString() const {
+    std::stringstream out;
+    out << "quat: " << rotation.coeffs().transpose()
+        << " time_offset: " << time_offset;
+    return out.str();
+  }
+};
+
+// Represents the calibration for a single IMU.
+template <typename Scalar>
+struct ImuConfig {
+  // Set to true if this IMU is to be considered the origin of the coordinate
+  // system. This will also mean that this IMU is treated as the source-of-truth
+  // for clock offsets.
+  bool is_origin;
+  // Will be nullopt if is_origin is true (the corresponding rotations and
+  // offsets will all be the identity/zero as appropriate).
+  std::optional<StaticImuParameters<Scalar>> parameters;
+
+  DynamicImuParameters<Scalar> dynamic_params{
+      .gravity = static_cast<Scalar>(1.0),
+      .gyro_zero = Eigen::Matrix<Scalar, 3, 1>::Zero()};
+  std::string ToString() const {
+    return absl::StrFormat(
+        "is_origin: %d params: %s dynamic: %s", is_origin,
+        parameters.has_value() ? parameters->ToString() : std::string("<null>"),
+        dynamic_params.ToString());
+  }
+};
+
+// Represents all of the configuration parameters for the entire system.
+template <typename Scalar>
+struct AllParameters {
+  // Each entry in the imus vector will be a single imu.
+  std::vector<ImuConfig<Scalar>> imus;
+  std::tuple<std::vector<double *>, std::vector<std::function<void()>>>
+  PopulateParameters(
+      ceres::EigenQuaternionParameterization *quaternion_local_parameterization,
+      ceres::Problem *problem, ceres::DynamicCostFunction *cost_function) {
+    std::vector<std::function<void()>> post_populate_methods;
+    std::vector<double *> parameters;
+    for (auto &imu : imus) {
+      if (imu.parameters.has_value()) {
+        imu.parameters.value().PopulateParameters(
+            quaternion_local_parameterization, problem, cost_function,
+            &parameters, &post_populate_methods);
+      }
+      imu.dynamic_params.PopulateParameters(problem, cost_function, &parameters,
+                                            &post_populate_methods);
+    }
+    return std::make_tuple(parameters, post_populate_methods);
+  }
+  static AllParameters PopulateFromParameters(
+      const std::vector<ImuConfig<double>> &nominal_configs,
+      const Scalar *const *parameters) {
+    std::vector<ImuConfig<Scalar>> result;
+    for (size_t imu_index = 0; imu_index < nominal_configs.size();
+         ++imu_index) {
+      ImuConfig<Scalar> config;
+      config.is_origin = nominal_configs[imu_index].is_origin;
+      if (!config.is_origin) {
+        std::tie(config.parameters, parameters) =
+            StaticImuParameters<Scalar>::PopulateFromParameters(parameters);
+      }
+      std::tie(config.dynamic_params, parameters) =
+          DynamicImuParameters<Scalar>::PopulateFromParameters(parameters);
+      result.emplace_back(std::move(config));
+    }
+    return {.imus = std::move(result)};
+  }
+  std::string ToString() const {
+    std::vector<std::string> imu_strings;
+    for (const auto &imu : imus) {
+      std::vector<std::string> dynamic_params;
+      imu_strings.push_back(absl::StrFormat("config: %s", imu.ToString()));
+    }
+    return absl::StrJoin(imu_strings, "\n");
+  }
+};
+
+// This class does the hard work to support calibrating multiple IMU's
+// orientations relative to one another. It is set up to readily be used with a
+// ceres solver (see imu_calibrator.*).
+//
+// The current theory of operation is to have some number of imus, one of which
+// we will consider to be fixed in position. We have a fixed set of data that we
+// feed into this class, which can be parameterized based on:
+// * The current zeroes for each IMU.
+// * The orientation of each non-fixed IMU.
+// * The time-offset of each non-fixed IMU.
+//
+// When run under ceres, ceres can then vary these parameters to solve for the
+// current calibrations of the IMUs.
+//
+// When solving, the main complexity is that some internal state has to be
+// tracked to try to determine when we should be calculating zeroes and when we
+// can calibrate out the magnitude of gravity. This is done by tracking when the
+// IMUs are "stationary". For a reading to be stationary, all values with
+// FLAGS_imu_zeroing_buffer readings of the reading must be "plausibly
+// stationary". Readings are plausibly stationary if they have sufficiently low
+// gyro values.
+//
+// TODO: Provide some utilities to plot the results of a calibration.
+template <typename Scalar>
+class ImuCalibrator {
+ public:
+  ImuCalibrator(const std::vector<ImuConfig<Scalar>> &imu_configs)
+      : imu_configs_(imu_configs), imu_readings_(imu_configs.size()) {
+    origin_index_ = -1;
+    for (size_t imu_index = 0; imu_index < imu_configs_.size(); ++imu_index) {
+      if (imu_configs_[imu_index].is_origin) {
+        CHECK_EQ(origin_index_, -1)
+            << ": Can't have more than one IMU specified as the origin.";
+        origin_index_ = imu_index;
+      }
+    }
+    CHECK_NE(origin_index_, -1)
+        << ": Must have at least one IMU specified as the origin.";
+  }
+
+  // These gyro readings will be "raw"---i.e., they still need to get
+  // transformed by nominal_transform.
+  // gyro readings are in radians / sec; accelerometer readings are in g's.
+  // Within a given imu, must be called in time order.
+  void InsertImu(size_t imu_index, const RawImuReading &reading);
+
+  // Populates all the residuals that we use for the cost in ceres.
+  void CalculateResiduals(std::span<Scalar> residuals);
+
+  // Returns the total number of residuals that this problem will have, given
+  // the number of readings for each IMU.
+  static size_t CalculateNumResiduals(const std::vector<size_t> &num_readings);
+
+ private:
+  // Represents an imu reading. The values in this are generally already
+  // adjusted for the provided parameters.
+  struct ImuReading {
+    // The "actual" provided capture time. Used for debugging.
+    aos::monotonic_clock::time_point capture_time_raw;
+    // The capture time, adjusted for this IMU's time offset.
+    Scalar capture_time_adjusted;
+    // gyro reading, adjusted for gyro zero but NOT rotation.
+    // In radians / sec.
+    Eigen::Matrix<Scalar, 3, 1> gyro;
+    // accelerometer reading, adjusted for gravity value but NOT rotation.
+    // In g's.
+    Eigen::Matrix<Scalar, 3, 1> accel;
+    // Residuals associated with the DynamicImuParameters for this imu.
+    DynamicImuParameters<Scalar> parameter_residuals;
+    // Set if this measurement *could* be part of a segment of time where the
+    // IMU is stationary.
+    bool plausibly_stationary;
+    // Set to true if all values with FLAGS_imu_zeroing_buffer of this reading
+    // are plausibly_stationary.
+    bool stationary;
+    // We set stationary_is_locked once we have enough readings to either side
+    // of this value to guarantee that it is stationary.
+    bool stationary_is_locked;
+    // Residuals that are used for calibrating the rotation values. These are
+    // nullopt if we can't calibrate for some reason (e.g., this is the origin
+    // IMU, or for the accelerometer residual, we don't populate it if we are
+    // not stationary).
+    std::optional<Eigen::Matrix<Scalar, 3, 1>> gyro_residual;
+    std::optional<Eigen::Matrix<Scalar, 3, 1>> accel_residual;
+  };
+  void EvaluateRelativeResiduals();
+
+  const std::vector<ImuConfig<Scalar>> imu_configs_;
+  // Index of the IMU which is the origin IMU.
+  int origin_index_;
+  std::vector<std::vector<ImuReading>> imu_readings_;
+};
+
+}  // namespace frc971::imu
+
+#include "frc971/imu/imu_calibrator-tmpl.h"
+#endif  // FRC971_IMU_IMU_CALIBRATOR_H_
diff --git a/frc971/imu/imu_calibrator_solver.cc b/frc971/imu/imu_calibrator_solver.cc
new file mode 100644
index 0000000..19b82e5
--- /dev/null
+++ b/frc971/imu/imu_calibrator_solver.cc
@@ -0,0 +1,89 @@
+#include "frc971/imu/imu_calibrator_solver.h"
+namespace frc971::imu {
+
+struct ImuCalibratorCostFunctor {
+  ImuCalibratorCostFunctor(
+      const std::vector<std::vector<RawImuReading>> &readings,
+      const std::vector<ImuConfig<double>> &nominal_config,
+      const size_t num_residuals)
+      : readings_(readings),
+        nominal_config_(nominal_config),
+        num_residuals_(num_residuals) {}
+  template <typename S>
+  bool operator()(const S *const *const parameters_ptr, S *residual) const {
+    AllParameters<S> params = AllParameters<S>::PopulateFromParameters(
+        nominal_config_, parameters_ptr);
+    std::vector<ImuConfig<S>> imu_configs;
+    for (const auto &param : params.imus) {
+      imu_configs.push_back(param);
+    }
+    ImuCalibrator<S> calibrator(imu_configs);
+    for (size_t imu_index = 0; imu_index < readings_.size(); ++imu_index) {
+      const auto imu_readings = readings_[imu_index];
+      for (size_t reading_index = 0; reading_index < imu_readings.size();
+           ++reading_index) {
+        calibrator.InsertImu(imu_index, imu_readings[reading_index]);
+      }
+    }
+    calibrator.CalculateResiduals({residual, num_residuals_});
+    return true;
+  }
+  const std::vector<std::vector<RawImuReading>> readings_;
+  const std::vector<ImuConfig<double>> nominal_config_;
+  const size_t num_residuals_;
+};
+
+AllParameters<double> Solve(
+    const std::vector<std::vector<RawImuReading>> &readings,
+    const std::vector<ImuConfig<double>> &nominal_config) {
+  ceres::Problem problem;
+
+  ceres::EigenQuaternionParameterization *quaternion_local_parameterization =
+      new ceres::EigenQuaternionParameterization();
+  AllParameters<double> parameters;
+  std::vector<size_t> num_readings;
+  CHECK_EQ(nominal_config.size(), readings.size());
+  for (size_t imu_index = 0; imu_index < nominal_config.size(); ++imu_index) {
+    const size_t num_params = readings[imu_index].size();
+    parameters.imus.emplace_back(nominal_config[imu_index]);
+    num_readings.push_back(num_params);
+  }
+  // Set up the only cost function (also known as residual). This uses
+  // auto-differentiation to obtain the derivative (jacobian).
+
+  {
+    const size_t num_residuals =
+        ImuCalibrator<double>::CalculateNumResiduals(num_readings);
+    ceres::DynamicCostFunction *cost_function =
+        new ceres::DynamicAutoDiffCostFunction<ImuCalibratorCostFunctor>(
+            new ImuCalibratorCostFunctor(readings, nominal_config,
+                                         num_residuals));
+
+    auto [vector_parameters, post_populate_methods] =
+        parameters.PopulateParameters(quaternion_local_parameterization,
+                                      &problem, cost_function);
+
+    cost_function->SetNumResiduals(num_residuals);
+
+    problem.AddResidualBlock(cost_function, new ceres::HuberLoss(1.0),
+                             vector_parameters);
+    for (auto &method : post_populate_methods) {
+      method();
+    }
+  }
+
+  // Run the solver!
+  ceres::Solver::Options options;
+  options.minimizer_progress_to_stdout = true;
+  options.gradient_tolerance = 1e-12;
+  options.function_tolerance = 1e-6;
+  options.parameter_tolerance = 1e-6;
+  ceres::Solver::Summary summary;
+  Solve(options, &problem, &summary);
+  LOG(INFO) << summary.FullReport();
+  LOG(INFO) << "Solution is " << (summary.IsSolutionUsable() ? "" : "NOT ")
+            << "usable";
+  LOG(INFO) << "Solution:\n" << parameters.ToString();
+  return parameters;
+}
+}  // namespace frc971::imu
diff --git a/frc971/imu/imu_calibrator_solver.h b/frc971/imu/imu_calibrator_solver.h
new file mode 100644
index 0000000..148fcc4
--- /dev/null
+++ b/frc971/imu/imu_calibrator_solver.h
@@ -0,0 +1,14 @@
+#ifndef FRC971_IMU_IMU_CALIBRATOR_SOLVER_H_
+#define FRC971_IMU_IMU_CALIBRATOR_SOLVER_H_
+
+#include "frc971/imu/imu_calibrator.h"
+
+namespace frc971::imu {
+
+// Stores all the IMU data from a log so that we can feed it into the
+// ImuCalibrator readily.
+AllParameters<double> Solve(
+    const std::vector<std::vector<RawImuReading>> &readings,
+    const std::vector<ImuConfig<double>> &nominal_config);
+}  // namespace frc971::imu
+#endif  // FRC971_IMU_IMU_CALIBRATOR_SOLVER_H_
diff --git a/frc971/imu/imu_calibrator_test.cc b/frc971/imu/imu_calibrator_test.cc
new file mode 100644
index 0000000..a341b2a
--- /dev/null
+++ b/frc971/imu/imu_calibrator_test.cc
@@ -0,0 +1,215 @@
+#include "frc971/imu/imu_calibrator.h"
+
+#include <random>
+
+#include "gtest/gtest.h"
+
+#include "frc971/imu/imu_calibrator_solver.h"
+
+namespace frc971::imu::testing {
+class ImuSimulator {
+ public:
+  ImuSimulator(const std::vector<ImuConfig<double>> &imu_configs)
+      : imu_configs_(imu_configs), readings_(imu_configs.size()) {}
+  void SimulateForTime(aos::monotonic_clock::duration duration,
+                       aos::monotonic_clock::duration dt,
+                       const Eigen::Vector3d &gravity_vector,
+                       const Eigen::Vector3d &accel,
+                       const Eigen::Vector3d &gyro) {
+    for (const aos::monotonic_clock::time_point end_time = now + duration;
+         now < end_time; now += dt) {
+      for (size_t imu_index = 0; imu_index < imu_configs_.size(); ++imu_index) {
+        const ImuConfig<double> &config = imu_configs_[imu_index];
+        const Eigen::Quaterniond rotation =
+            config.is_origin ? Eigen::Quaterniond::Identity()
+                             : config.parameters->rotation.inverse();
+        const std::chrono::nanoseconds time_offset{
+            config.is_origin
+                ? 0
+                : static_cast<uint64_t>(config.parameters->time_offset * 1e9)};
+        readings_[imu_index].emplace_back(
+            now + time_offset,
+            rotation * gyro + config.dynamic_params.gyro_zero + GyroNoise(),
+            rotation *
+                    (accel + gravity_vector * config.dynamic_params.gravity) +
+                AccelNoise());
+      }
+    }
+  }
+  Eigen::Vector3d GyroNoise() {
+    return (enable_noise_ ? 1.0 : 0.0) *
+           Eigen::Vector3d(distribution_(generator_), distribution_(generator_),
+                           distribution_(generator_));
+  }
+  Eigen::Vector3d AccelNoise() { return GyroNoise() * 2.0; }
+  void set_enable_noise(bool enable) { enable_noise_ = enable; }
+  std::vector<std::vector<RawImuReading>> readings() const {
+    return readings_;
+  };
+
+ private:
+  const std::vector<ImuConfig<double>> imu_configs_;
+  std::vector<std::vector<RawImuReading>> readings_;
+  aos::monotonic_clock::time_point now = aos::monotonic_clock::epoch();
+
+  std::mt19937 generator_;
+  std::normal_distribution<> distribution_{0.0, 0.00025};
+  bool enable_noise_ = false;
+};
+
+namespace {
+void VerifyParameters(const std::vector<ImuConfig<double>> &imus,
+                      const AllParameters<double> &params, double eps = 1e-8) {
+  ASSERT_EQ(imus.size(), params.imus.size());
+  for (size_t imu_index = 0; imu_index < imus.size(); ++imu_index) {
+    SCOPED_TRACE(imu_index);
+    const ImuConfig<double> &expected = imus[imu_index];
+    const ImuConfig<double> &calculated = params.imus[imu_index];
+    EXPECT_EQ(expected.parameters.has_value(),
+              calculated.parameters.has_value());
+    EXPECT_NEAR(expected.dynamic_params.gravity,
+                calculated.dynamic_params.gravity, eps);
+    EXPECT_LT((expected.dynamic_params.gyro_zero -
+               calculated.dynamic_params.gyro_zero)
+                  .norm(),
+              eps)
+        << expected.dynamic_params.gyro_zero.transpose() << " vs. "
+        << calculated.dynamic_params.gyro_zero.transpose();
+    if (expected.parameters.has_value()) {
+      EXPECT_NEAR(expected.parameters->time_offset,
+                  calculated.parameters->time_offset, eps);
+      EXPECT_LT(((expected.parameters->rotation *
+                  calculated.parameters->rotation.inverse())
+                     .coeffs() -
+                 Eigen::Quaterniond::Identity().coeffs())
+                    .norm(),
+                eps)
+          << expected.parameters->rotation.coeffs().transpose() << " vs. "
+          << calculated.parameters->rotation.coeffs().transpose();
+    }
+  }
+}
+}  // namespace
+
+// Confirms that we can calibrate in a relatively simple scenario where we have
+// some gyro/accelerometer offsets and a small rotation that is not accounted
+// for in the nominal parameters.
+TEST(ImuCalibratorTest, BasicCalibrationTest) {
+  std::vector<ImuConfig<double>> nominal_imus = {
+      ImuConfig<double>{true, std::nullopt},
+      ImuConfig<double>{false, std::make_optional<StaticImuParameters<double>>(
+                                   Eigen::Quaterniond::Identity(), 0.0)}};
+
+  std::vector<ImuConfig<double>> real_imus = nominal_imus;
+  real_imus[0].dynamic_params.gravity = 1.005;
+  real_imus[0].dynamic_params.gyro_zero << 0.001, 0.002, 0.003;
+  real_imus[1].dynamic_params.gyro_zero << -0.009, -0.007, -0.001;
+  real_imus[1].parameters->rotation =
+      Eigen::AngleAxisd(0.01, Eigen::Vector3d::UnitZ());
+  ImuSimulator simulator(real_imus);
+  simulator.SimulateForTime(std::chrono::seconds(1),
+                            std::chrono::milliseconds(1),
+                            Eigen::Vector3d(0, 0, 1), Eigen::Vector3d(0, 0, 0),
+                            Eigen::Vector3d(0.0, 0.0, 0.0));
+  simulator.SimulateForTime(std::chrono::seconds(1),
+                            std::chrono::milliseconds(1),
+                            Eigen::Vector3d(0, 1, 0), Eigen::Vector3d(0, 0, 0),
+                            Eigen::Vector3d(0.0, 0.0, 0.0));
+  simulator.SimulateForTime(std::chrono::seconds(1),
+                            std::chrono::milliseconds(1),
+                            Eigen::Vector3d(1, 0, 0), Eigen::Vector3d(0, 0, 0),
+                            Eigen::Vector3d(0.0, 0.0, 0.0));
+  simulator.SimulateForTime(
+      std::chrono::seconds(1), std::chrono::milliseconds(1),
+      Eigen::Vector3d(0, 0, 1), Eigen::Vector3d(0.1, 0.2, 0.3),
+      Eigen::Vector3d(1.0, 0.0, 0.0));
+  simulator.SimulateForTime(
+      std::chrono::seconds(1), std::chrono::milliseconds(1),
+      Eigen::Vector3d(0, 0, 1), Eigen::Vector3d(0.1, 0.2, 0.3),
+      Eigen::Vector3d(0.0, 1.0, 0.0));
+  auto params = Solve(simulator.readings(), nominal_imus);
+  LOG(INFO) << params.ToString();
+  LOG(INFO) << real_imus[0].ToString();
+  LOG(INFO) << real_imus[1].ToString();
+  VerifyParameters(real_imus, params);
+}
+
+// Separately test the estimation of the time offset between IMUs.
+// This is done separately because the optimization problem is poorly condition
+// for estimating time offsets when there is just a handful of step changes in
+// the IMU inputs; feeding in a sine wave works much better for allowing the
+// solver to estimate the offset.
+TEST(ImuCalibratorTest, TimeOffsetTest) {
+  gflags::FlagSaver flag_saver;
+
+  std::vector<ImuConfig<double>> nominal_imus = {
+      ImuConfig<double>{true, std::nullopt},
+      ImuConfig<double>{false, std::make_optional<StaticImuParameters<double>>(
+                                   Eigen::Quaterniond::Identity(), 0.0)}};
+
+  std::vector<ImuConfig<double>> real_imus = nominal_imus;
+  real_imus[1].parameters->time_offset = 0.0255;
+  ImuSimulator simulator(real_imus);
+  // Note on convergence: It is easy to end up in situations where the problem
+  // is not outstandingly well conditioned and we can end up with local minima
+  // where changes to physical calibration attributes can explain the time
+  // offset.
+  simulator.SimulateForTime(std::chrono::seconds(1),
+                            std::chrono::milliseconds(1),
+                            Eigen::Vector3d(0, 0, 1), Eigen::Vector3d(0, 0, 0),
+                            Eigen::Vector3d(0.0, 0.0, 0.0));
+  for (size_t ii = 0; ii < 10000; ++ii) {
+    simulator.SimulateForTime(
+        std::chrono::milliseconds(1), std::chrono::milliseconds(1),
+        Eigen::Vector3d(0, 0, 1), Eigen::Vector3d(0.0, 0.0, 0.0),
+        Eigen::Vector3d(std::sin(ii / 1000.0), 0.0, 0.0));
+  }
+  auto params = Solve(simulator.readings(), nominal_imus);
+  LOG(INFO) << params.ToString();
+  LOG(INFO) << real_imus[0].ToString();
+  LOG(INFO) << real_imus[1].ToString();
+  VerifyParameters(real_imus, params, 1e-6);
+}
+
+// Test that if we add in some random noise that the solver still behaves
+// itself.
+TEST(ImuCalibratorTest, RandomNoise) {
+  std::vector<ImuConfig<double>> nominal_imus = {
+      ImuConfig<double>{true, std::nullopt},
+      ImuConfig<double>{false, std::make_optional<StaticImuParameters<double>>(
+                                   Eigen::Quaterniond::Identity(), 0.0)}};
+
+  std::vector<ImuConfig<double>> real_imus = nominal_imus;
+  real_imus[0].dynamic_params.gravity = 0.999;
+  real_imus[0].dynamic_params.gyro_zero << 0.001, 0.002, 0.003;
+  real_imus[1].dynamic_params.gyro_zero << -0.009, -0.007, -0.001;
+  real_imus[1].parameters->rotation =
+      Eigen::AngleAxisd(0.01, Eigen::Vector3d::UnitZ());
+  ImuSimulator simulator(real_imus);
+  simulator.set_enable_noise(true);
+  simulator.SimulateForTime(std::chrono::seconds(1),
+                            std::chrono::milliseconds(1),
+                            Eigen::Vector3d(0, 0, 1), Eigen::Vector3d(0, 0, 0),
+                            Eigen::Vector3d(0.0, 0.0, 0.0));
+  simulator.SimulateForTime(std::chrono::seconds(1),
+                            std::chrono::milliseconds(1),
+                            Eigen::Vector3d(0, 1, 0), Eigen::Vector3d(0, 0, 0),
+                            Eigen::Vector3d(0.0, 0.0, 0.0));
+  simulator.SimulateForTime(std::chrono::seconds(1),
+                            std::chrono::milliseconds(1),
+                            Eigen::Vector3d(1, 0, 0), Eigen::Vector3d(0, 0, 0),
+                            Eigen::Vector3d(0.0, 0.0, 0.0));
+  for (size_t ii = 0; ii < 6000; ++ii) {
+    simulator.SimulateForTime(std::chrono::milliseconds(1),
+                              std::chrono::milliseconds(1),
+                              Eigen::Vector3d(0, 0, 1),
+                              Eigen::Vector3d(std::sin(ii / 1000.0), 0.0, 0.0),
+                              Eigen::Vector3d(1.0, 0.0, 0.0));
+  }
+  auto params = Solve(simulator.readings(), nominal_imus);
+  LOG(INFO) << params.ToString();
+  LOG(INFO) << real_imus[0].ToString();
+  LOG(INFO) << real_imus[1].ToString();
+  VerifyParameters(real_imus, params, 1e-4);
+}
+}  // namespace frc971::imu::testing
diff --git a/frc971/imu_fdcan/BUILD b/frc971/imu_fdcan/BUILD
index deb6a7e..c8a6cd7 100644
--- a/frc971/imu_fdcan/BUILD
+++ b/frc971/imu_fdcan/BUILD
@@ -1,3 +1,4 @@
+load("//aos:config.bzl", "aos_config")
 load("//aos/flatbuffers:generate.bzl", "static_flatbuffer")
 
 static_flatbuffer(
@@ -5,3 +6,125 @@
     srcs = ["dual_imu.fbs"],
     visibility = ["//visibility:public"],
 )
+
+static_flatbuffer(
+    name = "can_translator_status_fbs",
+    srcs = ["can_translator_status.fbs"],
+    visibility = ["//visibility:public"],
+)
+
+static_flatbuffer(
+    name = "dual_imu_blender_status_fbs",
+    srcs = ["dual_imu_blender_status.fbs"],
+    visibility = ["//visibility:public"],
+)
+
+cc_binary(
+    name = "can_translator",
+    srcs = ["can_translator_main.cc"],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":can_translator_lib",
+        "//aos:init",
+        "//aos/events:shm_event_loop",
+    ],
+)
+
+cc_library(
+    name = "can_translator_lib",
+    srcs = [
+        "can_translator_lib.cc",
+    ],
+    hdrs = [
+        "can_translator_lib.h",
+    ],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":can_translator_status_fbs",
+        ":dual_imu_fbs",
+        "//aos/events:event_loop",
+        "//frc971/can_logger:can_logging_fbs",
+    ],
+)
+
+cc_binary(
+    name = "dual_imu_blender",
+    srcs = ["dual_imu_blender_main.cc"],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":dual_imu_blender_lib",
+        "//aos:init",
+        "//aos/events:shm_event_loop",
+    ],
+)
+
+cc_library(
+    name = "dual_imu_blender_lib",
+    srcs = [
+        "dual_imu_blender_lib.cc",
+    ],
+    hdrs = [
+        "dual_imu_blender_lib.h",
+    ],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":dual_imu_blender_status_fbs",
+        ":dual_imu_fbs",
+        "//aos/events:event_loop",
+        "//frc971/can_logger:can_logging_fbs",
+        "//frc971/wpilib:imu_batch_fbs",
+    ],
+)
+
+cc_test(
+    name = "can_translator_lib_test",
+    srcs = [
+        "can_translator_lib_test.cc",
+    ],
+    data = [
+        ":dual_imu_test_config",
+    ],
+    deps = [
+        ":can_translator_lib",
+        ":can_translator_status_fbs",
+        ":dual_imu_fbs",
+        "//aos/events:simulated_event_loop",
+        "//aos/testing:googletest",
+        "//frc971/can_logger:can_logging_fbs",
+        "@com_github_google_glog//:glog",
+    ],
+)
+
+cc_test(
+    name = "dual_imu_blender_lib_test",
+    srcs = [
+        "dual_imu_blender_lib_test.cc",
+    ],
+    data = [
+        ":dual_imu_test_config",
+    ],
+    deps = [
+        ":dual_imu_blender_lib",
+        ":dual_imu_blender_status_fbs",
+        ":dual_imu_fbs",
+        "//aos/events:simulated_event_loop",
+        "//aos/testing:googletest",
+        "//frc971/can_logger:can_logging_fbs",
+        "@com_github_google_glog//:glog",
+    ],
+)
+
+aos_config(
+    name = "dual_imu_test_config",
+    src = "dual_imu_test_config_source.json",
+    flatbuffers = [
+        "//aos/logging:log_message_fbs",
+        ":dual_imu_fbs",
+        ":can_translator_status_fbs",
+        "//frc971/can_logger:can_logging_fbs",
+        ":dual_imu_blender_status_fbs",
+        "//frc971/wpilib:imu_batch_fbs",
+        "//aos/events:event_loop_fbs",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+)
diff --git a/frc971/imu_fdcan/can_translator_lib.cc b/frc971/imu_fdcan/can_translator_lib.cc
new file mode 100644
index 0000000..e4a927e
--- /dev/null
+++ b/frc971/imu_fdcan/can_translator_lib.cc
@@ -0,0 +1,138 @@
+#include "frc971/imu_fdcan/can_translator_lib.h"
+
+using frc971::imu_fdcan::CANTranslator;
+
+constexpr std::size_t kCanFrameSize = 64;
+constexpr int kImuCanId = 1;
+
+CANTranslator::CANTranslator(aos::EventLoop *event_loop,
+                             std::string_view canframe_channel)
+    : event_loop_(event_loop),
+      dual_imu_sender_(
+          event_loop->MakeSender<frc971::imu::DualImuStatic>("/imu")),
+      can_translator_status_sender_(
+          event_loop->MakeSender<frc971::imu::CanTranslatorStatusStatic>(
+              "/imu")) {
+  // TODO(max): Update this with a proper priority
+  event_loop->SetRuntimeRealtimePriority(15);
+
+  event_loop->MakeWatcher(
+      canframe_channel, [this](const frc971::can_logger::CanFrame &can_frame) {
+        if (can_frame.data()->size() / sizeof(uint8_t) != 64) {
+          invalid_packet_count_++;
+        }
+
+        if (can_frame.can_id() != kImuCanId) {
+          invalid_can_id_count_++;
+          return;
+        }
+
+        if (can_frame.data()->size() / sizeof(uint8_t) == 64) {
+          valid_packet_count_++;
+          HandleFrame(&can_frame);
+        }
+      });
+
+  event_loop->AddPhasedLoop(
+      [this](int) {
+        aos::Sender<frc971::imu::CanTranslatorStatusStatic>::StaticBuilder
+            status_builder = can_translator_status_sender_.MakeStaticBuilder();
+
+        status_builder->set_valid_packet_count(valid_packet_count_);
+        status_builder->set_invalid_packet_count(invalid_packet_count_);
+        status_builder->set_invalid_can_id_count(invalid_can_id_count_);
+
+        status_builder.CheckOk(status_builder.Send());
+      },
+      std::chrono::milliseconds(100));
+}
+
+// Gets the data from the span and iterates it to the next section of bytes.
+template <typename T>
+T GetAndPopDataFromBuffer(std::span<const uint8_t> &span) {
+  T value = 0;
+
+  std::memcpy(&value, span.data(), sizeof(T));
+  span = span.subspan(sizeof(T));
+
+  return value;
+}
+
+// Values from the data field mapping table in
+// https://docs.google.com/document/d/12AJUruW7DZ2pIrDzTyPC0qqFoia4QOSVlax6Jd7m4H0/edit?usp=sharing
+void CANTranslator::HandleFrame(const frc971::can_logger::CanFrame *can_frame) {
+  aos::Sender<frc971::imu::DualImuStatic>::StaticBuilder dual_imu_builder =
+      dual_imu_sender_.MakeStaticBuilder();
+
+  std::span can_data(can_frame->data()->data(), kCanFrameSize);
+
+  frc971::imu::SingleImuStatic *murata = dual_imu_builder->add_murata();
+
+  auto *murata_chip_states = murata->add_chip_states();
+  frc971::imu::ChipStateStatic *murata_uno_chip_state =
+      murata_chip_states->emplace_back();
+  frc971::imu::ChipStateStatic *murata_due_chip_state =
+      murata_chip_states->emplace_back();
+
+  frc971::imu::SingleImuStatic *tdk = dual_imu_builder->add_tdk();
+
+  auto tdk_chip_states = tdk->add_chip_states();
+  frc971::imu::ChipStateStatic *tdk_chip_state =
+      tdk_chip_states->emplace_back();
+
+  dual_imu_builder->set_board_timestamp_us(
+      GetAndPopDataFromBuffer<uint32_t>(can_data));
+
+  dual_imu_builder->set_packet_counter(
+      GetAndPopDataFromBuffer<uint16_t>(can_data));
+
+  tdk_chip_state->set_counter(GetAndPopDataFromBuffer<uint16_t>(can_data));
+  murata_uno_chip_state->set_counter(
+      GetAndPopDataFromBuffer<uint16_t>(can_data));
+  murata_due_chip_state->set_counter(
+      GetAndPopDataFromBuffer<uint16_t>(can_data));
+
+  tdk->set_accelerometer_x(GetAndPopDataFromBuffer<float>(can_data));
+  tdk->set_accelerometer_y(GetAndPopDataFromBuffer<float>(can_data));
+  tdk->set_accelerometer_z(GetAndPopDataFromBuffer<float>(can_data));
+
+  tdk->set_gyro_x(GetAndPopDataFromBuffer<float>(can_data));
+  tdk->set_gyro_y(GetAndPopDataFromBuffer<float>(can_data));
+  tdk->set_gyro_z(GetAndPopDataFromBuffer<float>(can_data));
+
+  murata->set_accelerometer_x(GetAndPopDataFromBuffer<float>(can_data));
+  murata->set_accelerometer_y(GetAndPopDataFromBuffer<float>(can_data));
+  murata->set_accelerometer_z(GetAndPopDataFromBuffer<float>(can_data));
+
+  murata->set_gyro_x(GetAndPopDataFromBuffer<float>(can_data));
+  murata->set_gyro_y(GetAndPopDataFromBuffer<float>(can_data));
+  murata->set_gyro_z(GetAndPopDataFromBuffer<float>(can_data));
+
+  tdk_chip_state->set_temperature(GetAndPopDataFromBuffer<uint8_t>(can_data));
+  murata_uno_chip_state->set_temperature(
+      GetAndPopDataFromBuffer<uint8_t>(can_data));
+  murata_due_chip_state->set_temperature(
+      GetAndPopDataFromBuffer<uint8_t>(can_data));
+
+  murata_uno_chip_state->set_max_counter(std::numeric_limits<uint16_t>::max());
+  murata_due_chip_state->set_max_counter(std::numeric_limits<uint16_t>::max());
+  tdk_chip_state->set_max_counter(std::numeric_limits<uint16_t>::max());
+  dual_imu_builder->set_max_packet_counter(
+      std::numeric_limits<uint16_t>::max());
+
+  // The timestamp coming from the CanFrame is a realtime timestamp. Calculate
+  // the offset into a monotonic time based on the clock samples from when the
+  // CanFrame was sent (this may not be ultra principled, as there are no strict
+  // bounds on how much time can pass between the clock samples we are using; in
+  // practice, the differences should be negligible).
+  const int64_t realtime_offset =
+      std::chrono::duration_cast<std::chrono::nanoseconds>(
+          event_loop_->context().monotonic_event_time.time_since_epoch() -
+          event_loop_->context().realtime_event_time.time_since_epoch())
+          .count();
+
+  dual_imu_builder->set_kernel_timestamp(can_frame->realtime_timestamp_ns() +
+                                         realtime_offset);
+
+  dual_imu_builder.CheckOk(dual_imu_builder.Send());
+}
diff --git a/frc971/imu_fdcan/can_translator_lib.h b/frc971/imu_fdcan/can_translator_lib.h
new file mode 100644
index 0000000..12acb0a
--- /dev/null
+++ b/frc971/imu_fdcan/can_translator_lib.h
@@ -0,0 +1,31 @@
+#ifndef FRC971_IMU_FDCAN_CAN_TRANSLATOR_LIB_H_
+#define FRC971_IMU_FDCAN_CAN_TRANSLATOR_LIB_H_
+#include "aos/events/event_loop.h"
+#include "frc971/can_logger/can_logging_generated.h"
+#include "frc971/imu_fdcan/can_translator_status_static.h"
+#include "frc971/imu_fdcan/dual_imu_static.h"
+
+namespace frc971::imu_fdcan {
+
+// Translates the CanFrames from the IMU into a DualIMU message based on the
+// spec defined in this doc:
+// https://docs.google.com/document/d/12AJUruW7DZ2pIrDzTyPC0qqFoia4QOSVlax6Jd7m4H0/edit?usp=sharing
+class CANTranslator {
+ public:
+  CANTranslator(aos::EventLoop *event_loop, std::string_view canframe_channel);
+
+ private:
+  void HandleFrame(const can_logger::CanFrame *can_frame);
+
+  aos::EventLoop *event_loop_;
+  aos::Sender<imu::DualImuStatic> dual_imu_sender_;
+  aos::Sender<imu::CanTranslatorStatusStatic> can_translator_status_sender_;
+
+  uint64_t valid_packet_count_ = 0;
+  uint64_t invalid_packet_count_ = 0;
+  uint64_t invalid_can_id_count_ = 0;
+};
+
+}  // namespace frc971::imu_fdcan
+
+#endif  // FRC971_IMU_FDCAN_CAN_TRANSLATOR_LIB_H_
diff --git a/frc971/imu_fdcan/can_translator_lib_test.cc b/frc971/imu_fdcan/can_translator_lib_test.cc
new file mode 100644
index 0000000..f8e6d85
--- /dev/null
+++ b/frc971/imu_fdcan/can_translator_lib_test.cc
@@ -0,0 +1,197 @@
+#include "frc971/imu_fdcan/can_translator_lib.h"
+
+#include "glog/logging.h"
+#include "gtest/gtest.h"
+
+#include "aos/events/simulated_event_loop.h"
+#include "frc971/can_logger/can_logging_static.h"
+#include "frc971/imu_fdcan/can_translator_status_generated.h"
+#include "frc971/imu_fdcan/dual_imu_generated.h"
+
+class CANTranslatorTest : public ::testing::Test {
+ public:
+  CANTranslatorTest()
+      : config_(aos::configuration::ReadConfig(
+            "frc971/imu_fdcan/dual_imu_test_config.json")),
+        event_loop_factory_(&config_.message()),
+        can_translator_event_loop_(
+            event_loop_factory_.MakeEventLoop("can_translator")),
+        can_frame_event_loop_(event_loop_factory_.MakeEventLoop("can_frame")),
+        dual_imu_fetcher_(
+            can_translator_event_loop_->MakeFetcher<frc971::imu::DualImu>(
+                "/imu")),
+        can_translator_status_fetcher_(
+            can_translator_event_loop_
+                ->MakeFetcher<frc971::imu::CanTranslatorStatus>("/imu")),
+        can_frame_sender_(
+            can_frame_event_loop_
+                ->MakeSender<frc971::can_logger::CanFrameStatic>("/can")),
+        can_translator_(can_translator_event_loop_.get(), "/can") {}
+
+ protected:
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config_;
+  aos::SimulatedEventLoopFactory event_loop_factory_;
+
+  std::unique_ptr<aos::EventLoop> can_translator_event_loop_;
+  std::unique_ptr<aos::EventLoop> can_frame_event_loop_;
+
+  aos::Fetcher<frc971::imu::DualImu> dual_imu_fetcher_;
+  aos::Fetcher<frc971::imu::CanTranslatorStatus> can_translator_status_fetcher_;
+
+  aos::Sender<frc971::can_logger::CanFrameStatic> can_frame_sender_;
+
+  frc971::imu_fdcan::CANTranslator can_translator_;
+};
+
+TEST_F(CANTranslatorTest, CheckValidFrame) {
+  event_loop_factory_.GetNodeEventLoopFactory(can_frame_event_loop_->node())
+      ->SetRealtimeOffset(
+          aos::monotonic_clock::epoch() + std::chrono::seconds(0),
+          aos::realtime_clock::epoch() + std::chrono::seconds(100));
+  can_frame_event_loop_->OnRun([this] {
+    aos::Sender<frc971::can_logger::CanFrameStatic>::StaticBuilder
+        can_frame_builder = can_frame_sender_.MakeStaticBuilder();
+
+    can_frame_builder->set_can_id(1);
+    can_frame_builder->set_realtime_timestamp_ns(100e9 + 971);
+    auto can_data = can_frame_builder->add_data();
+    CHECK(can_data->reserve(sizeof(uint8_t) * 64));
+
+    CHECK(can_data->emplace_back(226));
+    CHECK(can_data->emplace_back(100));
+    CHECK(can_data->emplace_back(108));
+    CHECK(can_data->emplace_back(8));
+    CHECK(can_data->emplace_back(152));
+    CHECK(can_data->emplace_back(40));
+    CHECK(can_data->emplace_back(202));
+    CHECK(can_data->emplace_back(121));
+    CHECK(can_data->emplace_back(202));
+    CHECK(can_data->emplace_back(121));
+    CHECK(can_data->emplace_back(202));
+    CHECK(can_data->emplace_back(121));
+    CHECK(can_data->emplace_back(85));
+    CHECK(can_data->emplace_back(85));
+    CHECK(can_data->emplace_back(81));
+    CHECK(can_data->emplace_back(189));
+    CHECK(can_data->emplace_back(0));
+    CHECK(can_data->emplace_back(0));
+    CHECK(can_data->emplace_back(8));
+    CHECK(can_data->emplace_back(189));
+    CHECK(can_data->emplace_back(85));
+    CHECK(can_data->emplace_back(213));
+    CHECK(can_data->emplace_back(127));
+    CHECK(can_data->emplace_back(191));
+    CHECK(can_data->emplace_back(12));
+    CHECK(can_data->emplace_back(189));
+    CHECK(can_data->emplace_back(34));
+    CHECK(can_data->emplace_back(187));
+    CHECK(can_data->emplace_back(255));
+    CHECK(can_data->emplace_back(219));
+    CHECK(can_data->emplace_back(220));
+    CHECK(can_data->emplace_back(59));
+    CHECK(can_data->emplace_back(147));
+    CHECK(can_data->emplace_back(173));
+    CHECK(can_data->emplace_back(5));
+    CHECK(can_data->emplace_back(61));
+    CHECK(can_data->emplace_back(88));
+    CHECK(can_data->emplace_back(68));
+    CHECK(can_data->emplace_back(205));
+    CHECK(can_data->emplace_back(188));
+    CHECK(can_data->emplace_back(230));
+    CHECK(can_data->emplace_back(92));
+    CHECK(can_data->emplace_back(24));
+    CHECK(can_data->emplace_back(189));
+    CHECK(can_data->emplace_back(235));
+    CHECK(can_data->emplace_back(1));
+    CHECK(can_data->emplace_back(127));
+    CHECK(can_data->emplace_back(191));
+    CHECK(can_data->emplace_back(210));
+    CHECK(can_data->emplace_back(7));
+    CHECK(can_data->emplace_back(34));
+    CHECK(can_data->emplace_back(54));
+    CHECK(can_data->emplace_back(86));
+    CHECK(can_data->emplace_back(103));
+    CHECK(can_data->emplace_back(133));
+    CHECK(can_data->emplace_back(186));
+    CHECK(can_data->emplace_back(100));
+    CHECK(can_data->emplace_back(205));
+    CHECK(can_data->emplace_back(101));
+    CHECK(can_data->emplace_back(185));
+    CHECK(can_data->emplace_back(29));
+    CHECK(can_data->emplace_back(26));
+    CHECK(can_data->emplace_back(26));
+    CHECK(can_data->emplace_back(0));
+
+    can_frame_builder.CheckOk(can_frame_builder.Send());
+  });
+
+  event_loop_factory_.RunFor(std::chrono::milliseconds(200));
+
+  ASSERT_TRUE(can_translator_status_fetcher_.Fetch());
+  ASSERT_TRUE(dual_imu_fetcher_.Fetch());
+
+  ASSERT_FALSE(can_translator_status_fetcher_->invalid_packet_count() > 0);
+  ASSERT_FALSE(can_translator_status_fetcher_->invalid_can_id_count() > 0);
+  EXPECT_EQ(can_translator_status_fetcher_->valid_packet_count(), 1);
+
+  EXPECT_EQ(dual_imu_fetcher_->board_timestamp_us(), 141321442);
+  EXPECT_EQ(dual_imu_fetcher_->packet_counter(), 10392);
+
+  EXPECT_NEAR(dual_imu_fetcher_->murata()->gyro_x(), 2.41444e-06, 0.00001);
+  EXPECT_NEAR(dual_imu_fetcher_->murata()->gyro_y(), -0.00101779, 0.00001);
+  EXPECT_NEAR(dual_imu_fetcher_->murata()->gyro_z(), -0.000219157, 0.00001);
+
+  EXPECT_NEAR(dual_imu_fetcher_->murata()->accelerometer_x(), -0.025057,
+              0.00001);
+  EXPECT_NEAR(dual_imu_fetcher_->murata()->accelerometer_y(), -0.037198,
+              0.00001);
+  EXPECT_NEAR(dual_imu_fetcher_->murata()->accelerometer_z(), -0.996123,
+              0.00001);
+
+  EXPECT_EQ(dual_imu_fetcher_->murata()->chip_states()->Get(0)->counter(),
+            31178);
+  EXPECT_EQ(dual_imu_fetcher_->murata()->chip_states()->Get(0)->temperature(),
+            26);
+
+  EXPECT_EQ(dual_imu_fetcher_->murata()->chip_states()->Get(1)->counter(),
+            31178);
+  EXPECT_EQ(dual_imu_fetcher_->murata()->chip_states()->Get(1)->temperature(),
+            26);
+
+  EXPECT_NEAR(dual_imu_fetcher_->tdk()->gyro_x(), -0.00248319, 0.00001);
+  EXPECT_NEAR(dual_imu_fetcher_->tdk()->gyro_y(), 0.00674009, 0.00001);
+  EXPECT_NEAR(dual_imu_fetcher_->tdk()->gyro_z(), 0.0326362, 0.00001);
+
+  EXPECT_NEAR(dual_imu_fetcher_->tdk()->accelerometer_x(), -0.0511068, 0.00001);
+  EXPECT_NEAR(dual_imu_fetcher_->tdk()->accelerometer_y(), -0.0332031, 0.00001);
+  EXPECT_NEAR(dual_imu_fetcher_->tdk()->accelerometer_z(), -0.999349, 0.00001);
+
+  EXPECT_EQ(dual_imu_fetcher_->tdk()->chip_states()->Get(0)->counter(), 31178);
+  EXPECT_EQ(dual_imu_fetcher_->tdk()->chip_states()->Get(0)->temperature(), 29);
+
+  EXPECT_EQ(dual_imu_fetcher_->kernel_timestamp(), 971);
+}
+
+TEST_F(CANTranslatorTest, CheckInvalidFrame) {
+  can_frame_event_loop_->OnRun([this] {
+    aos::Sender<frc971::can_logger::CanFrameStatic>::StaticBuilder
+        can_frame_builder = can_frame_sender_.MakeStaticBuilder();
+
+    can_frame_builder->set_can_id(2);
+    can_frame_builder->set_realtime_timestamp_ns(100);
+    auto can_data = can_frame_builder->add_data();
+    CHECK(can_data->reserve(sizeof(uint8_t) * 1));
+
+    CHECK(can_data->emplace_back(0));
+
+    can_frame_builder.CheckOk(can_frame_builder.Send());
+  });
+
+  event_loop_factory_.RunFor(std::chrono::milliseconds(200));
+
+  ASSERT_TRUE(can_translator_status_fetcher_.Fetch());
+  ASSERT_FALSE(dual_imu_fetcher_.Fetch());
+
+  EXPECT_EQ(can_translator_status_fetcher_->invalid_packet_count(), 1);
+  EXPECT_EQ(can_translator_status_fetcher_->invalid_can_id_count(), 1);
+}
diff --git a/frc971/imu_fdcan/can_translator_main.cc b/frc971/imu_fdcan/can_translator_main.cc
new file mode 100644
index 0000000..fdb7107
--- /dev/null
+++ b/frc971/imu_fdcan/can_translator_main.cc
@@ -0,0 +1,22 @@
+#include "aos/events/shm_event_loop.h"
+#include "aos/init.h"
+#include "frc971/imu_fdcan/can_translator_lib.h"
+
+DEFINE_string(channel, "/can", "The CAN channel to use");
+
+using frc971::imu_fdcan::CANTranslator;
+
+int main(int argc, char **argv) {
+  ::aos::InitGoogle(&argc, &argv);
+
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config =
+      aos::configuration::ReadConfig("aos_config.json");
+
+  ::aos::ShmEventLoop event_loop(&config.message());
+
+  CANTranslator translator(&event_loop, FLAGS_channel);
+
+  event_loop.Run();
+
+  return 0;
+}
diff --git a/frc971/imu_fdcan/can_translator_status.fbs b/frc971/imu_fdcan/can_translator_status.fbs
new file mode 100644
index 0000000..232f3ba
--- /dev/null
+++ b/frc971/imu_fdcan/can_translator_status.fbs
@@ -0,0 +1,12 @@
+namespace frc971.imu;
+
+table CanTranslatorStatus {
+  // Number of times we've gotten valid packets at 64 bytes
+  valid_packet_count: uint64 (id: 0);
+  // Number of times we've gotten packets under 64 bytes
+  invalid_packet_count: uint64 (id: 1);
+  // Number of times we've gotten an invalid can id
+  invalid_can_id_count: uint64 (id: 2);
+}
+
+root_type CanTranslatorStatus;
diff --git a/frc971/imu_fdcan/dual_imu.fbs b/frc971/imu_fdcan/dual_imu.fbs
index 7236acf..61cc976 100644
--- a/frc971/imu_fdcan/dual_imu.fbs
+++ b/frc971/imu_fdcan/dual_imu.fbs
@@ -1,5 +1,7 @@
 namespace frc971.imu;
 
+attribute "static_length";
+
 table ChipState {
   // Counter indicating how many samples have been taken on this IMU.
   // Note that because averaging occurs on the microcontroller, this
@@ -28,14 +30,14 @@
   accelerometer_z:double (id: 5);
 
   // State for the individual ASIC(s) for this IMU.
-  chip_states:[ChipState] (id: 6);
+  chip_states:[ChipState] (id: 6, static_length: 2);
 }
 
 table DualImu {
-  // Timestamp from the board corresponding to these samples.
+  // Timestamp from the board corresponding to these samples in nanoseconds.
   // Future changes may lead us to add per-chip timestamps, but for now
   // we treat them as being sampled at the same time.
-  board_timestamp_us:uint32 (id: 0);
+  kernel_timestamp:int64 (id: 0);
   // Packet counter that should increment by 1 for every incoming packet.
   packet_counter:uint64 (id: 1);
   // Value at which the packet counter wraps, such that
@@ -45,6 +47,8 @@
   murata:SingleImu (id: 3);
   // Readings associated with the TDK IMU.
   tdk:SingleImu (id: 4);
+  // Timestamp from the IMU in microseconds
+  board_timestamp_us:uint32 (id: 5);
 }
 
 root_type DualImu;
diff --git a/frc971/imu_fdcan/dual_imu_blender_lib.cc b/frc971/imu_fdcan/dual_imu_blender_lib.cc
new file mode 100644
index 0000000..09655e3
--- /dev/null
+++ b/frc971/imu_fdcan/dual_imu_blender_lib.cc
@@ -0,0 +1,128 @@
+#include "frc971/imu_fdcan/dual_imu_blender_lib.h"
+
+#include "gflags/gflags.h"
+
+DEFINE_bool(murata_only, false,
+            "If true then only use the murata value and ignore the tdk.");
+
+// Saturation for the gyro is measured in +- radians/s
+static constexpr double kMurataGyroSaturation = (300.0 * M_PI) / 180;
+
+// Measured in gs
+static constexpr double kMurataAccelSaturation = 6.0;
+
+// Coefficient to multiply the saturation values by to give some room on where
+// we switch to tdk.
+static constexpr double kSaturationCoeff = 0.9;
+
+using frc971::imu_fdcan::DualImuBlender;
+
+DualImuBlender::DualImuBlender(aos::EventLoop *event_loop)
+    : imu_values_batch_sender_(
+          event_loop->MakeSender<frc971::IMUValuesBatchStatic>("/localizer")),
+      dual_imu_blender_status_sender_(
+          event_loop->MakeSender<frc971::imu::DualImuBlenderStatusStatic>(
+              "/imu")) {
+  // TODO(max): Give this a proper priority
+  event_loop->SetRuntimeRealtimePriority(15);
+
+  event_loop->MakeWatcher("/imu", [this](const frc971::imu::DualImu &dual_imu) {
+    HandleDualImu(&dual_imu);
+  });
+}
+
+void DualImuBlender::HandleDualImu(const frc971::imu::DualImu *dual_imu) {
+  aos::Sender<frc971::IMUValuesBatchStatic>::StaticBuilder
+      imu_values_batch_builder_ = imu_values_batch_sender_.MakeStaticBuilder();
+
+  aos::Sender<frc971::imu::DualImuBlenderStatusStatic>::StaticBuilder
+      dual_imu_blender_status_builder =
+          dual_imu_blender_status_sender_.MakeStaticBuilder();
+
+  frc971::IMUValuesStatic *imu_values =
+      CHECK_NOTNULL(imu_values_batch_builder_->add_readings()->emplace_back());
+
+  imu_values->set_pico_timestamp_us(dual_imu->board_timestamp_us());
+  imu_values->set_monotonic_timestamp_ns(dual_imu->kernel_timestamp());
+  imu_values->set_data_counter(dual_imu->packet_counter());
+
+  if (std::abs(dual_imu->tdk()->gyro_x()) >=
+      kSaturationCoeff * kMurataGyroSaturation) {
+    dual_imu_blender_status_builder->set_gyro_x(imu::ImuType::TDK);
+    imu_values->set_gyro_x(dual_imu->tdk()->gyro_x());
+  } else {
+    dual_imu_blender_status_builder->set_gyro_x(imu::ImuType::MURATA);
+    imu_values->set_gyro_x(dual_imu->murata()->gyro_x());
+  }
+
+  if (std::abs(dual_imu->tdk()->gyro_y()) >=
+      kSaturationCoeff * kMurataGyroSaturation) {
+    dual_imu_blender_status_builder->set_gyro_y(imu::ImuType::TDK);
+    imu_values->set_gyro_y(dual_imu->tdk()->gyro_y());
+  } else {
+    dual_imu_blender_status_builder->set_gyro_y(imu::ImuType::MURATA);
+    imu_values->set_gyro_y(dual_imu->murata()->gyro_y());
+  }
+
+  if (std::abs(dual_imu->tdk()->gyro_z()) >=
+      kSaturationCoeff * kMurataGyroSaturation) {
+    dual_imu_blender_status_builder->set_gyro_z(imu::ImuType::TDK);
+    imu_values->set_gyro_z(dual_imu->tdk()->gyro_z());
+  } else {
+    dual_imu_blender_status_builder->set_gyro_z(imu::ImuType::MURATA);
+    imu_values->set_gyro_z(dual_imu->murata()->gyro_z());
+  }
+
+  if (std::abs(dual_imu->tdk()->accelerometer_x()) >=
+      kSaturationCoeff * kMurataAccelSaturation) {
+    dual_imu_blender_status_builder->set_accelerometer_x(imu::ImuType::TDK);
+    imu_values->set_accelerometer_x(dual_imu->tdk()->accelerometer_x());
+  } else {
+    dual_imu_blender_status_builder->set_accelerometer_x(imu::ImuType::MURATA);
+    imu_values->set_accelerometer_x(dual_imu->murata()->accelerometer_x());
+  }
+
+  if (std::abs(dual_imu->tdk()->accelerometer_y()) >=
+      kSaturationCoeff * kMurataAccelSaturation) {
+    dual_imu_blender_status_builder->set_accelerometer_y(imu::ImuType::TDK);
+    imu_values->set_accelerometer_y(dual_imu->tdk()->accelerometer_y());
+  } else {
+    dual_imu_blender_status_builder->set_accelerometer_y(imu::ImuType::MURATA);
+    imu_values->set_accelerometer_y(dual_imu->murata()->accelerometer_y());
+  }
+
+  if (std::abs(dual_imu->tdk()->accelerometer_z()) >=
+      kSaturationCoeff * kMurataAccelSaturation) {
+    dual_imu_blender_status_builder->set_accelerometer_z(imu::ImuType::TDK);
+    imu_values->set_accelerometer_z(dual_imu->tdk()->accelerometer_z());
+  } else {
+    dual_imu_blender_status_builder->set_accelerometer_z(imu::ImuType::MURATA);
+    imu_values->set_accelerometer_z(dual_imu->murata()->accelerometer_z());
+  }
+
+  if (FLAGS_murata_only) {
+    imu_values->set_gyro_x(dual_imu->murata()->gyro_x());
+    imu_values->set_gyro_y(dual_imu->murata()->gyro_y());
+    imu_values->set_gyro_z(dual_imu->murata()->gyro_z());
+
+    imu_values->set_accelerometer_x(dual_imu->murata()->accelerometer_x());
+    imu_values->set_accelerometer_y(dual_imu->murata()->accelerometer_y());
+    imu_values->set_accelerometer_z(dual_imu->murata()->accelerometer_z());
+
+    dual_imu_blender_status_builder->set_gyro_x(imu::ImuType::MURATA);
+    dual_imu_blender_status_builder->set_gyro_y(imu::ImuType::MURATA);
+    dual_imu_blender_status_builder->set_gyro_z(imu::ImuType::MURATA);
+
+    dual_imu_blender_status_builder->set_accelerometer_x(imu::ImuType::MURATA);
+    dual_imu_blender_status_builder->set_accelerometer_y(imu::ImuType::MURATA);
+    dual_imu_blender_status_builder->set_accelerometer_z(imu::ImuType::MURATA);
+  }
+
+  dual_imu_blender_status_builder.CheckOk(
+      dual_imu_blender_status_builder.Send());
+
+  imu_values->set_temperature(
+      dual_imu->murata()->chip_states()->Get(0)->temperature());
+
+  imu_values_batch_builder_.CheckOk(imu_values_batch_builder_.Send());
+}
diff --git a/frc971/imu_fdcan/dual_imu_blender_lib.h b/frc971/imu_fdcan/dual_imu_blender_lib.h
new file mode 100644
index 0000000..04044d2
--- /dev/null
+++ b/frc971/imu_fdcan/dual_imu_blender_lib.h
@@ -0,0 +1,27 @@
+#ifndef FRC971_IMU_FDCAN_DUAL_IMU_BLENDER_H_
+#define FRC971_IMU_FDCAN_DUAL_IMU_BLENDER_H_
+
+#include "aos/events/event_loop.h"
+#include "frc971/imu_fdcan/dual_imu_blender_status_static.h"
+#include "frc971/imu_fdcan/dual_imu_generated.h"
+#include "frc971/wpilib/imu_batch_static.h"
+
+namespace frc971::imu_fdcan {
+
+// Takes in the values from the dual_imu and creates an IMUValuesBatch. Will use
+// the murata until we've hit saturation according to the tdk, then we will
+// switch to using tdk IMU values.
+class DualImuBlender {
+ public:
+  DualImuBlender(aos::EventLoop *event_loop);
+
+  void HandleDualImu(const frc971::imu::DualImu *dual_imu);
+
+ private:
+  aos::Sender<IMUValuesBatchStatic> imu_values_batch_sender_;
+  aos::Sender<imu::DualImuBlenderStatusStatic> dual_imu_blender_status_sender_;
+};
+
+}  // namespace frc971::imu_fdcan
+
+#endif  // FRC971_IMU_FDCAN_DUAL_IMU_BLENDER_H_
diff --git a/frc971/imu_fdcan/dual_imu_blender_lib_test.cc b/frc971/imu_fdcan/dual_imu_blender_lib_test.cc
new file mode 100644
index 0000000..b3a3015
--- /dev/null
+++ b/frc971/imu_fdcan/dual_imu_blender_lib_test.cc
@@ -0,0 +1,302 @@
+#include "frc971/imu_fdcan/dual_imu_blender_lib.h"
+
+#include "glog/logging.h"
+#include "gtest/gtest.h"
+
+#include "aos/events/simulated_event_loop.h"
+#include "frc971/imu_fdcan/dual_imu_blender_lib.h"
+#include "frc971/imu_fdcan/dual_imu_blender_status_generated.h"
+#include "frc971/imu_fdcan/dual_imu_generated.h"
+#include "frc971/imu_fdcan/dual_imu_static.h"
+
+class DualImuBlenderTest : public ::testing::Test {
+ public:
+  DualImuBlenderTest()
+      : config_(aos::configuration::ReadConfig(
+            "frc971/imu_fdcan/dual_imu_test_config.json")),
+        event_loop_factory_(&config_.message()),
+        dual_imu_blender_event_loop_(
+            event_loop_factory_.MakeEventLoop("dual_imu_blender")),
+        dual_imu_event_loop_(event_loop_factory_.MakeEventLoop("dual_imu")),
+        imu_values_batch_fetcher_(
+            dual_imu_event_loop_->MakeFetcher<frc971::IMUValuesBatch>(
+                "/localizer")),
+        dual_imu_blender_status_fetcher_(
+            dual_imu_blender_event_loop_
+                ->MakeFetcher<frc971::imu::DualImuBlenderStatus>("/imu")),
+        dual_imu_sender_(
+            dual_imu_event_loop_->MakeSender<frc971::imu::DualImuStatic>(
+                "/imu")),
+        dual_imu_blender_(dual_imu_blender_event_loop_.get()) {}
+
+  void CheckImuType(frc971::imu::ImuType type) {
+    EXPECT_EQ(dual_imu_blender_status_fetcher_->gyro_x(), type);
+    EXPECT_EQ(dual_imu_blender_status_fetcher_->gyro_y(), type);
+    EXPECT_EQ(dual_imu_blender_status_fetcher_->gyro_z(), type);
+    EXPECT_EQ(dual_imu_blender_status_fetcher_->accelerometer_x(), type);
+    EXPECT_EQ(dual_imu_blender_status_fetcher_->accelerometer_y(), type);
+    EXPECT_EQ(dual_imu_blender_status_fetcher_->accelerometer_z(), type);
+  }
+
+ protected:
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config_;
+  aos::SimulatedEventLoopFactory event_loop_factory_;
+
+  std::unique_ptr<aos::EventLoop> dual_imu_blender_event_loop_;
+  std::unique_ptr<aos::EventLoop> dual_imu_event_loop_;
+
+  aos::Fetcher<frc971::IMUValuesBatch> imu_values_batch_fetcher_;
+  aos::Fetcher<frc971::imu::DualImuBlenderStatus>
+      dual_imu_blender_status_fetcher_;
+
+  aos::Sender<frc971::imu::DualImuStatic> dual_imu_sender_;
+
+  frc971::imu_fdcan::DualImuBlender dual_imu_blender_;
+};
+
+// Sanity check that some sane values in are the same values out
+TEST_F(DualImuBlenderTest, SanityCheck) {
+  dual_imu_blender_event_loop_->OnRun([this] {
+    aos::Sender<frc971::imu::DualImuStatic>::StaticBuilder dual_imu_builder =
+        dual_imu_sender_.MakeStaticBuilder();
+
+    frc971::imu::SingleImuStatic *murata = dual_imu_builder->add_murata();
+
+    auto *murata_chip_states = murata->add_chip_states();
+    frc971::imu::ChipStateStatic *murata_uno_chip_state =
+        murata_chip_states->emplace_back();
+    frc971::imu::ChipStateStatic *murata_due_chip_state =
+        murata_chip_states->emplace_back();
+
+    frc971::imu::SingleImuStatic *tdk = dual_imu_builder->add_tdk();
+
+    dual_imu_builder->set_board_timestamp_us(0);
+    dual_imu_builder->set_kernel_timestamp(0);
+
+    tdk->set_gyro_x(0.3);
+    tdk->set_gyro_y(0.2);
+    tdk->set_gyro_z(0.2);
+
+    murata->set_gyro_x(0.351);
+    murata->set_gyro_y(0.284);
+    murata->set_gyro_z(0.293);
+
+    tdk->set_accelerometer_x(1.5);
+    tdk->set_accelerometer_y(1.5);
+    tdk->set_accelerometer_z(1.5);
+
+    murata->set_accelerometer_x(1.58);
+    murata->set_accelerometer_y(1.51);
+    murata->set_accelerometer_z(1.52);
+
+    murata_uno_chip_state->set_temperature(20);
+    murata_due_chip_state->set_temperature(10);
+
+    dual_imu_builder.CheckOk(dual_imu_builder.Send());
+  });
+
+  event_loop_factory_.RunFor(std::chrono::milliseconds(200));
+
+  ASSERT_TRUE(imu_values_batch_fetcher_.Fetch());
+  ASSERT_TRUE(dual_imu_blender_status_fetcher_.Fetch());
+
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->gyro_x(), 0.351,
+              0.0001);
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->gyro_y(), 0.284,
+              0.0001);
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->gyro_z(), 0.293,
+              0.0001);
+
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->accelerometer_x(),
+              1.58, 0.0001);
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->accelerometer_y(),
+              1.51, 0.0001);
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->accelerometer_z(),
+              1.52, 0.0001);
+
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->temperature(), 20,
+              0.0001);
+
+  CheckImuType(frc971::imu::ImuType::MURATA);
+}
+
+TEST_F(DualImuBlenderTest, Saturation) {
+  dual_imu_blender_event_loop_->OnRun([this] {
+    aos::Sender<frc971::imu::DualImuStatic>::StaticBuilder dual_imu_builder =
+        dual_imu_sender_.MakeStaticBuilder();
+
+    frc971::imu::SingleImuStatic *murata = dual_imu_builder->add_murata();
+
+    auto *murata_chip_states = murata->add_chip_states();
+    frc971::imu::ChipStateStatic *murata_uno_chip_state =
+        murata_chip_states->emplace_back();
+
+    frc971::imu::SingleImuStatic *tdk = dual_imu_builder->add_tdk();
+
+    dual_imu_builder->set_board_timestamp_us(0);
+    dual_imu_builder->set_kernel_timestamp(0);
+
+    tdk->set_gyro_x(0.7);
+    tdk->set_gyro_y(0.7);
+    tdk->set_gyro_z(0.7);
+
+    murata->set_gyro_x(0.71);
+    murata->set_gyro_y(0.79);
+    murata->set_gyro_z(0.78);
+
+    tdk->set_accelerometer_x(1.0);
+    tdk->set_accelerometer_y(1.0);
+    tdk->set_accelerometer_z(1.0);
+
+    murata->set_accelerometer_x(1.3);
+    murata->set_accelerometer_y(1.1);
+    murata->set_accelerometer_z(1.1);
+
+    murata_uno_chip_state->set_temperature(20);
+
+    dual_imu_builder.CheckOk(dual_imu_builder.Send());
+  });
+
+  event_loop_factory_.RunFor(std::chrono::milliseconds(200));
+
+  ASSERT_TRUE(imu_values_batch_fetcher_.Fetch());
+  ASSERT_TRUE(dual_imu_blender_status_fetcher_.Fetch());
+
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->gyro_x(), 0.71,
+              0.0001);
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->gyro_y(), 0.79,
+              0.0001);
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->gyro_z(), 0.78,
+              0.0001);
+
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->accelerometer_x(),
+              1.3, 0.0001);
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->accelerometer_y(),
+              1.1, 0.0001);
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->accelerometer_z(),
+              1.1, 0.0001);
+
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->temperature(), 20,
+              0.0001);
+
+  CheckImuType(frc971::imu::ImuType::MURATA);
+
+  // Make sure we switch to TDK on saturation
+  dual_imu_blender_event_loop_->OnRun([this] {
+    aos::Sender<frc971::imu::DualImuStatic>::StaticBuilder dual_imu_builder =
+        dual_imu_sender_.MakeStaticBuilder();
+
+    frc971::imu::SingleImuStatic *murata = dual_imu_builder->add_murata();
+
+    auto *murata_chip_states = murata->add_chip_states();
+    frc971::imu::ChipStateStatic *murata_uno_chip_state =
+        murata_chip_states->emplace_back();
+
+    frc971::imu::SingleImuStatic *tdk = dual_imu_builder->add_tdk();
+
+    dual_imu_builder->set_board_timestamp_us(1);
+    dual_imu_builder->set_kernel_timestamp(1);
+
+    tdk->set_gyro_x(6.0);
+    tdk->set_gyro_y(6.0);
+    tdk->set_gyro_z(6.0);
+
+    murata->set_gyro_x(5.2);
+    murata->set_gyro_y(5.2);
+    murata->set_gyro_z(5.2);
+
+    tdk->set_accelerometer_x(6.2);
+    tdk->set_accelerometer_y(6.3);
+    tdk->set_accelerometer_z(6.5);
+
+    murata->set_accelerometer_x(5.5);
+    murata->set_accelerometer_y(5.5);
+    murata->set_accelerometer_z(5.5);
+
+    murata_uno_chip_state->set_temperature(20);
+
+    dual_imu_builder.CheckOk(dual_imu_builder.Send());
+  });
+
+  event_loop_factory_.RunFor(std::chrono::milliseconds(200));
+
+  ASSERT_TRUE(imu_values_batch_fetcher_.Fetch());
+  ASSERT_TRUE(dual_imu_blender_status_fetcher_.Fetch());
+
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->gyro_x(), 6.0,
+              0.0001);
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->gyro_y(), 6.0,
+              0.0001);
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->gyro_z(), 6.0,
+              0.0001);
+
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->accelerometer_x(),
+              6.2, 0.0001);
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->accelerometer_y(),
+              6.3, 0.0001);
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->accelerometer_z(),
+              6.5, 0.0001);
+
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->temperature(), 20,
+              0.0001);
+
+  CheckImuType(frc971::imu::ImuType::TDK);
+
+  // Check negative values as well
+  dual_imu_blender_event_loop_->OnRun([this] {
+    aos::Sender<frc971::imu::DualImuStatic>::StaticBuilder dual_imu_builder =
+        dual_imu_sender_.MakeStaticBuilder();
+
+    frc971::imu::SingleImuStatic *murata = dual_imu_builder->add_murata();
+
+    auto *murata_chip_states = murata->add_chip_states();
+    frc971::imu::ChipStateStatic *murata_uno_chip_state =
+        murata_chip_states->emplace_back();
+
+    frc971::imu::SingleImuStatic *tdk = dual_imu_builder->add_tdk();
+
+    dual_imu_builder->set_board_timestamp_us(1);
+    dual_imu_builder->set_kernel_timestamp(1);
+
+    tdk->set_gyro_x(-6.0);
+    tdk->set_gyro_y(-6.0);
+    tdk->set_gyro_z(-6.0);
+
+    murata->set_gyro_x(-5.2);
+    murata->set_gyro_y(-5.2);
+    murata->set_gyro_z(-5.2);
+
+    tdk->set_accelerometer_x(-6.2);
+    tdk->set_accelerometer_y(-6.3);
+    tdk->set_accelerometer_z(-6.5);
+
+    murata->set_accelerometer_x(-5.5);
+    murata->set_accelerometer_y(-5.5);
+    murata->set_accelerometer_z(-5.5);
+
+    murata_uno_chip_state->set_temperature(20);
+
+    dual_imu_builder.CheckOk(dual_imu_builder.Send());
+  });
+
+  event_loop_factory_.RunFor(std::chrono::milliseconds(200));
+
+  ASSERT_TRUE(imu_values_batch_fetcher_.Fetch());
+  ASSERT_TRUE(dual_imu_blender_status_fetcher_.Fetch());
+
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->gyro_x(), -6.0,
+              0.0001);
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->gyro_y(), -6.0,
+              0.0001);
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->gyro_z(), -6.0,
+              0.0001);
+
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->accelerometer_x(),
+              -6.2, 0.0001);
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->accelerometer_y(),
+              -6.3, 0.0001);
+  EXPECT_NEAR(imu_values_batch_fetcher_->readings()->Get(0)->accelerometer_z(),
+              -6.5, 0.0001);
+
+  CheckImuType(frc971::imu::ImuType::TDK);
+}
diff --git a/frc971/imu_fdcan/dual_imu_blender_main.cc b/frc971/imu_fdcan/dual_imu_blender_main.cc
new file mode 100644
index 0000000..1d5ee22
--- /dev/null
+++ b/frc971/imu_fdcan/dual_imu_blender_main.cc
@@ -0,0 +1,20 @@
+#include "aos/events/shm_event_loop.h"
+#include "aos/init.h"
+#include "frc971/imu_fdcan/dual_imu_blender_lib.h"
+
+using frc971::imu_fdcan::DualImuBlender;
+
+int main(int argc, char **argv) {
+  ::aos::InitGoogle(&argc, &argv);
+
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config =
+      aos::configuration::ReadConfig("aos_config.json");
+
+  ::aos::ShmEventLoop event_loop(&config.message());
+
+  DualImuBlender blender(&event_loop);
+
+  event_loop.Run();
+
+  return 0;
+}
diff --git a/frc971/imu_fdcan/dual_imu_blender_status.fbs b/frc971/imu_fdcan/dual_imu_blender_status.fbs
new file mode 100644
index 0000000..e3c9893
--- /dev/null
+++ b/frc971/imu_fdcan/dual_imu_blender_status.fbs
@@ -0,0 +1,19 @@
+namespace frc971.imu;
+
+enum ImuType : ubyte {
+  MURATA = 0,
+  TDK = 1,
+}
+
+table DualImuBlenderStatus {
+  // These values explain if we're using the tdk or the murata for our accelerometers and gyro.
+  gyro_x: ImuType (id: 0);
+  gyro_y: ImuType (id: 1);
+  gyro_z: ImuType (id: 2);
+
+  accelerometer_x: ImuType (id: 3);
+  accelerometer_y: ImuType (id: 4);
+  accelerometer_z: ImuType (id: 5);
+}
+
+root_type DualImuBlenderStatus;
diff --git a/frc971/imu_fdcan/dual_imu_test_config_source.json b/frc971/imu_fdcan/dual_imu_test_config_source.json
new file mode 100644
index 0000000..eda06da
--- /dev/null
+++ b/frc971/imu_fdcan/dual_imu_test_config_source.json
@@ -0,0 +1,38 @@
+{
+  "channels": [
+    {
+      "name": "/aos",
+      "type": "aos.timing.Report"
+    },
+    {
+      "name": "/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "frequency": 400
+    },
+    {
+      "name": "/imu",
+      "type": "frc971.imu.DualImu",
+      "frequency": 200
+    },
+    {
+      "name": "/can",
+      "type": "frc971.can_logger.CanFrame",
+      "frequency": 200
+    },
+    {
+      "name": "/localizer",
+      "type": "frc971.IMUValuesBatch",
+      "frequency": 1100
+    },
+    {
+      "name": "/imu",
+      "type": "frc971.imu.CanTranslatorStatus",
+      "frequency": 100
+    },
+    {
+      "name": "/imu",
+      "type": "frc971.imu.DualImuBlenderStatus",
+      "frequency": 100
+    },
+  ]
+}
diff --git a/frc971/imu_reader/imu_watcher.cc b/frc971/imu_reader/imu_watcher.cc
index 153b84f..ae69996 100644
--- a/frc971/imu_reader/imu_watcher.cc
+++ b/frc971/imu_reader/imu_watcher.cc
@@ -37,13 +37,15 @@
       if (zeroer_.Faulted()) {
         if (value->checksum_failed()) {
           imu_fault_tracker_.pico_to_pi_checksum_mismatch++;
-        } else if (value->previous_reading_diag_stat()->checksum_mismatch()) {
+        } else if (value->has_previous_reading_diag_stat() &&
+                   value->previous_reading_diag_stat()->checksum_mismatch()) {
           imu_fault_tracker_.imu_to_pico_checksum_mismatch++;
         } else {
           imu_fault_tracker_.other_zeroing_faults++;
         }
       } else {
-        if (!first_valid_data_counter_.has_value()) {
+        if (!first_valid_data_counter_.has_value() &&
+            value->has_data_counter()) {
           first_valid_data_counter_ = value->data_counter();
         }
       }
@@ -68,12 +70,14 @@
       }
       // Set encoders to nullopt if we are faulted at all (faults may include
       // checksum mismatches).
+      const bool have_encoders = !zeroer_.Faulted() &&
+                                 value->has_left_encoder() &&
+                                 value->has_right_encoder();
       const std::optional<Eigen::Vector2d> encoders =
-          zeroer_.Faulted()
-              ? std::nullopt
-              : std::make_optional(Eigen::Vector2d{
-                    left_encoder_.Unwrap(value->left_encoder()),
-                    right_encoder_.Unwrap(value->right_encoder())});
+          have_encoders ? std::make_optional(Eigen::Vector2d{
+                              left_encoder_.Unwrap(value->left_encoder()),
+                              right_encoder_.Unwrap(value->right_encoder())})
+                        : std::nullopt;
       {
         const aos::monotonic_clock::time_point pi_read_timestamp =
             aos::monotonic_clock::time_point(
diff --git a/frc971/orin/BUILD b/frc971/orin/BUILD
index 6383423..06b1d6c 100644
--- a/frc971/orin/BUILD
+++ b/frc971/orin/BUILD
@@ -61,6 +61,8 @@
     ],
     data = [
         "@apriltag_test_bfbs_images",
+        "@orin_capture_24_04//file",
+        "@orin_capture_24_04_side//file",
         "@orin_image_apriltag//file",
         "@orin_large_image_apriltag//file",
     ],
diff --git a/frc971/orin/apriltag.cc b/frc971/orin/apriltag.cc
index dd253d1..92513c2 100644
--- a/frc971/orin/apriltag.cc
+++ b/frc971/orin/apriltag.cc
@@ -113,7 +113,9 @@
 }  // namespace
 
 GpuDetector::GpuDetector(size_t width, size_t height,
-                         apriltag_detector_t *tag_detector)
+                         apriltag_detector_t *tag_detector,
+                         CameraMatrix camera_matrix,
+                         DistCoeffs distortion_coefficients)
     : width_(width),
       height_(height),
       tag_detector_(tag_detector),
@@ -141,6 +143,8 @@
       compressed_peaks_device_(line_fit_points_device_.size()),
       sorted_compressed_peaks_device_(line_fit_points_device_.size()),
       peak_extents_device_(kMaxBlobs),
+      camera_matrix_(camera_matrix),
+      distortion_coefficients_(distortion_coefficients),
       fit_quads_device_(kMaxBlobs),
       radix_sort_tmpstorage_device_(RadixSortScratchSpace<QuadBoundaryPoint>(
           sorted_union_marker_pair_device_.size())),
diff --git a/frc971/orin/apriltag.h b/frc971/orin/apriltag.h
index d3e041b..3309996 100644
--- a/frc971/orin/apriltag.h
+++ b/frc971/orin/apriltag.h
@@ -59,6 +59,21 @@
   uint32_t blob_index;
 };
 
+struct CameraMatrix {
+  double fx;
+  double cx;
+  double fy;
+  double cy;
+};
+
+struct DistCoeffs {
+  double k1;
+  double k2;
+  double p1;
+  double p2;
+  double k3;
+};
+
 // GPU based april tag detector.
 class GpuDetector {
  public:
@@ -67,8 +82,8 @@
 
   // Constructs a detector, reserving space for detecting tags of the provided
   // with and height, using the provided detector options.
-  GpuDetector(size_t width, size_t height, apriltag_detector_t *tag_detector);
-
+  GpuDetector(size_t width, size_t height, apriltag_detector_t *tag_detector,
+              CameraMatrix camera_matrix, DistCoeffs distortion_coefficients);
   virtual ~GpuDetector();
 
   // Detects april tags in the provided image.
@@ -168,6 +183,16 @@
 
   void AdjustCenter(float corners[4][2]) const;
 
+  // TODO(max): We probably don't want to use these after our test images are
+  // just orin images
+  void SetCameraMatrix(CameraMatrix camera_matrix) {
+    camera_matrix_ = camera_matrix;
+  }
+
+  void SetDistortionCoefficients(DistCoeffs distortion_coefficients) {
+    distortion_coefficients_ = distortion_coefficients;
+  }
+
  private:
   void UpdateFitQuads();
 
@@ -292,6 +317,9 @@
   GpuMemory<int> num_quad_peaked_quads_device_{/* allocate 1 integer...*/ 1};
   GpuMemory<PeakExtents> peak_extents_device_;
 
+  CameraMatrix camera_matrix_;
+  DistCoeffs distortion_coefficients_;
+
   GpuMemory<FitQuad> fit_quads_device_;
 
   std::vector<FitQuad> fit_quads_host_;
diff --git a/frc971/orin/apriltag_detect.cc b/frc971/orin/apriltag_detect.cc
index 69c93cc..41b687e 100644
--- a/frc971/orin/apriltag_detect.cc
+++ b/frc971/orin/apriltag_detect.cc
@@ -295,11 +295,79 @@
   zarray_t *detections;
 
   image_u8_t *im_samples;
+
+  CameraMatrix *camera_matrix;
+  DistCoeffs *distortion_coefficients;
 };
 
+// Dewarps points from the image based on various constants
+// Algorithm mainly taken from
+// https://docs.opencv.org/4.x/dc/dbb/tutorial_py_calibration.html
+void ReDistort(double *x, double *y, CameraMatrix *camera_matrix,
+               DistCoeffs *distortion_coefficients) {
+  double k1 = distortion_coefficients->k1;
+  double k2 = distortion_coefficients->k2;
+  double p1 = distortion_coefficients->p1;
+  double p2 = distortion_coefficients->p2;
+  double k3 = distortion_coefficients->k3;
+
+  double fx = camera_matrix->fx;
+  double cx = camera_matrix->cx;
+  double fy = camera_matrix->fy;
+  double cy = camera_matrix->cy;
+
+  double xP = (*x - cx) / fx;
+  double yP = (*y - cy) / fy;
+
+  double rSq = xP * xP + yP * yP;
+
+  double linCoef = 1 + k1 * rSq + k2 * rSq * rSq + k3 * rSq * rSq * rSq;
+  double xPP = xP * linCoef + 2 * p1 * xP * yP + p2 * (rSq + 2 * xP * xP);
+  double yPP = yP * linCoef + p1 * (rSq + 2 * yP * yP) + 2 * p2 * xP * yP;
+
+  *x = xPP * fx + cx;
+  *y = yPP * fy + cy;
+}
+
+// We're undistorting using math found from this github page
+// https://yangyushi.github.io/code/2020/03/04/opencv-undistort.html
+void UnDistort(double *x, double *y, CameraMatrix *camera_matrix,
+               DistCoeffs *distortion_coefficients) {
+  double k1 = distortion_coefficients->k1;
+  double k2 = distortion_coefficients->k2;
+  double p1 = distortion_coefficients->p1;
+  double p2 = distortion_coefficients->p2;
+  double k3 = distortion_coefficients->k3;
+
+  double fx = camera_matrix->fx;
+  double cx = camera_matrix->cx;
+  double fy = camera_matrix->fy;
+  double cy = camera_matrix->cy;
+
+  double xP = (*x - cx) / fx;
+  double yP = (*y - cy) / fy;
+
+  double x0 = xP;
+  double y0 = yP;
+
+  for (int i = 0; i < 10; i++) {
+    double rSq = xP * xP + yP * yP;
+    double linCoef = 1 + k1 * rSq + k2 * rSq * rSq + k3 * rSq * rSq * rSq;
+    double kInv = 1 / linCoef;
+    double dx = 2 * p1 * xP * yP + p2 * (rSq + k3 * rSq * rSq * rSq);
+    double dy = p1 * (rSq + 2 * yP * yP) + 2 * p2 * xP * yP;
+    xP = (x0 - dx) * kInv;
+    yP = (y0 - dy) * kInv;
+  }
+
+  *x = xP * fx + cx;
+  *y = yP * fy + cy;
+}
+
 // Mostly stolen from aprilrobotics, but modified to implement the dewarp.
 void RefineEdges(apriltag_detector_t *td, image_u8_t *im_orig,
-                 struct quad *quad) {
+                 struct quad *quad, CameraMatrix *camera_matrix,
+                 DistCoeffs *distortion_coefficients) {
   double lines[4][4];  // for each line, [Ex Ey nx ny]
 
   for (int edge = 0; edge < 4; edge++) {
@@ -395,6 +463,8 @@
       double bestx = x0 + n0 * nx;
       double besty = y0 + n0 * ny;
 
+      UnDistort(&bestx, &besty, camera_matrix, distortion_coefficients);
+
       // update our line fit statistics
       Mx += bestx;
       My += besty;
@@ -440,8 +510,12 @@
       // Compute intersection. Note that line i represents the line from corner
       // i to (i+1)&3, so the intersection of line i with line (i+1)&3
       // represents corner (i+1)&3.
-      quad->p[(i + 1) & 3][0] = lines[i][0] + L0 * A00;
-      quad->p[(i + 1) & 3][1] = lines[i][1] + L0 * A10;
+      double px = lines[i][0] + L0 * A00;
+      double py = lines[i][1] + L0 * A10;
+
+      ReDistort(&px, &py, camera_matrix, distortion_coefficients);
+      quad->p[(i + 1) & 3][0] = px;
+      quad->p[(i + 1) & 3][1] = py;
     } else {
       // this is a bad sign. We'll just keep the corner we had.
       //            debug_print("bad det: %15f %15f %15f %15f %15f\n", A00, A11,
@@ -465,7 +539,36 @@
     quad_original.Hinv = nullptr;
 
     if (td->refine_edges) {
-      RefineEdges(td, im, &quad_original);
+      RefineEdges(td, im, &quad_original, task->camera_matrix,
+                  task->distortion_coefficients);
+    }
+
+    if (td->debug) {
+      image_u8_t *im_quads = image_u8_copy(im);
+      image_u8_darken(im_quads);
+      image_u8_darken(im_quads);
+
+      srandom(0);
+
+      const int bias = 100;
+      int color = bias + (random() % (255 - bias));
+
+      image_u8_draw_line(im_quads, quad_original.p[0][0], quad_original.p[0][1],
+                         quad_original.p[1][0], quad_original.p[1][1], color,
+                         1);
+      image_u8_draw_line(im_quads, quad_original.p[1][0], quad_original.p[1][1],
+                         quad_original.p[2][0], quad_original.p[2][1], color,
+                         1);
+      image_u8_draw_line(im_quads, quad_original.p[2][0], quad_original.p[2][1],
+                         quad_original.p[3][0], quad_original.p[3][1], color,
+                         1);
+      image_u8_draw_line(im_quads, quad_original.p[3][0], quad_original.p[3][1],
+                         quad_original.p[0][0], quad_original.p[0][1], color,
+                         1);
+
+      image_u8_write_pnm(
+          im_quads,
+          std::string("/tmp/quad" + std::to_string(quadidx) + ".pnm").c_str());
     }
 
     quad_decode_index(td, &quad_original, im, task->im_samples,
@@ -504,6 +607,8 @@
     tasks[ntasks].td = tag_detector_;
     tasks[ntasks].im = &im_orig;
     tasks[ntasks].detections = detections_;
+    tasks[ntasks].camera_matrix = &camera_matrix_;
+    tasks[ntasks].distortion_coefficients = &distortion_coefficients_;
 
     tasks[ntasks].im_samples = nullptr;
 
diff --git a/frc971/orin/argus_camera.cc b/frc971/orin/argus_camera.cc
index 37f7e7d..a4dca06 100644
--- a/frc971/orin/argus_camera.cc
+++ b/frc971/orin/argus_camera.cc
@@ -431,7 +431,6 @@
       const Argus::CaptureMetadata *metadata = ibuffer->getMetadata();
       const Argus::ICaptureMetadata *imetadata =
           Argus::interface_cast<const Argus::ICaptureMetadata>(metadata);
-      CHECK(imetadata);
       return imetadata;
     }
 
@@ -566,51 +565,60 @@
 
       if (buffer.nvbuf_surf() == nullptr) {
         // TODO(austin): Control-C isn't working for some reason, debug it...
+        // We're restarting nvargus-daemon here because if we exit like this its
+        // likely that nvargus-daemon has run into an error that it can't
+        // recover from. Which means even if this program restarts it can't get
+        // new camera images.
+        CHECK_EQ(std::system("sudo systemctl restart nvargus-daemon.service"),
+                 0);
         event_loop.Exit();
         return;
       }
 
       const Argus::ICaptureMetadata *imetadata = buffer.imetadata();
 
-      aos::Sender<frc971::vision::CameraImage>::Builder builder =
-          sender.MakeBuilder();
+      if (imetadata) {
+        aos::Sender<frc971::vision::CameraImage>::Builder builder =
+            sender.MakeBuilder();
 
-      uint8_t *data_pointer = nullptr;
-      builder.fbb()->StartIndeterminateVector(FLAGS_width * FLAGS_height * 2, 1,
-                                              64, &data_pointer);
+        uint8_t *data_pointer = nullptr;
+        builder.fbb()->StartIndeterminateVector(FLAGS_width * FLAGS_height * 2,
+                                                1, 64, &data_pointer);
 
-      YCbCr422(buffer.nvbuf_surf(), data_pointer);
-      flatbuffers::Offset<flatbuffers::Vector<uint8_t>> data_offset =
-          builder.fbb()->EndIndeterminateVector(FLAGS_width * FLAGS_height * 2,
-                                                1);
+        YCbCr422(buffer.nvbuf_surf(), data_pointer);
+        flatbuffers::Offset<flatbuffers::Vector<uint8_t>> data_offset =
+            builder.fbb()->EndIndeterminateVector(
+                FLAGS_width * FLAGS_height * 2, 1);
 
-      auto image_builder = builder.MakeBuilder<frc971::vision::CameraImage>();
-      image_builder.add_data(data_offset);
-      image_builder.add_rows(FLAGS_height);
-      image_builder.add_cols(FLAGS_width);
-      {
-        aos::ScopedNotRealtime nrt;
-        image_builder.add_monotonic_timestamp_ns(
-            imetadata->getSensorTimestamp());
+        auto image_builder = builder.MakeBuilder<frc971::vision::CameraImage>();
+        image_builder.add_data(data_offset);
+        image_builder.add_rows(FLAGS_height);
+        image_builder.add_cols(FLAGS_width);
+        {
+          aos::ScopedNotRealtime nrt;
+          image_builder.add_monotonic_timestamp_ns(
+              imetadata->getSensorTimestamp());
+        }
+        builder.CheckOk(builder.Send(image_builder.Finish()));
+
+        const aos::monotonic_clock::time_point after_send =
+            aos::monotonic_clock::now();
+
+        VLOG(1)
+            << "Got " << imetadata->getCaptureId() << " delay "
+            << chrono::duration<double>(
+                   chrono::nanoseconds(
+                       (buffer.start_time().time_since_epoch().count() -
+                        (imetadata->getSensorTimestamp() +
+                         imetadata->getFrameReadoutTime()))))
+                   .count()
+            << " mmap "
+            << chrono::duration<double>(after_send - buffer.start_time())
+                   .count()
+            << "sec dt "
+            << chrono::duration<double>(buffer.start_time() - last_time).count()
+            << "sec, exposure " << imetadata->getSensorExposureTime();
       }
-      builder.CheckOk(builder.Send(image_builder.Finish()));
-
-      const aos::monotonic_clock::time_point after_send =
-          aos::monotonic_clock::now();
-
-      VLOG(1)
-          << "Got " << imetadata->getCaptureId() << " delay "
-          << chrono::duration<double>(
-                 chrono::nanoseconds(
-                     (buffer.start_time().time_since_epoch().count() -
-                      (imetadata->getSensorTimestamp() +
-                       imetadata->getFrameReadoutTime()))))
-                 .count()
-          << " mmap "
-          << chrono::duration<double>(after_send - buffer.start_time()).count()
-          << "sec dt "
-          << chrono::duration<double>(buffer.start_time() - last_time).count()
-          << "sec, exposure " << imetadata->getSensorExposureTime();
 
       last_time = buffer.start_time();
       timer->Schedule(event_loop.monotonic_now());
diff --git a/frc971/orin/build_rootfs.py b/frc971/orin/build_rootfs.py
index ea15cf8..e0559a1 100755
--- a/frc971/orin/build_rootfs.py
+++ b/frc971/orin/build_rootfs.py
@@ -1240,6 +1240,8 @@
         copyfile("root:root", "700", "root/trace.sh")
         copyfile("root:root", "440", "etc/sudoers")
         copyfile("root:root", "644", "etc/fstab")
+        copyfile("root:root", "644", "etc/modprobe.d/audio.conf")
+        copyfile("root:root", "644", "etc/modprobe.d/can.conf")
         copyfile("root:root", "644",
                  "var/nvidia/nvcam/settings/camera_overrides.isp")
         copyfile("root:root", "644", "/etc/ld.so.conf.d/yocto.conf")
@@ -1259,6 +1261,14 @@
         target(["systemctl", "enable", "frc971"])
         target(["systemctl", "enable", "frc971chrt"])
 
+        # Set up HW clock to use /dev/rtc0 and install hwclock service
+        target(["ln", "-sf", "/dev/rtc0", "/dev/rtc"])
+        target_unescaped(
+            "sed -i s/ATTR{hctosys}==\\\"1\\\"/ATTR{hctosys}==\\\"0\\\"/ /lib/udev/rules.d/50-udev-default.rules"
+        )
+        copyfile("root:root", "644", "etc/systemd/system/hwclock.service")
+        target(["systemctl", "enable", "hwclock"])
+
         target(["apt-file", "update"])
 
         target(["ldconfig"])
diff --git a/frc971/orin/contents/etc/modprobe.d/audio.conf b/frc971/orin/contents/etc/modprobe.d/audio.conf
new file mode 100644
index 0000000..1a0f72b
--- /dev/null
+++ b/frc971/orin/contents/etc/modprobe.d/audio.conf
@@ -0,0 +1 @@
+blacklist snd_hda_tegra
diff --git a/frc971/orin/contents/etc/modprobe.d/can.conf b/frc971/orin/contents/etc/modprobe.d/can.conf
new file mode 100644
index 0000000..4dd5db2
--- /dev/null
+++ b/frc971/orin/contents/etc/modprobe.d/can.conf
@@ -0,0 +1 @@
+options peak_pciefd irq_batch_max_messages=1
diff --git a/frc971/orin/contents/etc/systemd/network/80-cana.network b/frc971/orin/contents/etc/systemd/network/80-cana.network
index 0e4ad11..56f692a 100644
--- a/frc971/orin/contents/etc/systemd/network/80-cana.network
+++ b/frc971/orin/contents/etc/systemd/network/80-cana.network
@@ -5,3 +5,6 @@
 BitRate=1M
 RestartSec=1000ms
 BusErrorReporting=yes
+
+[Link]
+RequiredForOnline=no
diff --git a/frc971/orin/contents/etc/systemd/network/80-canb.network b/frc971/orin/contents/etc/systemd/network/80-canb.network
index 1bef0ee..55bc297 100644
--- a/frc971/orin/contents/etc/systemd/network/80-canb.network
+++ b/frc971/orin/contents/etc/systemd/network/80-canb.network
@@ -7,3 +7,6 @@
 RestartSec=1000ms
 BusErrorReporting=yes
 FDMode=yes
+
+[Link]
+RequiredForOnline=no
diff --git a/frc971/orin/contents/etc/systemd/network/80-canc.network b/frc971/orin/contents/etc/systemd/network/80-canc.network
index cdcbe81..4d43e47 100644
--- a/frc971/orin/contents/etc/systemd/network/80-canc.network
+++ b/frc971/orin/contents/etc/systemd/network/80-canc.network
@@ -7,3 +7,6 @@
 RestartSec=1000ms
 BusErrorReporting=yes
 FDMode=yes
+
+[Link]
+RequiredForOnline=no
diff --git a/frc971/orin/contents/etc/systemd/system/hwclock.service b/frc971/orin/contents/etc/systemd/system/hwclock.service
new file mode 100644
index 0000000..40c446f
--- /dev/null
+++ b/frc971/orin/contents/etc/systemd/system/hwclock.service
@@ -0,0 +1,8 @@
+[Unit]
+Description=Hardware clock synchronization
+[Service]
+Type=oneshot
+ExecStart=/sbin/hwclock --hctosys --utc --noadjfile
+ExecStop=/sbin/hwclock --systohc --utc --noadjfile
+[Install]
+WantedBy=multi-user.target
diff --git a/frc971/orin/cuda_april_tag_test.cc b/frc971/orin/cuda_april_tag_test.cc
index b30943f..638bf6a 100644
--- a/frc971/orin/cuda_april_tag_test.cc
+++ b/frc971/orin/cuda_april_tag_test.cc
@@ -8,6 +8,8 @@
 #include "third_party/apriltag/apriltag.h"
 #include "third_party/apriltag/common/unionfind.h"
 #include "third_party/apriltag/tag16h5.h"
+#include "third_party/apriltag/tag36h11.h"
+#include <opencv2/calib3d.hpp>
 #include <opencv2/highgui.hpp>
 
 #include "aos/flatbuffer_merge.h"
@@ -243,17 +245,35 @@
   return tag_detector;
 }
 
+// TODO(max): Create a function which will take in the calibration data
+CameraMatrix create_camera_matrix() {
+  return CameraMatrix{
+      1,
+      1,
+      1,
+      1,
+  };
+}
+
+DistCoeffs create_distortion_coefficients() {
+  return DistCoeffs{
+      0, 0, 0, 0, 0,
+  };
+}
+
 class CudaAprilTagDetector {
  public:
-  CudaAprilTagDetector(size_t width, size_t height)
-      : tag_family_(tag16h5_create()),
+  CudaAprilTagDetector(size_t width, size_t height,
+                       apriltag_family_t *tag_family = tag16h5_create())
+      : tag_family_(tag_family),
         tag_detector_(MakeTagDetector(tag_family_)),
         gray_cuda_(cv::Size(width, height), CV_8UC1),
         decimated_cuda_(gray_cuda_.size() / 2, CV_8UC1),
         thresholded_cuda_(decimated_cuda_.size(), CV_8UC1),
         union_markers_(decimated_cuda_.size(), CV_32SC1),
         union_markers_size_(decimated_cuda_.size(), CV_32SC1),
-        gpu_detector_(width, height, tag_detector_),
+        gpu_detector_(width, height, tag_detector_, create_camera_matrix(),
+                      create_distortion_coefficients()),
         width_(width),
         height_(height) {
     // Report out info about our GPU.
@@ -1716,6 +1736,8 @@
     }
   }
 
+  void set_undistort(bool value) { undistort_ = value; }
+
   void CheckDetections(zarray_t *aprilrobotics_detections,
                        const zarray_t *_gpu_detections) {
     zarray_t *gpu_detections = zarray_copy(_gpu_detections);
@@ -1757,7 +1779,7 @@
 
       // TODO(austin): Crank down the thresholds and figure out why these
       // deviate.  It should be the same function for both at this point.
-      const double threshold = valid ? 2e-3 : 1e-1;
+      const double threshold = undistort_ ? 15.0 : (valid ? 2e-3 : 1e-1);
 
       CHECK_EQ(aprilrobotics_detection->id, gpu_detection->id);
       CHECK_EQ(aprilrobotics_detection->hamming, gpu_detection->hamming);
@@ -1869,7 +1891,9 @@
     zarray_t *april_clusters =
         OrderAprilroboticsLikeCuda(thresholded_im, uf, cuda_grouped_points);
 
-    CheckQuads(april_clusters, sorted_selected_blobs_cuda_, fit_quads_);
+    if (!undistort_) {
+      CheckQuads(april_clusters, sorted_selected_blobs_cuda_, fit_quads_);
+    }
 
     const zarray_t *gpu_detections = gpu_detector_.Detections();
     CheckDetections(aprilrobotics_detections_, gpu_detections);
@@ -2045,6 +2069,14 @@
     }
   }
 
+  // Sets the camera constants for camera 24-04
+  void SetCameraFourConstants() {
+    gpu_detector_.SetCameraMatrix(
+        CameraMatrix{642.80365, 718.017517, 642.83667, 555.022461});
+    gpu_detector_.SetDistortionCoefficients(
+        DistCoeffs{-0.239969, 0.055889, 0.000086, 0.000099, -0.005468});
+  }
+
  private:
   apriltag_family_t *tag_family_;
   apriltag_detector_t *tag_detector_;
@@ -2082,6 +2114,8 @@
 
   bool normal_border_ = false;
   bool reversed_border_ = false;
+
+  bool undistort_ = false;
   int min_tag_width_ = 1000000;
 };
 
@@ -2236,4 +2270,52 @@
   EXPECT_EQ(overall_count, MaxRankedIndex());
 }
 
+// Tests our Undistort is working properly
+TEST_F(AprilDetectionTest, Undistort) {
+  auto image = ReadImage("orin_capture_24_04/file/orin_capture_24_04.bfbs");
+
+  LOG(INFO) << "Image is: " << image.message().cols() << " x "
+            << image.message().rows();
+
+  CudaAprilTagDetector cuda_detector(image.message().cols(),
+                                     image.message().rows(), tag36h11_create());
+
+  cuda_detector.set_undistort(true);
+
+  const cv::Mat color_image = ToMat(&image.message());
+
+  cuda_detector.SetCameraFourConstants();
+
+  cuda_detector.DetectGPU(color_image.clone());
+  cuda_detector.DetectCPU(color_image.clone());
+  cuda_detector.Check(color_image.clone());
+  if (FLAGS_debug) {
+    cuda_detector.WriteDebug(color_image);
+  }
+}
+
+// Tests our Undistort is working properly with a tag at the edge of the image
+TEST_F(AprilDetectionTest, UndistortEdge) {
+  auto image =
+      ReadImage("orin_capture_24_04_side/file/orin_capture_24_04_side.bfbs");
+
+  LOG(INFO) << "Image is: " << image.message().cols() << " x "
+            << image.message().rows();
+
+  CudaAprilTagDetector cuda_detector(image.message().cols(),
+                                     image.message().rows(), tag36h11_create());
+
+  cuda_detector.set_undistort(true);
+
+  const cv::Mat color_image = ToMat(&image.message());
+
+  cuda_detector.SetCameraFourConstants();
+
+  cuda_detector.DetectGPU(color_image.clone());
+  cuda_detector.DetectCPU(color_image.clone());
+  cuda_detector.Check(color_image.clone());
+  if (FLAGS_debug) {
+    cuda_detector.WriteDebug(color_image);
+  }
+}
 }  // namespace frc971::apriltag::testing
diff --git a/frc971/orin/gpu_apriltag.cc b/frc971/orin/gpu_apriltag.cc
index 89625b6..27cab4d 100644
--- a/frc971/orin/gpu_apriltag.cc
+++ b/frc971/orin/gpu_apriltag.cc
@@ -40,15 +40,42 @@
 
 namespace chrono = std::chrono;
 
+CameraMatrix GetCameraMatrix(
+    const frc971::vision::calibration::CameraCalibration *calibration) {
+  auto intrinsics = calibration->intrinsics();
+  return CameraMatrix{
+      .fx = intrinsics->Get(0),
+      .cx = intrinsics->Get(2),
+      .fy = intrinsics->Get(4),
+      .cy = intrinsics->Get(5),
+  };
+}
+
+DistCoeffs GetDistCoeffs(
+    const frc971::vision::calibration::CameraCalibration *calibration) {
+  auto dist_coeffs = calibration->dist_coeffs();
+  return DistCoeffs{
+      .k1 = dist_coeffs->Get(0),
+      .k2 = dist_coeffs->Get(1),
+      .p1 = dist_coeffs->Get(2),
+      .p2 = dist_coeffs->Get(3),
+      .k3 = dist_coeffs->Get(4),
+  };
+}
+
 ApriltagDetector::ApriltagDetector(
     aos::EventLoop *event_loop, std::string_view channel_name,
     const frc971::vision::calibration::CameraCalibration *calibration,
     size_t width, size_t height)
     : tag_family_(tag36h11_create()),
       tag_detector_(MakeTagDetector(tag_family_)),
-      gpu_detector_(width, height, tag_detector_),
       node_name_(event_loop->node()->name()->string_view()),
       calibration_(calibration),
+      intrinsics_(frc971::vision::CameraIntrinsics(calibration_)),
+      extrinsics_(frc971::vision::CameraExtrinsics(calibration_)),
+      dist_coeffs_(frc971::vision::CameraDistCoeffs(calibration_)),
+      gpu_detector_(width, height, tag_detector_, GetCameraMatrix(calibration_),
+                    GetDistCoeffs(calibration_)),
       image_callback_(
           event_loop, channel_name,
           [this](cv::Mat image_color_mat,
@@ -63,10 +90,6 @@
       rejections_(0) {
   image_callback_.set_format(frc971::vision::ImageCallback::Format::YUYV2);
 
-  extrinsics_ = frc971::vision::CameraExtrinsics(calibration_);
-  intrinsics_ = frc971::vision::CameraIntrinsics(calibration_);
-  dist_coeffs_ = frc971::vision::CameraDistCoeffs(calibration_);
-
   projection_matrix_ = cv::Mat::zeros(3, 4, CV_64F);
   intrinsics_.rowRange(0, 3).colRange(0, 3).copyTo(
       projection_matrix_.rowRange(0, 3).colRange(0, 3));
@@ -245,7 +268,7 @@
       // parameters.
       apriltag_detection_info_t info;
       info.det = gpu_detection;
-      info.tagsize = 0.1524;
+      info.tagsize = 6.5 * 0.0254;
 
       info.fx = intrinsics_.at<double>(0, 0);
       info.fy = intrinsics_.at<double>(1, 1);
diff --git a/frc971/orin/gpu_apriltag.h b/frc971/orin/gpu_apriltag.h
index 09c8579..39d2b60 100644
--- a/frc971/orin/gpu_apriltag.h
+++ b/frc971/orin/gpu_apriltag.h
@@ -73,7 +73,6 @@
  private:
   apriltag_family_t *tag_family_;
   apriltag_detector_t *tag_detector_;
-  frc971::apriltag::GpuDetector gpu_detector_;
   std::string_view node_name_;
 
   const frc971::vision::calibration::CameraCalibration *calibration_;
@@ -81,6 +80,8 @@
   cv::Mat projection_matrix_;
   std::optional<cv::Mat> extrinsics_;
   cv::Mat dist_coeffs_;
+
+  frc971::apriltag::GpuDetector gpu_detector_;
   cv::Size image_size_;
 
   frc971::vision::ImageCallback image_callback_;
diff --git a/frc971/orin/orin_irq_config.json b/frc971/orin/orin_irq_config.json
new file mode 100644
index 0000000..0edb72b
--- /dev/null
+++ b/frc971/orin/orin_irq_config.json
@@ -0,0 +1,413 @@
+{
+  "irqs": [
+    {
+      /* Control BackBone */
+      "name": "13a00000.cbb-fabric",
+      "affinity": [0]
+    },
+    {
+      /* AON controller */
+      "name": "c600000.aon-fabric",
+      "affinity": [0]
+    },
+    {
+      /* Boot and Power Management Processor */
+      "name": "d600000.bpmp-fabric",
+      "affinity": [0]
+    },
+    {
+      /* Display Controller Engine */
+      "name": "d600000.bpmp-fabric",
+      "affinity": [0]
+    },
+    {
+      /* RCE is related to tracing */
+      "name": "be00000.rce-fabric",
+      "affinity": [0]
+    },
+    {
+      "name": "b600000.sce-fabric",
+      "affinity": [0]
+    },
+    {
+      /* PIPE to UPHY */
+      "name": "tegra-p2u-intr",
+      "affinity": [0]
+    },
+    {
+      "name": "peak_pciefd",
+      "affinity": [5]
+    },
+    {
+      "name": "cana",
+      "affinity": [4]
+    },
+    {
+      "name": "uart-pl011",
+      "affinity": [0]
+    },
+    {
+      /* Hardware mailboxes for IPC */
+      "name": "3c00000.tegra-hsp",
+      "affinity": [2, 3]
+    },
+    {
+      "name": "3210000.spi",
+      "affinity": [0, 1]
+    },
+    {
+      "name": "3230000.spi",
+      "affinity": [0, 1]
+    },
+    {
+      "name": "3160000.i2c",
+      "affinity": [2, 3]
+    },
+    {
+      "name": "c240000.i2c",
+      "affinity": [2, 3]
+    },
+    {
+      "name": "3180000.i2c",
+      "affinity": [2, 3]
+    },
+    {
+      "name": "3190000.i2c",
+      "affinity": [2, 3]
+    },
+    {
+      "name": "31b0000.i2c",
+      "affinity": [2, 3]
+    },
+    {
+      "name": "31c0000.i2c",
+      "affinity": [2, 3]
+    },
+    {
+      "name": "c250000.i2c",
+      "affinity": [2, 3]
+    },
+    {
+      "name": "31e0000.i2c",
+      "affinity": [2, 3]
+    },
+    {
+      /* General Purpose Central DMA controller */
+      "name": "gpcdma.0",
+      "affinity": [0]
+    },
+    {
+      "name": "gpcdma.1",
+      "affinity": [0]
+    },
+    {
+      "name": "gpcdma.2",
+      "affinity": [0]
+    },
+    {
+      "name": "gpcdma.3",
+      "affinity": [0]
+    },
+    {
+      "name": "gpcdma.4",
+      "affinity": [0]
+    },
+    {
+      "name": "gpcdma.5",
+      "affinity": [0]
+    },
+    {
+      "name": "gpcdma.6",
+      "affinity": [0]
+    },
+    {
+      "name": "gpcdma.7",
+      "affinity": [0]
+    },
+    {
+      "name": "gpcdma.8",
+      "affinity": [0]
+    },
+    {
+      "name": "gpcdma.9",
+      "affinity": [0]
+    },
+    {
+      "name": "gpcdma.10",
+      "affinity": [0]
+    },
+    {
+      "name": "gpcdma.11",
+      "affinity": [0]
+    },
+    {
+      "name": "gpcdma.12",
+      "affinity": [0]
+    },
+    {
+      "name": "gpcdma.13",
+      "affinity": [0]
+    },
+    {
+      "name": "gpcdma.14",
+      "affinity": [0]
+    },
+    {
+      "name": "gpcdma.15",
+      "affinity": [0]
+    },
+    {
+      "name": "gpcdma.16",
+      "affinity": [0]
+    },
+    {
+      "name": "gpcdma.17",
+      "affinity": [0]
+    },
+    {
+      "name": "gpcdma.18",
+      "affinity": [0]
+    },
+    {
+      "name": "gpcdma.19",
+      "affinity": [0]
+    },
+    {
+      "name": "bc00000.rtcpu",
+      "affinity": [0]
+    },
+    {
+      "name": "d230000.actmon",
+      "affinity": [0]
+    },
+    {
+      "name": "e860000.psc",
+      "affinity": [0]
+    },
+    {
+      "name": "host_syncpt",
+      "affinity": [2, 3]
+    },
+    {
+      "name": "c150000.tegra-hsp",
+      "affinity": [3]
+    },
+    {
+      "name": "3c00000.tegra-hsp",
+      "affinity": [3]
+    },
+    {
+      "name": "3d00000.tegra-hsp",
+      "affinity": [3]
+    },
+    {
+      "name": "b950000.tegra-hsp",
+      "affinity": [3]
+    },
+    {
+      "name": "2190000.watchdog",
+      "affinity": [0]
+    },
+    {
+      "name": "3610000.xhci",
+      "affinity": [0]
+    },
+    {
+      "name": "3550000.xudc",
+      "affinity": [0]
+    },
+    {
+      "name": "xhci-hcd:usb1",
+      "affinity": [0]
+    },
+    {
+      "name": "3610000.xhci",
+      "affinity": [0]
+    },
+    {
+      "name": "gk20a_stall",
+      "affinity": [2, 3]
+    },
+    {
+      "name": "eth0",
+      "affinity": [1]
+    }
+  ],
+  "kthreads": [
+    {
+      "name": "irq/*nvme0q0",
+      "scheduler": "SCHEDULER_OTHER"
+    },
+    {
+      "name": "irq/*nvme0q1",
+      "scheduler": "SCHEDULER_OTHER"
+    },
+    {
+      "name": "irq/*nvme0q2",
+      "scheduler": "SCHEDULER_OTHER"
+    },
+    {
+      "name": "irq/*nvme0q3",
+      "scheduler": "SCHEDULER_OTHER"
+    },
+    {
+      "name": "irq/*nvme0q4",
+      "scheduler": "SCHEDULER_OTHER"
+    },
+    {
+      "name": "irq/*nvme0q5",
+      "scheduler": "SCHEDULER_OTHER"
+    },
+    {
+      "name": "irq/*nvme0q6",
+      "scheduler": "SCHEDULER_OTHER"
+    },
+    {
+      "name": "irq/*-uart-pl0",
+      "scheduler": "SCHEDULER_OTHER",
+      "nice": -20
+    },
+    {
+      "name": "irq/*-eth0",
+      "scheduler": "SCHEDULER_FIFO",
+      "priority": 51,
+      "affinity": [1]
+    },
+    {
+      "name": "irq/*-cana",
+      "scheduler": "SCHEDULER_FIFO",
+      "priority": 51,
+      "affinity": [4]
+    },
+    {
+      "name": "irq/*-peak_pcie",
+      "scheduler": "SCHEDULER_FIFO",
+      "priority": 51,
+      "affinity": [5]
+    },
+    {
+      "name": "irq/*-PCIe PME",
+      "scheduler": "SCHEDULER_OTHER",
+      "nice": -20
+    },
+    {
+      "name": "irq/*-tegra-pc",
+      "scheduler": "SCHEDULER_OTHER",
+      "nice": -20
+    },
+    {
+      "name": "irq/*-s-tegra-",
+      "scheduler": "SCHEDULER_OTHER",
+      "nice": -20
+    },
+    {
+      "name": "irq/*-s-aerdrv",
+      "scheduler": "SCHEDULER_OTHER",
+      "nice": -20
+    },
+    {
+      "name": "irq/*-aerdrv",
+      "scheduler": "SCHEDULER_OTHER",
+      "nice": -20
+    },
+    {
+      "name": "irq/*-tegra-p2",
+      "scheduler": "SCHEDULER_OTHER",
+      "nice": -20
+    },
+    {
+      "name": "irq/*-host_sy",
+      "scheduler": "SCHEDULER_FIFO",
+      "affinity": [2, 3],
+      "priority": 51
+    },
+    {
+      "name": "irq/*-b950000",
+      "scheduler": "SCHEDULER_FIFO",
+      "affinity": [2, 3],
+      "priority": 51
+    },
+    {
+      "name": "ivc/*.rtc",
+      "scheduler": "SCHEDULER_FIFO",
+      "nice": 49
+    },
+    {
+      "name": "nvgpu_nvs_ga10b",
+      "scheduler": "SCHEDULER_OTHER",
+      "affinity": [2, 3],
+      "nice": 0
+    },
+    {
+      "name": "irq/*-gpcdma.0",
+      "scheduler": "SCHEDULER_OTHER",
+      "nice": 0
+    },
+    {
+      "name": "irq/*-gpcdma.1",
+      "scheduler": "SCHEDULER_OTHER",
+      "nice": 0
+    },
+    {
+      "name": "irq/*-gpcdma.2",
+      "scheduler": "SCHEDULER_OTHER",
+      "nice": 0
+    },
+    {
+      "name": "irq/*-gpcdma.3",
+      "scheduler": "SCHEDULER_OTHER",
+      "nice": 0
+    },
+    {
+      "name": "irq/*-gpcdma.4",
+      "scheduler": "SCHEDULER_OTHER",
+      "nice": 0
+    },
+    {
+      "name": "irq/*-gpcdma.5",
+      "scheduler": "SCHEDULER_OTHER",
+      "nice": 0
+    },
+    {
+      "name": "irq/*-gpcdma.6",
+      "scheduler": "SCHEDULER_OTHER",
+      "nice": 0
+    },
+    {
+      "name": "irq/*-gpcdma.7",
+      "scheduler": "SCHEDULER_OTHER",
+      "nice": 0
+    },
+    {
+      "name": "irq/*-gpcdma.8",
+      "scheduler": "SCHEDULER_OTHER",
+      "nice": 0
+    },
+    {
+      "name": "irq/*-gpcdma.9",
+      "scheduler": "SCHEDULER_OTHER",
+      "nice": 0
+    },
+    {
+      "name": "irq/*-s-gk20a",
+      "scheduler": "SCHEDULER_OTHER",
+      "nice": 0
+    },
+    {
+      "name": "irq/*-gk20a_n",
+      "scheduler": "SCHEDULER_OTHER",
+      "nice": 0
+    },
+    {
+      "name": "nvmap-bz*",
+      "scheduler": "SCHEDULER_FIFO",
+      "priority": 50
+    },
+    {
+      "name": "nvgpu_channel_p",
+      "scheduler": "SCHEDULER_OTHER",
+      "affinity": [2, 3],
+      "nice": 0
+    }
+  ]
+}
diff --git a/frc971/orin/set_orin_clock.sh b/frc971/orin/set_orin_clock.sh
new file mode 100755
index 0000000..32edb86
--- /dev/null
+++ b/frc971/orin/set_orin_clock.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+# Helper script to set orins clock based on our local timestamp
+
+set -e
+
+ROBOT_PREFIX="9" #71  (Should be one of 79, 89, 99, or 9)
+
+ORIN_LIST="1"
+
+echo "Setting hwclock on Orins"
+
+for orin in $ORIN_LIST; do
+    echo "========================================================"
+    echo "Setting clock for ${ROBOT_PREFIX}71.1${orin}"
+    echo "========================================================"
+    current_time=`sudo hwclock`
+    IFS="."
+    read -ra split_time <<< "$current_time"
+    echo "Setting time to ${split_time[0]}"
+    ssh pi@10.${ROBOT_PREFIX}.71.10${orin} "sudo timedatectl set-timezone US/Pacific"
+    ssh pi@10.${ROBOT_PREFIX}.71.10${orin} "sudo hwclock --set --date \"${split_time[0]}\""
+    ssh pi@10.${ROBOT_PREFIX}.71.10${orin} "sudo hwclock -s"
+done
diff --git a/frc971/vision/BUILD b/frc971/vision/BUILD
index 6cee780..d96822b 100644
--- a/frc971/vision/BUILD
+++ b/frc971/vision/BUILD
@@ -328,6 +328,7 @@
         "//frc971/control_loops/drivetrain:improved_down_estimator",
         "//frc971/vision:charuco_lib",
         "//frc971/vision:vision_fbs",
+        "//frc971/vision:vision_util_lib",
         "//frc971/wpilib:imu_batch_fbs",
         "//frc971/wpilib:imu_fbs",
         "//third_party:opencv",
@@ -374,3 +375,25 @@
         "@com_github_google_glog//:glog",
     ],
 )
+
+cc_test(
+    name = "vision_util_lib_test",
+    srcs = ["vision_util_lib_test.cc"],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//aos/testing:googletest",
+        "//frc971/vision:vision_util_lib",
+        "@com_github_google_glog//:glog",
+    ],
+)
+
+cc_library(
+    name = "target_map_utils",
+    srcs = ["target_map_utils.cc"],
+    hdrs = ["target_map_utils.h"],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//frc971/vision:target_map_fbs",
+        "@org_tuxfamily_eigen//:eigen",
+    ],
+)
diff --git a/frc971/vision/calibration.fbs b/frc971/vision/calibration.fbs
index df8b7b0..99b6228 100644
--- a/frc971/vision/calibration.fbs
+++ b/frc971/vision/calibration.fbs
@@ -8,7 +8,7 @@
 
 // Calibration information for a given camera on a given robot.
 table CameraCalibration {
-  // The name of the camera node which this calibration data applies to.
+  // The name of the compute node which this calibration data applies to.
   node_name:string (id: 0);
   // The team number of the robot which this calibration data applies to.
   team_number:int (id: 1);
@@ -48,6 +48,12 @@
   // ID for the physical camera hardware (typically will be a string of the form
   // YY-NN, with a two-digit year and an index).
   camera_id:string (id: 7);
+
+  // The camera number.  This may be empty, "0", or "1".
+  camera_number:int (id: 8);
+
+  // The reprojection error associated with the camera calibration.
+  reprojection_error:float (id: 9);
 }
 
 // Calibration information for all the cameras we know about.
diff --git a/frc971/vision/calibration_accumulator.cc b/frc971/vision/calibration_accumulator.cc
index 9c6222b..13481b4 100644
--- a/frc971/vision/calibration_accumulator.cc
+++ b/frc971/vision/calibration_accumulator.cc
@@ -114,12 +114,13 @@
 }
 
 CalibrationFoxgloveVisualizer::CalibrationFoxgloveVisualizer(
-    aos::EventLoop *event_loop)
+    aos::EventLoop *event_loop, std::string_view camera_channel)
     : event_loop_(event_loop),
-      image_converter_(event_loop_, "/camera", "/camera",
+      image_converter_(event_loop_, camera_channel, camera_channel,
                        ImageCompression::kJpeg),
       annotations_sender_(
-          event_loop_->MakeSender<foxglove::ImageAnnotations>("/camera")) {}
+          event_loop_->MakeSender<foxglove::ImageAnnotations>(camera_channel)) {
+}
 
 aos::FlatbufferDetachedBuffer<aos::Configuration>
 CalibrationFoxgloveVisualizer::AddVisualizationChannels(
@@ -175,7 +176,7 @@
           }),
       data_(data),
       visualizer_event_loop_(image_factory_->MakeEventLoop("visualization")),
-      visualizer_(visualizer_event_loop_.get()) {
+      visualizer_(visualizer_event_loop_.get(), image_channel) {
   imu_factory_->OnShutdown([]() { cv::destroyAllWindows(); });
 
   // Check for IMUValuesBatch topic on both /localizer and /drivetrain channels,
diff --git a/frc971/vision/calibration_accumulator.h b/frc971/vision/calibration_accumulator.h
index 550b691..dbed9ae 100644
--- a/frc971/vision/calibration_accumulator.h
+++ b/frc971/vision/calibration_accumulator.h
@@ -79,7 +79,8 @@
 
 class CalibrationFoxgloveVisualizer {
  public:
-  CalibrationFoxgloveVisualizer(aos::EventLoop *event_loop);
+  CalibrationFoxgloveVisualizer(aos::EventLoop *event_loop,
+                                std::string_view camera_channel = "/camera");
 
   static aos::FlatbufferDetachedBuffer<aos::Configuration>
   AddVisualizationChannels(const aos::Configuration *config,
diff --git a/frc971/vision/intrinsics_calibration.cc b/frc971/vision/intrinsics_calibration.cc
index f98aadb..16a53a7 100644
--- a/frc971/vision/intrinsics_calibration.cc
+++ b/frc971/vision/intrinsics_calibration.cc
@@ -13,15 +13,17 @@
 #include "aos/util/file.h"
 #include "frc971/vision/intrinsics_calibration_lib.h"
 
-DEFINE_string(calibration_folder, ".", "Folder to place calibration files.");
-DEFINE_string(camera_id, "", "Camera ID in format YY-NN-- year and number.");
-DEFINE_string(config, "aos_config.json", "Path to the config file to use.");
-DEFINE_bool(display_undistorted, false,
-            "If true, display the undistorted image.");
-DEFINE_string(pi, "", "Pi name to calibrate.");
+// TODO: Would be nice to remove this, but it depends on year-by-year Constants
 DEFINE_string(base_intrinsics, "",
               "Intrinsics to use for estimating board pose prior to solving "
               "for the new intrinsics.");
+DEFINE_string(calibration_folder, ".", "Folder to place calibration files.");
+DEFINE_string(camera_id, "", "Camera ID in format YY-NN-- year and number.");
+DEFINE_string(channel, "/camera", "Camera channel to use");
+DEFINE_string(config, "aos_config.json", "Path to the config file to use.");
+DEFINE_string(cpu_name, "", "Pi/Orin name to calibrate.");
+DEFINE_bool(display_undistorted, false,
+            "If true, display the undistorted image.");
 
 namespace frc971::vision {
 namespace {
@@ -32,7 +34,7 @@
 
   aos::ShmEventLoop event_loop(&config.message());
 
-  std::string hostname = FLAGS_pi;
+  std::string hostname = FLAGS_cpu_name;
   if (hostname == "") {
     hostname = aos::network::GetHostname();
     LOG(INFO) << "Using pi/orin name from hostname as " << hostname;
@@ -41,9 +43,10 @@
       << "Need a base intrinsics json to use to auto-capture images when the "
          "camera moves.";
   std::unique_ptr<aos::ExitHandle> exit_handle = event_loop.MakeExitHandle();
-  IntrinsicsCalibration extractor(
-      &event_loop, hostname, FLAGS_camera_id, FLAGS_base_intrinsics,
-      FLAGS_display_undistorted, FLAGS_calibration_folder, exit_handle.get());
+  IntrinsicsCalibration extractor(&event_loop, hostname, FLAGS_channel,
+                                  FLAGS_camera_id, FLAGS_base_intrinsics,
+                                  FLAGS_display_undistorted,
+                                  FLAGS_calibration_folder, exit_handle.get());
 
   event_loop.Run();
 
diff --git a/frc971/vision/intrinsics_calibration_lib.cc b/frc971/vision/intrinsics_calibration_lib.cc
index a5ffec7..c48c12d 100644
--- a/frc971/vision/intrinsics_calibration_lib.cc
+++ b/frc971/vision/intrinsics_calibration_lib.cc
@@ -1,15 +1,23 @@
 #include "frc971/vision/intrinsics_calibration_lib.h"
 
+DECLARE_bool(visualize);
+
 namespace frc971::vision {
 
+// Found that under 50 ms would fail image too often on intrinsics with
+// visualize on
+constexpr aos::monotonic_clock::duration kMaxImageAge =
+    aos::monotonic_clock::duration(std::chrono::milliseconds(50));
+
 IntrinsicsCalibration::IntrinsicsCalibration(
     aos::EventLoop *event_loop, std::string_view hostname,
-    std::string_view camera_id, std::string_view base_intrinsics_file,
-    bool display_undistorted, std::string_view calibration_folder,
-    aos::ExitHandle *exit_handle)
+    std::string_view camera_channel, std::string_view camera_id,
+    std::string_view base_intrinsics_file, bool display_undistorted,
+    std::string_view calibration_folder, aos::ExitHandle *exit_handle)
     : hostname_(hostname),
       cpu_type_(aos::network::ParsePiOrOrin(hostname_)),
       cpu_number_(aos::network::ParsePiOrOrinNumber(hostname_)),
+      camera_channel_(camera_channel),
       camera_id_(camera_id),
       prev_H_camera_board_(Eigen::Affine3d()),
       prev_image_H_camera_board_(Eigen::Affine3d()),
@@ -18,7 +26,7 @@
               base_intrinsics_file)),
       charuco_extractor_(
           event_loop, &base_intrinsics_.message(), TargetType::kCharuco,
-          "/camera",
+          camera_channel_,
           [this](cv::Mat rgb_image, const aos::monotonic_clock::time_point eof,
                  std::vector<cv::Vec4i> charuco_ids,
                  std::vector<std::vector<cv::Point2f>> charuco_corners,
@@ -30,17 +38,18 @@
       image_callback_(
           event_loop,
           absl::StrCat("/", aos::network::ParsePiOrOrin(hostname_).value(),
-                       // TODO: Need to make this work with multiple cameras
-                       std::to_string(cpu_number_.value()), "/camera"),
+                       std::to_string(cpu_number_.value()), camera_channel_),
           [this](cv::Mat rgb_image,
                  const aos::monotonic_clock::time_point eof) {
             charuco_extractor_.HandleImage(rgb_image, eof);
           },
-          std::chrono::milliseconds(5)),
+          kMaxImageAge),
       display_undistorted_(display_undistorted),
       calibration_folder_(calibration_folder),
       exit_handle_(exit_handle) {
-  LOG(INFO) << "Hostname is: " << hostname_;
+  LOG(INFO) << "Hostname is: " << hostname_ << " and camera channel is "
+            << camera_channel_;
+
   CHECK(cpu_number_) << ": Invalid cpu number " << hostname_
                      << ", failed to parse cpu number";
   std::regex re{"^[0-9][0-9]-[0-9][0-9]"};
@@ -54,25 +63,28 @@
     std::vector<std::vector<cv::Point2f>> charuco_corners, bool valid,
     std::vector<Eigen::Vector3d> rvecs_eigen,
     std::vector<Eigen::Vector3d> tvecs_eigen) {
-  // Reduce resolution displayed on remote viewer to prevent lag
-  cv::resize(rgb_image, rgb_image,
-             cv::Size(rgb_image.cols / 2, rgb_image.rows / 2));
-  cv::imshow("Display", rgb_image);
+  if (FLAGS_visualize) {
+    // Reduce resolution displayed on remote viewer to prevent lag
+    cv::resize(rgb_image, rgb_image,
+               cv::Size(rgb_image.cols / 2, rgb_image.rows / 2));
+    cv::imshow("Display", rgb_image);
 
-  if (display_undistorted_) {
-    const cv::Size image_size(rgb_image.cols, rgb_image.rows);
-    cv::Mat undistorted_rgb_image(image_size, CV_8UC3);
-    cv::undistort(rgb_image, undistorted_rgb_image,
-                  charuco_extractor_.camera_matrix(),
-                  charuco_extractor_.dist_coeffs());
+    if (display_undistorted_) {
+      const cv::Size image_size(rgb_image.cols, rgb_image.rows);
+      cv::Mat undistorted_rgb_image(image_size, CV_8UC3);
+      cv::undistort(rgb_image, undistorted_rgb_image,
+                    charuco_extractor_.camera_matrix(),
+                    charuco_extractor_.dist_coeffs());
 
-    cv::imshow("Display undist", undistorted_rgb_image);
+      cv::imshow("Display undist", undistorted_rgb_image);
+    }
   }
 
   int keystroke = cv::waitKey(1);
 
   // If we haven't got a valid pose estimate, don't use these points
   if (!valid) {
+    LOG(INFO) << "Skipping because pose is not valid";
     return;
   }
   CHECK(tvecs_eigen.size() == 1)
@@ -99,8 +111,8 @@
   bool store_image = false;
   double percent_motion =
       std::max<double>(r_norm / kDeltaRThreshold, t_norm / kDeltaTThreshold);
-  LOG(INFO) << "Captured: " << all_charuco_ids_.size() << " points; Moved "
-            << percent_motion << "% of what's needed";
+  LOG(INFO) << "Captured: " << all_charuco_ids_.size() << " points; \nMoved "
+            << static_cast<int>(percent_motion * 100) << "% of what's needed";
   // Verify that camera has moved enough from last stored image
   if (r_norm > kDeltaRThreshold || t_norm > kDeltaTThreshold) {
     // frame_ refers to deltas between current and last captured image
@@ -120,9 +132,11 @@
     double percent_stop = std::max<double>(frame_r_norm / kFrameDeltaRLimit,
                                            frame_t_norm / kFrameDeltaTLimit);
     LOG(INFO) << "Captured: " << all_charuco_ids_.size()
-              << "points; Moved enough (" << percent_motion
-              << "%); Need to stop (last motion was " << percent_stop
-              << "% of limit; needs to be < 1 to capture)";
+              << "points; \nMoved enough ("
+              << static_cast<int>(percent_motion * 100)
+              << "%); Need to stop (last motion was "
+              << static_cast<int>(percent_stop * 100)
+              << "% of limit; needs to be < 1% to capture)";
   }
   prev_image_H_camera_board_ = H_camera_board_;
 
@@ -157,7 +171,9 @@
 IntrinsicsCalibration::BuildCalibration(
     cv::Mat camera_matrix, cv::Mat dist_coeffs,
     aos::realtime_clock::time_point realtime_now, std::string_view cpu_type,
-    int cpu_number, std::string_view camera_id, uint16_t team_number) {
+    uint16_t cpu_number, std::string_view camera_channel,
+    std::string_view camera_id, uint16_t team_number,
+    double reprojection_error) {
   flatbuffers::FlatBufferBuilder fbb;
   flatbuffers::Offset<flatbuffers::String> name_offset =
       fbb.CreateString(absl::StrFormat("%s%d", cpu_type, cpu_number));
@@ -169,6 +185,9 @@
             reinterpret_cast<double *>(camera_matrix.data)[i]);
       });
 
+  std::optional<uint16_t> camera_number =
+      frc971::vision::CameraNumberFromChannel(std::string(camera_channel));
+
   flatbuffers::Offset<flatbuffers::Vector<float>>
       distortion_coefficients_offset =
           fbb.CreateVector<float>(5u, [&dist_coeffs](size_t i) {
@@ -180,7 +199,10 @@
 
   camera_calibration_builder.add_node_name(name_offset);
   camera_calibration_builder.add_team_number(team_number);
+  camera_calibration_builder.add_camera_number(camera_number.value());
   camera_calibration_builder.add_camera_id(camera_id_offset);
+  camera_calibration_builder.add_reprojection_error(
+      static_cast<float>(reprojection_error));
   camera_calibration_builder.add_calibration_timestamp(
       realtime_now.time_since_epoch().count());
   camera_calibration_builder.add_intrinsics(intrinsics_offset);
@@ -209,7 +231,7 @@
         img_size, camera_matrix, dist_coeffs, rvecs, tvecs,
         std_deviations_intrinsics, std_deviations_extrinsics, per_view_errors,
         calibration_flags);
-    CHECK_LE(reprojection_error, 1.0)
+    CHECK_LE(reprojection_error, 2.0)
         << ": Reproduction error is bad-- greater than 1 pixel.";
     LOG(INFO) << "Reprojection Error is " << reprojection_error;
 
@@ -222,15 +244,23 @@
     aos::FlatbufferDetachedBuffer<calibration::CameraCalibration>
         camera_calibration = BuildCalibration(
             camera_matrix, dist_coeffs, realtime_now, cpu_type_.value(),
-            cpu_number_.value(), camera_id_, team_number.value());
+            cpu_number_.value(), camera_channel_, camera_id_,
+            team_number.value(), reprojection_error);
     std::stringstream time_ss;
     time_ss << realtime_now;
 
+    std::string camera_number_optional = "";
+    std::optional<uint16_t> camera_number =
+        frc971::vision::CameraNumberFromChannel(camera_channel_);
+    if (camera_number != std::nullopt) {
+      camera_number_optional = "-" + std::to_string(camera_number.value());
+    }
     const std::string calibration_filename =
         calibration_folder_ +
-        absl::StrFormat("/calibration_%s-%d-%d_cam-%s_%s.json",
+        absl::StrFormat("/calibration_%s-%d-%d%s_cam-%s_%s.json",
                         cpu_type_.value(), team_number.value(),
-                        cpu_number_.value(), camera_id_, time_ss.str());
+                        cpu_number_.value(), camera_number_optional, camera_id_,
+                        time_ss.str());
 
     LOG(INFO) << calibration_filename << " -> "
               << aos::FlatbufferToJson(camera_calibration,
diff --git a/frc971/vision/intrinsics_calibration_lib.h b/frc971/vision/intrinsics_calibration_lib.h
index 7f08138..605741f 100644
--- a/frc971/vision/intrinsics_calibration_lib.h
+++ b/frc971/vision/intrinsics_calibration_lib.h
@@ -15,12 +15,14 @@
 #include "aos/time/time.h"
 #include "aos/util/file.h"
 #include "frc971/vision/charuco_lib.h"
+#include "frc971/vision/vision_util_lib.h"
 
 namespace frc971::vision {
 
 class IntrinsicsCalibration {
  public:
   IntrinsicsCalibration(aos::EventLoop *event_loop, std::string_view hostname,
+                        std::string_view camera_channel,
                         std::string_view camera_id,
                         std::string_view base_intrinsics_file,
                         bool display_undistorted,
@@ -39,8 +41,9 @@
   static aos::FlatbufferDetachedBuffer<calibration::CameraCalibration>
   BuildCalibration(cv::Mat camera_matrix, cv::Mat dist_coeffs,
                    aos::realtime_clock::time_point realtime_now,
-                   std::string_view cpu_type, int cpu_number,
-                   std::string_view camera_id, uint16_t team_number);
+                   std::string_view cpu_type, uint16_t cpu_number,
+                   std::string_view camera_channel, std::string_view camera_id,
+                   uint16_t team_number, double reprojection_error);
 
  private:
   static constexpr double kDeltaRThreshold = M_PI / 6.0;
@@ -52,6 +55,7 @@
   std::string hostname_;
   const std::optional<std::string_view> cpu_type_;
   const std::optional<uint16_t> cpu_number_;
+  const std::string camera_channel_;
   const std::string camera_id_;
 
   std::vector<std::vector<int>> all_charuco_ids_;
diff --git a/y2023/localizer/utils.cc b/frc971/vision/target_map_utils.cc
similarity index 70%
rename from y2023/localizer/utils.cc
rename to frc971/vision/target_map_utils.cc
index 7faca1f..3800628 100644
--- a/y2023/localizer/utils.cc
+++ b/frc971/vision/target_map_utils.cc
@@ -1,6 +1,6 @@
-#include "y2023/localizer/utils.h"
+#include "frc971/vision/target_map_utils.h"
 
-namespace y2023::localizer {
+namespace frc971::vision {
 Eigen::Matrix<double, 4, 4> PoseToTransform(
     const frc971::vision::TargetPoseFbs *pose) {
   const frc971::vision::Position *position = pose->position();
@@ -8,7 +8,8 @@
   return (Eigen::Translation3d(
               Eigen::Vector3d(position->x(), position->y(), position->z())) *
           Eigen::Quaterniond(quaternion->w(), quaternion->x(), quaternion->y(),
-                             quaternion->z()))
+                             quaternion->z())
+              .normalized())
       .matrix();
 }
-}  // namespace y2023::localizer
+}  // namespace frc971::vision
diff --git a/frc971/vision/target_map_utils.h b/frc971/vision/target_map_utils.h
new file mode 100644
index 0000000..36b4e30
--- /dev/null
+++ b/frc971/vision/target_map_utils.h
@@ -0,0 +1,14 @@
+#ifndef FRC971_VISION_TARGET_MAP_UTILS_H_
+#define FRC971_VISION_TARGET_MAP_UTILS_H_
+
+#include <Eigen/Dense>
+
+#include "frc971/vision/target_map_generated.h"
+
+namespace frc971::vision {
+// Converts a TargetPoseFbs into a transformation matrix.
+Eigen::Matrix<double, 4, 4> PoseToTransform(
+    const frc971::vision::TargetPoseFbs *pose);
+}  // namespace frc971::vision
+
+#endif  // FRC971_VISION_TARGET_MAP_UTILS_H_
diff --git a/frc971/vision/target_mapper.cc b/frc971/vision/target_mapper.cc
index 8077f16..c14b032 100644
--- a/frc971/vision/target_mapper.cc
+++ b/frc971/vision/target_mapper.cc
@@ -104,7 +104,8 @@
               Eigen::Quaterniond(target_pose_fbs.orientation()->w(),
                                  target_pose_fbs.orientation()->x(),
                                  target_pose_fbs.orientation()->y(),
-                                 target_pose_fbs.orientation()->z())}};
+                                 target_pose_fbs.orientation()->z())
+                  .normalized()}};
 }
 
 ceres::examples::VectorOfConstraints DataAdapter::MatchTargetDetections(
diff --git a/frc971/vision/vision_util_lib.cc b/frc971/vision/vision_util_lib.cc
index b65e883..bfd6209 100644
--- a/frc971/vision/vision_util_lib.cc
+++ b/frc971/vision/vision_util_lib.cc
@@ -43,4 +43,19 @@
   return result;
 }
 
+std::optional<uint16_t> CameraNumberFromChannel(std::string camera_channel) {
+  if (camera_channel.find("/camera") == std::string::npos) {
+    return std::nullopt;
+  }
+  // If the string doesn't end in /camera#, return nullopt
+  uint16_t cam_len = std::string("/camera").length();
+  if (camera_channel.length() != camera_channel.find("/camera") + cam_len + 1) {
+    return std::nullopt;
+  }
+
+  uint16_t camera_number = std::stoi(
+      camera_channel.substr(camera_channel.find("/camera") + cam_len, 1));
+  return camera_number;
+}
+
 }  // namespace frc971::vision
diff --git a/frc971/vision/vision_util_lib.h b/frc971/vision/vision_util_lib.h
index 6fb32eb..8ce651c 100644
--- a/frc971/vision/vision_util_lib.h
+++ b/frc971/vision/vision_util_lib.h
@@ -7,16 +7,24 @@
 
 #include "frc971/vision/calibration_generated.h"
 
+// Extract the CameraExtrinsics from a CameraCalibration struct
 namespace frc971::vision {
 std::optional<cv::Mat> CameraExtrinsics(
     const frc971::vision::calibration::CameraCalibration *camera_calibration);
 
+// Extract the CameraIntrinsics from a CameraCalibration struct
 cv::Mat CameraIntrinsics(
     const frc971::vision::calibration::CameraCalibration *camera_calibration);
 
+// Extract the CameraDistCoeffs from a CameraCalibration struct
 cv::Mat CameraDistCoeffs(
     const frc971::vision::calibration::CameraCalibration *camera_calibration);
 
+// Get the camera number from a camera channel name, e.g., return 2 from
+// "/camera2".  Returns nullopt if string doesn't start with "/camera" or does
+// not have a number
+std::optional<uint16_t> CameraNumberFromChannel(std::string camera_channel);
+
 }  // namespace frc971::vision
 
 #endif  // FRC971_VISION_VISION_UTIL_LIB_H_
diff --git a/frc971/vision/vision_util_lib_test.cc b/frc971/vision/vision_util_lib_test.cc
new file mode 100644
index 0000000..ff9c0a3
--- /dev/null
+++ b/frc971/vision/vision_util_lib_test.cc
@@ -0,0 +1,15 @@
+#include "frc971/vision/vision_util_lib.h"
+
+#include "gtest/gtest.h"
+
+namespace frc971::vision {
+// For now, just testing extracting camera number from channel name
+TEST(VisionUtilsTest, CameraNumberFromChannel) {
+  ASSERT_EQ(CameraNumberFromChannel("/camera0").value(), 0);
+  ASSERT_EQ(CameraNumberFromChannel("/camera1").value(), 1);
+  ASSERT_EQ(CameraNumberFromChannel("/camera"), std::nullopt);
+  ASSERT_EQ(CameraNumberFromChannel("/orin1/camera0").value(), 0);
+  ASSERT_EQ(CameraNumberFromChannel("/orin1/camera1").value(), 1);
+  ASSERT_EQ(CameraNumberFromChannel("/orin1"), std::nullopt);
+}
+}  // namespace frc971::vision
diff --git a/frc971/wpilib/can_sensor_reader.cc b/frc971/wpilib/can_sensor_reader.cc
index 54be05d..1f56b17 100644
--- a/frc971/wpilib/can_sensor_reader.cc
+++ b/frc971/wpilib/can_sensor_reader.cc
@@ -7,8 +7,10 @@
     aos::EventLoop *event_loop,
     std::vector<ctre::phoenix6::BaseStatusSignal *> signals_registry,
     std::vector<std::shared_ptr<TalonFX>> talonfxs,
-    std::function<void(ctre::phoenix::StatusCode status)> flatbuffer_callback)
+    std::function<void(ctre::phoenix::StatusCode status)> flatbuffer_callback,
+    SignalSync sync)
     : event_loop_(event_loop),
+      sync_(sync),
       signals_(signals_registry.begin(), signals_registry.end()),
       talonfxs_(talonfxs),
       flatbuffer_callback_(flatbuffer_callback) {
@@ -28,8 +30,12 @@
 }
 
 void CANSensorReader::Loop() {
-  ctre::phoenix::StatusCode status =
-      ctre::phoenix6::BaseStatusSignal::WaitForAll(20_ms, signals_);
+  ctre::phoenix::StatusCode status;
+  if (sync_ == SignalSync::kDoSync) {
+    status = ctre::phoenix6::BaseStatusSignal::WaitForAll(20_ms, signals_);
+  } else {
+    status = ctre::phoenix6::BaseStatusSignal::RefreshAll(signals_);
+  }
 
   if (!status.IsOK()) {
     AOS_LOG(ERROR, "Failed to read signals from talonfx motors: %s: %s",
diff --git a/frc971/wpilib/can_sensor_reader.h b/frc971/wpilib/can_sensor_reader.h
index 8eddfca..c79d7f2 100644
--- a/frc971/wpilib/can_sensor_reader.h
+++ b/frc971/wpilib/can_sensor_reader.h
@@ -12,18 +12,23 @@
 namespace frc971::wpilib {
 class CANSensorReader {
  public:
+  enum class SignalSync {
+    kDoSync,
+    kNoSync,
+  };
   CANSensorReader(
       aos::EventLoop *event_loop,
       std::vector<ctre::phoenix6::BaseStatusSignal *> signals_registry,
       std::vector<std::shared_ptr<TalonFX>> talonfxs,
-      std::function<void(ctre::phoenix::StatusCode status)>
-          flatbuffer_callback);
+      std::function<void(ctre::phoenix::StatusCode status)> flatbuffer_callback,
+      SignalSync sync = SignalSync::kDoSync);
 
  private:
   void Loop();
 
   aos::EventLoop *event_loop_;
 
+  const SignalSync sync_;
   const std::vector<ctre::phoenix6::BaseStatusSignal *> signals_;
 
   // This is a vector of talonfxs becuase we don't need to care
diff --git a/frc971/wpilib/encoder_and_potentiometer.h b/frc971/wpilib/encoder_and_potentiometer.h
index a5eac24..84bfb64 100644
--- a/frc971/wpilib/encoder_and_potentiometer.h
+++ b/frc971/wpilib/encoder_and_potentiometer.h
@@ -9,6 +9,7 @@
 #include "aos/time/time.h"
 #include "frc971/wpilib/ahal/AnalogInput.h"
 #include "frc971/wpilib/ahal/Counter.h"
+#include "frc971/wpilib/ahal/DigitalInput.h"
 #include "frc971/wpilib/ahal/DigitalSource.h"
 #include "frc971/wpilib/ahal/Encoder.h"
 #include "frc971/wpilib/dma.h"
@@ -168,6 +169,38 @@
   ::std::unique_ptr<::frc::DigitalInput> input_;
 };
 
+class DMAAbsoluteEncoderAndPotentiometer {
+ public:
+  void set_absolute_pwm(::std::unique_ptr<frc::DigitalInput> input) {
+    duty_cycle_input_ = ::std::move(input);
+    duty_cycle_reader_.set_input(duty_cycle_input_.get());
+  }
+
+  void set_encoder(::std::unique_ptr<frc::Encoder> encoder) {
+    encoder_ = ::std::move(encoder);
+  }
+
+  void set_potentiometer(::std::unique_ptr<frc::AnalogInput> potentiometer) {
+    potentiometer_ = ::std::move(potentiometer);
+  }
+
+  double ReadAbsoluteEncoder() const {
+    return duty_cycle_reader_.last_width() / duty_cycle_reader_.last_period();
+  }
+  int32_t ReadRelativeEncoder() const { return encoder_->GetRaw(); }
+  double ReadPotentiometerVoltage() const {
+    return potentiometer_->GetVoltage();
+  }
+
+  DMAPulseWidthReader &reader() { return duty_cycle_reader_; }
+
+ private:
+  DMAPulseWidthReader duty_cycle_reader_;
+  ::std::unique_ptr<::frc::DigitalInput> duty_cycle_input_;
+  ::std::unique_ptr<frc::Encoder> encoder_;
+  ::std::unique_ptr<frc::AnalogInput> potentiometer_;
+};
+
 // Class to hold a CTRE encoder with absolute angle pwm and potentiometer pair.
 class AbsoluteEncoderAndPotentiometer {
  public:
diff --git a/frc971/wpilib/imu_batch.fbs b/frc971/wpilib/imu_batch.fbs
index 1483314..bead5df 100644
--- a/frc971/wpilib/imu_batch.fbs
+++ b/frc971/wpilib/imu_batch.fbs
@@ -2,8 +2,10 @@
 
 namespace frc971;
 
+attribute "static_length";
+
 table IMUValuesBatch {
-  readings:[IMUValues] (id: 0);
+  readings:[IMUValues] (id: 0, static_length: 1);
 }
 
 root_type IMUValuesBatch;
diff --git a/frc971/wpilib/sensor_reader.h b/frc971/wpilib/sensor_reader.h
index 455ed0e..da650a5 100644
--- a/frc971/wpilib/sensor_reader.h
+++ b/frc971/wpilib/sensor_reader.h
@@ -102,6 +102,27 @@
                                    encoder_ratio * (2.0 * M_PI));
   }
 
+  void CopyPosition(
+      const ::frc971::wpilib::DMAAbsoluteEncoderAndPotentiometer &encoder,
+      ::frc971::PotAndAbsolutePositionStatic *position,
+      double encoder_counts_per_revolution, double encoder_ratio,
+      ::std::function<double(double)> potentiometer_translate, bool reverse,
+      double pot_offset) {
+    const double multiplier = reverse ? -1.0 : 1.0;
+    position->set_pot(multiplier * potentiometer_translate(
+                                       encoder.ReadPotentiometerVoltage()) +
+                      pot_offset);
+    position->set_encoder(multiplier *
+                          encoder_translate(encoder.ReadRelativeEncoder(),
+                                            encoder_counts_per_revolution,
+                                            encoder_ratio));
+
+    position->set_absolute_encoder((reverse
+                                        ? (1.0 - encoder.ReadAbsoluteEncoder())
+                                        : encoder.ReadAbsoluteEncoder()) *
+                                   encoder_ratio * (2.0 * M_PI));
+  }
+
   // Copies an AbsoluteEncoderAndPotentiometer to an AbsoluteAndAbsolutePosition
   // with the correct unit and direction changes.
   void CopyPosition(const ::frc971::wpilib::AbsoluteAndAbsoluteEncoder &encoder,
diff --git a/frc971/wpilib/talonfx.cc b/frc971/wpilib/talonfx.cc
index 5e77618..7959f2f 100644
--- a/frc971/wpilib/talonfx.cc
+++ b/frc971/wpilib/talonfx.cc
@@ -8,6 +8,7 @@
                  double stator_current_limit, double supply_current_limit)
     : talon_(device_id, canbus),
       device_id_(device_id),
+      neutral_mode_(ctre::phoenix6::signals::NeutralModeValue::Brake),
       inverted_(inverted),
       device_temp_(talon_.GetDeviceTemp()),
       supply_voltage_(talon_.GetSupplyVoltage()),
@@ -63,7 +64,7 @@
   current_limits.SupplyCurrentLimitEnable = true;
 
   ctre::phoenix6::configs::MotorOutputConfigs output_configs;
-  output_configs.NeutralMode = ctre::phoenix6::signals::NeutralModeValue::Brake;
+  output_configs.NeutralMode = neutral_mode_;
   output_configs.DutyCycleNeutralDeadband = 0;
 
   output_configs.Inverted = inverted_;
diff --git a/frc971/wpilib/talonfx.h b/frc971/wpilib/talonfx.h
index a3e3066..1f64b5b 100644
--- a/frc971/wpilib/talonfx.h
+++ b/frc971/wpilib/talonfx.h
@@ -79,6 +79,10 @@
     supply_current_limit_ = supply_current_limit;
   }
 
+  void set_neutral_mode(ctre::phoenix6::signals::NeutralModeValue value) {
+    neutral_mode_ = value;
+  }
+
   static double SafeSpeed(double voltage) {
     return (::aos::Clip(voltage, -kMaxBringupPower, kMaxBringupPower) / 12.0);
   }
@@ -87,6 +91,7 @@
   ctre::phoenix6::hardware::TalonFX talon_;
   int device_id_;
 
+  ctre::phoenix6::signals::NeutralModeValue neutral_mode_;
   ctre::phoenix6::signals::InvertedValue inverted_;
 
   ctre::phoenix6::StatusSignal<units::temperature::celsius_t> device_temp_;
diff --git a/package.json b/package.json
index 8456a91..df5df67 100644
--- a/package.json
+++ b/package.json
@@ -4,28 +4,30 @@
   "type": "module",
   "private": true,
   "devDependencies": {
-    "@angular/animations": "15.1.5",
-    "@angular/common": "15.1.5",
-    "@angular/compiler": "15.1.5",
-    "@angular/compiler-cli": "15.1.5",
-    "@angular/core": "15.1.5",
-    "@angular/forms": "15.1.5",
-    "@angular/platform-browser": "15.1.5",
-    "@angular/cli": "15.1.5",
-    "@babel/cli": "^7.6.0",
-    "@babel/core": "^7.6.0",
+    "@angular/animations": "v16-lts",
+    "@angular/common": "v16-lts",
+    "@angular/compiler": "v16-lts",
+    "@angular/compiler-cli": "v16-lts",
+    "@angular/core": "v16-lts",
+    "@angular/forms": "v16-lts",
+    "@angular/platform-browser": "v16-lts",
+    "@angular/cli": "v16-lts",
+    "@babel/cli": "^7.16.0",
+    "@babel/core": "^7.16.0",
     "@types/jasmine": "3.10.3",
+    "@types/babel__core": "^7.20.5",
+    "@types/babel__generator": "^7.6.8",
     "html-insert-assets": "0.14.3",
     "cypress": "12.3.0",
     "prettier": "2.6.1",
     "requirejs": "2.3.6",
-    "rollup": "3.17.2",
+    "rollup": "4.12.0",
     "rxjs": "7.5.7",
-    "@rollup/plugin-node-resolve": "13.1.3",
+    "@rollup/plugin-node-resolve": "15.2.3",
     "@types/flatbuffers": "1.10.0",
-    "@types/node": "17.0.21",
-    "typescript": "4.8.4",
+    "@types/node": "20.11.19",
+    "typescript": "5.1.6",
     "terser": "5.16.4",
-    "zone.js": "^0.11.4"
+    "zone.js": "^0.13.0"
   }
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d844678..8f37b3b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1,115 +1,146 @@
-lockfileVersion: 5.4
+lockfileVersion: '6.0'
+
+settings:
+  autoInstallPeers: true
+  excludeLinksFromLockfile: false
 
 importers:
 
   .:
-    specifiers:
-      '@angular/animations': 15.1.5
-      '@angular/cli': 15.1.5
-      '@angular/common': 15.1.5
-      '@angular/compiler': 15.1.5
-      '@angular/compiler-cli': 15.1.5
-      '@angular/core': 15.1.5
-      '@angular/forms': 15.1.5
-      '@angular/platform-browser': 15.1.5
-      '@babel/cli': ^7.6.0
-      '@babel/core': ^7.6.0
-      '@rollup/plugin-node-resolve': 13.1.3
-      '@types/flatbuffers': 1.10.0
-      '@types/jasmine': 3.10.3
-      '@types/node': 17.0.21
-      cypress: 12.3.0
-      html-insert-assets: 0.14.3
-      prettier: 2.6.1
-      requirejs: 2.3.6
-      rollup: 3.17.2
-      rxjs: 7.5.7
-      terser: 5.16.4
-      typescript: 4.8.4
-      zone.js: ^0.11.4
     devDependencies:
-      '@angular/animations': 15.1.5_@angular+core@15.1.5
-      '@angular/cli': 15.1.5
-      '@angular/common': 15.1.5_w2a4ar2ssyezibn6c65i4snjzu
-      '@angular/compiler': 15.1.5_@angular+core@15.1.5
-      '@angular/compiler-cli': 15.1.5_4dhd3kzleoe6yecgeixihz776m
-      '@angular/core': 15.1.5_rxjs@7.5.7+zone.js@0.11.8
-      '@angular/forms': 15.1.5_kkteiffnjutwxhimrtvkwyrg3a
-      '@angular/platform-browser': 15.1.5_s7kwnqxnlkypgp4vtemlnxkbmi
-      '@babel/cli': 7.22.5_@babel+core@7.22.5
-      '@babel/core': 7.22.5
-      '@rollup/plugin-node-resolve': 13.1.3_rollup@3.17.2
-      '@types/flatbuffers': 1.10.0
-      '@types/jasmine': 3.10.3
-      '@types/node': 17.0.21
-      cypress: 12.3.0
-      html-insert-assets: 0.14.3
-      prettier: 2.6.1
-      requirejs: 2.3.6
-      rollup: 3.17.2
-      rxjs: 7.5.7
-      terser: 5.16.4
-      typescript: 4.8.4
-      zone.js: 0.11.8
+      '@angular/animations':
+        specifier: v16-lts
+        version: 16.2.12(@angular/core@16.2.12)
+      '@angular/cli':
+        specifier: v16-lts
+        version: 16.2.12
+      '@angular/common':
+        specifier: v16-lts
+        version: 16.2.12(@angular/core@16.2.12)(rxjs@7.5.7)
+      '@angular/compiler':
+        specifier: v16-lts
+        version: 16.2.12(@angular/core@16.2.12)
+      '@angular/compiler-cli':
+        specifier: v16-lts
+        version: 16.2.12(@angular/compiler@16.2.12)(typescript@5.1.6)
+      '@angular/core':
+        specifier: v16-lts
+        version: 16.2.12(rxjs@7.5.7)(zone.js@0.13.3)
+      '@angular/forms':
+        specifier: v16-lts
+        version: 16.2.12(@angular/common@16.2.12)(@angular/core@16.2.12)(@angular/platform-browser@16.2.12)(rxjs@7.5.7)
+      '@angular/platform-browser':
+        specifier: v16-lts
+        version: 16.2.12(@angular/animations@16.2.12)(@angular/common@16.2.12)(@angular/core@16.2.12)
+      '@babel/cli':
+        specifier: ^7.16.0
+        version: 7.23.9(@babel/core@7.23.9)
+      '@babel/core':
+        specifier: ^7.16.0
+        version: 7.23.9
+      '@rollup/plugin-node-resolve':
+        specifier: 15.2.3
+        version: 15.2.3(rollup@4.12.0)
+      '@types/babel__core':
+        specifier: ^7.20.5
+        version: 7.20.5
+      '@types/babel__generator':
+        specifier: ^7.6.8
+        version: 7.6.8
+      '@types/flatbuffers':
+        specifier: 1.10.0
+        version: 1.10.0
+      '@types/jasmine':
+        specifier: 3.10.3
+        version: 3.10.3
+      '@types/node':
+        specifier: 20.11.19
+        version: 20.11.19
+      cypress:
+        specifier: 12.3.0
+        version: 12.3.0
+      html-insert-assets:
+        specifier: 0.14.3
+        version: 0.14.3
+      prettier:
+        specifier: 2.6.1
+        version: 2.6.1
+      requirejs:
+        specifier: 2.3.6
+        version: 2.3.6
+      rollup:
+        specifier: 4.12.0
+        version: 4.12.0
+      rxjs:
+        specifier: 7.5.7
+        version: 7.5.7
+      terser:
+        specifier: 5.16.4
+        version: 5.16.4
+      typescript:
+        specifier: 5.1.6
+        version: 5.1.6
+      zone.js:
+        specifier: ^0.13.0
+        version: 0.13.3
 
-  scouting/www:
-    specifiers: {}
+  scouting/www: {}
 
-  scouting/www/counter_button:
-    specifiers: {}
+  scouting/www/counter_button: {}
 
   scouting/www/driver_ranking:
-    specifiers:
-      '@angular/forms': 15.1.5
     dependencies:
-      '@angular/forms': 15.1.5
+      '@angular/forms':
+        specifier: v16-lts
+        version: 16.2.12(@angular/common@16.2.12)(@angular/core@16.2.12)(@angular/platform-browser@16.2.12)(rxjs@7.5.7)
 
   scouting/www/entry:
-    specifiers:
-      '@angular/forms': 15.1.5
-      '@org_frc971/scouting/www/counter_button': workspace:*
     dependencies:
-      '@angular/forms': 15.1.5
-      '@org_frc971/scouting/www/counter_button': link:../counter_button
+      '@angular/forms':
+        specifier: v16-lts
+        version: 16.2.12(@angular/common@16.2.12)(@angular/core@16.2.12)(@angular/platform-browser@16.2.12)(rxjs@7.5.7)
+      '@org_frc971/scouting/www/counter_button':
+        specifier: workspace:*
+        version: link:../counter_button
 
   scouting/www/match_list:
-    specifiers:
-      '@angular/forms': 15.1.5
-      '@org_frc971/scouting/www/rpc': workspace:*
     dependencies:
-      '@angular/forms': 15.1.5
-      '@org_frc971/scouting/www/rpc': link:../rpc
+      '@angular/forms':
+        specifier: v16-lts
+        version: 16.2.12(@angular/common@16.2.12)(@angular/core@16.2.12)(@angular/platform-browser@16.2.12)(rxjs@7.5.7)
+      '@org_frc971/scouting/www/rpc':
+        specifier: workspace:*
+        version: link:../rpc
 
   scouting/www/notes:
-    specifiers:
-      '@angular/forms': 15.1.5
     dependencies:
-      '@angular/forms': 15.1.5
+      '@angular/forms':
+        specifier: v16-lts
+        version: 16.2.12(@angular/common@16.2.12)(@angular/core@16.2.12)(@angular/platform-browser@16.2.12)(rxjs@7.5.7)
 
   scouting/www/pit_scouting:
-    specifiers:
-      '@angular/forms': 15.1.5
     dependencies:
-      '@angular/forms': 15.1.5
+      '@angular/forms':
+        specifier: v16-lts
+        version: 16.2.12(@angular/common@16.2.12)(@angular/core@16.2.12)(@angular/platform-browser@16.2.12)(rxjs@7.5.7)
 
-  scouting/www/rpc:
-    specifiers: {}
+  scouting/www/rpc: {}
 
   scouting/www/shift_schedule:
-    specifiers:
-      '@angular/forms': 15.1.5
     dependencies:
-      '@angular/forms': 15.1.5
+      '@angular/forms':
+        specifier: v16-lts
+        version: 16.2.12(@angular/common@16.2.12)(@angular/core@16.2.12)(@angular/platform-browser@16.2.12)(rxjs@7.5.7)
 
   scouting/www/view:
-    specifiers:
-      '@angular/forms': 15.1.5
     dependencies:
-      '@angular/forms': 15.1.5
+      '@angular/forms':
+        specifier: v16-lts
+        version: 16.2.12(@angular/common@16.2.12)(@angular/core@16.2.12)(@angular/platform-browser@16.2.12)(rxjs@7.5.7)
 
 packages:
 
-  /@ampproject/remapping/2.2.1:
+  /@ampproject/remapping@2.2.1:
     resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==}
     engines: {node: '>=6.0.0'}
     dependencies:
@@ -117,19 +148,19 @@
       '@jridgewell/trace-mapping': 0.3.18
     dev: true
 
-  /@angular-devkit/architect/0.1501.5:
-    resolution: {integrity: sha512-T4zJMvJvCqZeeENdeHcFtdrISrZSe8MycQOWZwPYU9zBTGMmdYpa4GQKQmFRZGBwX2PKHFlkQ1HLLe366sySAQ==}
-    engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
+  /@angular-devkit/architect@0.1602.12:
+    resolution: {integrity: sha512-19Fwwfx+KvJ01SyI6cstRgqT9+cwer8Ro1T27t1JqlGyOX8tY3pV78ulwxy2+wCzPjR18V6W7cb7Cv6fyK4xog==}
+    engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
     dependencies:
-      '@angular-devkit/core': 15.1.5
-      rxjs: 6.6.7
+      '@angular-devkit/core': 16.2.12
+      rxjs: 7.8.1
     transitivePeerDependencies:
       - chokidar
     dev: true
 
-  /@angular-devkit/core/15.1.5:
-    resolution: {integrity: sha512-SkGQFkruTwVM77WEOIQivfFBtnHW41tttsGrT6MTrti98hs8tvOTlzfYD/sDTyh0WKbZGeAtkRXx0raevb63YQ==}
-    engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
+  /@angular-devkit/core@16.2.12:
+    resolution: {integrity: sha512-o6ziQs+EcEonFezrsA46jbZqkQrs4ckS1bAQj93g5ZjGtieUz8l/U3lclvKpL/iEzWkGVViSYuP2KyW2oqTDiQ==}
+    engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
     peerDependencies:
       chokidar: ^3.5.2
     peerDependenciesMeta:
@@ -137,181 +168,163 @@
         optional: true
     dependencies:
       ajv: 8.12.0
-      ajv-formats: 2.1.1
+      ajv-formats: 2.1.1(ajv@8.12.0)
       jsonc-parser: 3.2.0
-      rxjs: 6.6.7
+      picomatch: 2.3.1
+      rxjs: 7.8.1
       source-map: 0.7.4
     dev: true
 
-  /@angular-devkit/schematics/15.1.5:
-    resolution: {integrity: sha512-9MPuy0BjJAlSJVMqPmt50lDq6nq6AL5XJwv6NVP1fLSLXABlLBZe7jjaHLg8XVHaKbzS7BSPnHaGfHJkUipP+A==}
-    engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
+  /@angular-devkit/schematics@16.2.12:
+    resolution: {integrity: sha512-lf/Nz2o875pllxGNUcI2by4rctfRsOZOxvaLq2UaH6XG6Re9tqeNfn40a8qXrr9/IYntXnlvEid/pd9e8gFBIw==}
+    engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
     dependencies:
-      '@angular-devkit/core': 15.1.5
+      '@angular-devkit/core': 16.2.12
       jsonc-parser: 3.2.0
-      magic-string: 0.27.0
+      magic-string: 0.30.1
       ora: 5.4.1
-      rxjs: 6.6.7
+      rxjs: 7.8.1
     transitivePeerDependencies:
       - chokidar
     dev: true
 
-  /@angular/animations/15.1.5_@angular+core@15.1.5:
-    resolution: {integrity: sha512-yac9PHy5Y72MtKQhaBSQFOdIxEJIacmJrYNRFoa82z0YCa3VrEYjvuG0x5JewBN4gQGC5IOpj2C7c9zdXZv5HA==}
-    engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0}
+  /@angular/animations@16.2.12(@angular/core@16.2.12):
+    resolution: {integrity: sha512-MD0ElviEfAJY8qMOd6/jjSSvtqER2RDAi0lxe6EtUacC1DHCYkaPrKW4vLqY+tmZBg1yf+6n+uS77pXcHHcA3w==}
+    engines: {node: ^16.14.0 || >=18.10.0}
     peerDependencies:
-      '@angular/core': 15.1.5
+      '@angular/core': 16.2.12
     dependencies:
-      '@angular/core': 15.1.5_rxjs@7.5.7+zone.js@0.11.8
+      '@angular/core': 16.2.12(rxjs@7.5.7)(zone.js@0.13.3)
       tslib: 2.6.0
-    dev: true
 
-  /@angular/cli/15.1.5:
-    resolution: {integrity: sha512-R+mi0+IJyBFobinCI9nu7hdGR5tXW6mBa/nsN3fwoebV0Qc07rSf9qqYkvnPjLWMiJ5eQxdLJhPcmMjB9Xs0aA==}
-    engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
+  /@angular/cli@16.2.12:
+    resolution: {integrity: sha512-Pcbiraoqdw4rR2Ey5Ooy0ESLS1Ffbjkb6sPfinKRkHmAvyqsmlvkfbB/qK8GrzDSFSWvAKMMXRw9l8nbjvQEXg==}
+    engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
     hasBin: true
     dependencies:
-      '@angular-devkit/architect': 0.1501.5
-      '@angular-devkit/core': 15.1.5
-      '@angular-devkit/schematics': 15.1.5
-      '@schematics/angular': 15.1.5
+      '@angular-devkit/architect': 0.1602.12
+      '@angular-devkit/core': 16.2.12
+      '@angular-devkit/schematics': 16.2.12
+      '@schematics/angular': 16.2.12
       '@yarnpkg/lockfile': 1.1.0
       ansi-colors: 4.1.3
-      ini: 3.0.1
+      ini: 4.1.1
       inquirer: 8.2.4
       jsonc-parser: 3.2.0
       npm-package-arg: 10.1.0
       npm-pick-manifest: 8.0.1
-      open: 8.4.0
+      open: 8.4.2
       ora: 5.4.1
-      pacote: 15.0.8
-      resolve: 1.22.1
-      semver: 7.3.8
+      pacote: 15.2.0
+      resolve: 1.22.2
+      semver: 7.5.4
       symbol-observable: 4.0.0
-      yargs: 17.6.2
+      yargs: 17.7.2
     transitivePeerDependencies:
       - bluebird
       - chokidar
       - supports-color
     dev: true
 
-  /@angular/common/15.1.5_w2a4ar2ssyezibn6c65i4snjzu:
-    resolution: {integrity: sha512-52Ut/IeoM3avzV3Ts/ISkq7cc1FlA6dhLUq+L3ebY+Z8zZskCWjJWu4UgLGyVdtgSuAItyQm9CoZd+DrPLYtDA==}
-    engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0}
+  /@angular/common@16.2.12(@angular/core@16.2.12)(rxjs@7.5.7):
+    resolution: {integrity: sha512-B+WY/cT2VgEaz9HfJitBmgdk4I333XG/ybC98CMC4Wz8E49T8yzivmmxXB3OD6qvjcOB6ftuicl6WBqLbZNg2w==}
+    engines: {node: ^16.14.0 || >=18.10.0}
     peerDependencies:
-      '@angular/core': 15.1.5
+      '@angular/core': 16.2.12
       rxjs: ^6.5.3 || ^7.4.0
     dependencies:
-      '@angular/core': 15.1.5_rxjs@7.5.7+zone.js@0.11.8
+      '@angular/core': 16.2.12(rxjs@7.5.7)(zone.js@0.13.3)
       rxjs: 7.5.7
       tslib: 2.6.0
-    dev: true
 
-  /@angular/compiler-cli/15.1.5_4dhd3kzleoe6yecgeixihz776m:
-    resolution: {integrity: sha512-gWg6MpMJOpfkwf2zxHJDp9EGwORga4MLTkvugL+1KbN+lvx4Ac9Y0GinlJ4+EGpttvQlTYHzn8GabWhcdzzUiQ==}
-    engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0}
+  /@angular/compiler-cli@16.2.12(@angular/compiler@16.2.12)(typescript@5.1.6):
+    resolution: {integrity: sha512-pWSrr152562ujh6lsFZR8NfNc5Ljj+zSTQO44DsuB0tZjwEpnRcjJEgzuhGXr+CoiBf+jTSPZKemtSktDk5aaA==}
+    engines: {node: ^16.14.0 || >=18.10.0}
     hasBin: true
     peerDependencies:
-      '@angular/compiler': 15.1.5
-      typescript: '>=4.8.2 <5.0'
+      '@angular/compiler': 16.2.12
+      typescript: '>=4.9.3 <5.2'
     dependencies:
-      '@angular/compiler': 15.1.5_@angular+core@15.1.5
-      '@babel/core': 7.19.3
+      '@angular/compiler': 16.2.12(@angular/core@16.2.12)
+      '@babel/core': 7.23.2
       '@jridgewell/sourcemap-codec': 1.4.15
       chokidar: 3.5.3
       convert-source-map: 1.9.0
-      dependency-graph: 0.11.0
-      magic-string: 0.27.0
-      reflect-metadata: 0.1.13
-      semver: 7.5.3
+      reflect-metadata: 0.1.14
+      semver: 7.6.0
       tslib: 2.6.0
-      typescript: 4.8.4
+      typescript: 5.1.6
       yargs: 17.7.2
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /@angular/compiler/15.1.5_@angular+core@15.1.5:
-    resolution: {integrity: sha512-4Ciswu3HKE+Pk+6Lhi6v3inZ01WkNBi9D33OKGC+7uEAjl8DCNF13rBXLyMF6tIFd+L98KYpzwUyQYk8FI/vgA==}
-    engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0}
+  /@angular/compiler@16.2.12(@angular/core@16.2.12):
+    resolution: {integrity: sha512-6SMXUgSVekGM7R6l1Z9rCtUGtlg58GFmgbpMCsGf+VXxP468Njw8rjT2YZkf5aEPxEuRpSHhDYjqz7n14cwCXQ==}
+    engines: {node: ^16.14.0 || >=18.10.0}
     peerDependencies:
-      '@angular/core': 15.1.5
+      '@angular/core': 16.2.12
     peerDependenciesMeta:
       '@angular/core':
         optional: true
     dependencies:
-      '@angular/core': 15.1.5_rxjs@7.5.7+zone.js@0.11.8
+      '@angular/core': 16.2.12(rxjs@7.5.7)(zone.js@0.13.3)
       tslib: 2.6.0
     dev: true
 
-  /@angular/core/15.1.5_rxjs@7.5.7+zone.js@0.11.8:
-    resolution: {integrity: sha512-JCbhGVaskqrstLB8CJoPtMQKH4gryhuLFUVL5cwbVy3UJGGNmc3Gzvk+9I7zDf/D08vKyXGGmBNBVx2J65SJgw==}
-    engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0}
+  /@angular/core@16.2.12(rxjs@7.5.7)(zone.js@0.13.3):
+    resolution: {integrity: sha512-GLLlDeke/NjroaLYOks0uyzFVo6HyLl7VOm0K1QpLXnYvW63W9Ql/T3yguRZa7tRkOAeFZ3jw+1wnBD4O8MoUA==}
+    engines: {node: ^16.14.0 || >=18.10.0}
     peerDependencies:
       rxjs: ^6.5.3 || ^7.4.0
-      zone.js: ~0.11.4 || ~0.12.0
+      zone.js: ~0.13.0
     dependencies:
       rxjs: 7.5.7
       tslib: 2.6.0
-      zone.js: 0.11.8
-    dev: true
+      zone.js: 0.13.3
 
-  /@angular/forms/15.1.5:
-    resolution: {integrity: sha512-FnuEdyYs1o/DJepLpTsY2/GwKTEXJ7sZlQb+NKkRWOoGpA0E4nSbhn3aCUic++MTgbZyHO0rmFKnD8TI2yyJDA==}
-    engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0}
+  /@angular/forms@16.2.12(@angular/common@16.2.12)(@angular/core@16.2.12)(@angular/platform-browser@16.2.12)(rxjs@7.5.7):
+    resolution: {integrity: sha512-1Eao89hlBgLR3v8tU91vccn21BBKL06WWxl7zLpQmG6Hun+2jrThgOE4Pf3os4fkkbH4Apj0tWL2fNIWe/blbw==}
+    engines: {node: ^16.14.0 || >=18.10.0}
     peerDependencies:
-      '@angular/common': 15.1.5
-      '@angular/core': 15.1.5
-      '@angular/platform-browser': 15.1.5
+      '@angular/common': 16.2.12
+      '@angular/core': 16.2.12
+      '@angular/platform-browser': 16.2.12
       rxjs: ^6.5.3 || ^7.4.0
     dependencies:
-      tslib: 2.6.0
-    dev: false
-
-  /@angular/forms/15.1.5_kkteiffnjutwxhimrtvkwyrg3a:
-    resolution: {integrity: sha512-FnuEdyYs1o/DJepLpTsY2/GwKTEXJ7sZlQb+NKkRWOoGpA0E4nSbhn3aCUic++MTgbZyHO0rmFKnD8TI2yyJDA==}
-    engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0}
-    peerDependencies:
-      '@angular/common': 15.1.5
-      '@angular/core': 15.1.5
-      '@angular/platform-browser': 15.1.5
-      rxjs: ^6.5.3 || ^7.4.0
-    dependencies:
-      '@angular/common': 15.1.5_w2a4ar2ssyezibn6c65i4snjzu
-      '@angular/core': 15.1.5_rxjs@7.5.7+zone.js@0.11.8
-      '@angular/platform-browser': 15.1.5_s7kwnqxnlkypgp4vtemlnxkbmi
+      '@angular/common': 16.2.12(@angular/core@16.2.12)(rxjs@7.5.7)
+      '@angular/core': 16.2.12(rxjs@7.5.7)(zone.js@0.13.3)
+      '@angular/platform-browser': 16.2.12(@angular/animations@16.2.12)(@angular/common@16.2.12)(@angular/core@16.2.12)
       rxjs: 7.5.7
       tslib: 2.6.0
-    dev: true
 
-  /@angular/platform-browser/15.1.5_s7kwnqxnlkypgp4vtemlnxkbmi:
-    resolution: {integrity: sha512-epeESrWEt41W6i2NqIbGKNE0Oa1JfeDtKfMXtcjUNCgT76qS3zmC0G6irO8BOVbrwpA/YI4yYx1B9vTDUXYbEg==}
-    engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0}
+  /@angular/platform-browser@16.2.12(@angular/animations@16.2.12)(@angular/common@16.2.12)(@angular/core@16.2.12):
+    resolution: {integrity: sha512-NnH7ju1iirmVEsUq432DTm0nZBGQsBrU40M3ZeVHMQ2subnGiyUs3QyzDz8+VWLL/T5xTxWLt9BkDn65vgzlIQ==}
+    engines: {node: ^16.14.0 || >=18.10.0}
     peerDependencies:
-      '@angular/animations': 15.1.5
-      '@angular/common': 15.1.5
-      '@angular/core': 15.1.5
+      '@angular/animations': 16.2.12
+      '@angular/common': 16.2.12
+      '@angular/core': 16.2.12
     peerDependenciesMeta:
       '@angular/animations':
         optional: true
     dependencies:
-      '@angular/animations': 15.1.5_@angular+core@15.1.5
-      '@angular/common': 15.1.5_w2a4ar2ssyezibn6c65i4snjzu
-      '@angular/core': 15.1.5_rxjs@7.5.7+zone.js@0.11.8
+      '@angular/animations': 16.2.12(@angular/core@16.2.12)
+      '@angular/common': 16.2.12(@angular/core@16.2.12)(rxjs@7.5.7)
+      '@angular/core': 16.2.12(rxjs@7.5.7)(zone.js@0.13.3)
       tslib: 2.6.0
-    dev: true
 
-  /@babel/cli/7.22.5_@babel+core@7.22.5:
-    resolution: {integrity: sha512-N5d7MjzwsQ2wppwjhrsicVDhJSqF9labEP/swYiHhio4Ca2XjEehpgPmerjnLQl7BPE59BLud0PTWGYwqFl/cQ==}
+  /@babel/cli@7.23.9(@babel/core@7.23.9):
+    resolution: {integrity: sha512-vB1UXmGDNEhcf1jNAHKT9IlYk1R+hehVTLFlCLHBi8gfuHQGP6uRjgXVYU0EVlI/qwAWpstqkBdf2aez3/z/5Q==}
     engines: {node: '>=6.9.0'}
     hasBin: true
     peerDependencies:
       '@babel/core': ^7.0.0-0
     dependencies:
-      '@babel/core': 7.22.5
+      '@babel/core': 7.23.9
       '@jridgewell/trace-mapping': 0.3.18
       commander: 4.1.1
-      convert-source-map: 1.9.0
+      convert-source-map: 2.0.0
       fs-readdir-recursive: 1.1.0
       glob: 7.2.3
       make-dir: 2.1.0
@@ -321,246 +334,242 @@
       chokidar: 3.5.3
     dev: true
 
-  /@babel/code-frame/7.22.5:
-    resolution: {integrity: sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==}
+  /@babel/code-frame@7.23.5:
+    resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==}
     engines: {node: '>=6.9.0'}
     dependencies:
-      '@babel/highlight': 7.22.5
+      '@babel/highlight': 7.23.4
+      chalk: 2.4.2
     dev: true
 
-  /@babel/compat-data/7.22.5:
-    resolution: {integrity: sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==}
+  /@babel/compat-data@7.23.5:
+    resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==}
     engines: {node: '>=6.9.0'}
     dev: true
 
-  /@babel/core/7.19.3:
-    resolution: {integrity: sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==}
+  /@babel/core@7.23.2:
+    resolution: {integrity: sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==}
     engines: {node: '>=6.9.0'}
     dependencies:
       '@ampproject/remapping': 2.2.1
-      '@babel/code-frame': 7.22.5
-      '@babel/generator': 7.22.5
-      '@babel/helper-compilation-targets': 7.22.5_@babel+core@7.19.3
-      '@babel/helper-module-transforms': 7.22.5
-      '@babel/helpers': 7.22.5
-      '@babel/parser': 7.22.5
-      '@babel/template': 7.22.5
-      '@babel/traverse': 7.22.5
-      '@babel/types': 7.22.5
-      convert-source-map: 1.9.0
-      debug: 4.3.4
+      '@babel/code-frame': 7.23.5
+      '@babel/generator': 7.23.6
+      '@babel/helper-compilation-targets': 7.23.6
+      '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.2)
+      '@babel/helpers': 7.23.9
+      '@babel/parser': 7.23.9
+      '@babel/template': 7.23.9
+      '@babel/traverse': 7.23.9
+      '@babel/types': 7.23.9
+      convert-source-map: 2.0.0
+      debug: 4.3.4(supports-color@8.1.1)
       gensync: 1.0.0-beta.2
       json5: 2.2.3
-      semver: 6.3.0
+      semver: 6.3.1
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /@babel/core/7.22.5:
-    resolution: {integrity: sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==}
+  /@babel/core@7.23.9:
+    resolution: {integrity: sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==}
     engines: {node: '>=6.9.0'}
     dependencies:
       '@ampproject/remapping': 2.2.1
-      '@babel/code-frame': 7.22.5
-      '@babel/generator': 7.22.5
-      '@babel/helper-compilation-targets': 7.22.5_@babel+core@7.22.5
-      '@babel/helper-module-transforms': 7.22.5
-      '@babel/helpers': 7.22.5
-      '@babel/parser': 7.22.5
-      '@babel/template': 7.22.5
-      '@babel/traverse': 7.22.5
-      '@babel/types': 7.22.5
-      convert-source-map: 1.9.0
-      debug: 4.3.4
+      '@babel/code-frame': 7.23.5
+      '@babel/generator': 7.23.6
+      '@babel/helper-compilation-targets': 7.23.6
+      '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.9)
+      '@babel/helpers': 7.23.9
+      '@babel/parser': 7.23.9
+      '@babel/template': 7.23.9
+      '@babel/traverse': 7.23.9
+      '@babel/types': 7.23.9
+      convert-source-map: 2.0.0
+      debug: 4.3.4(supports-color@8.1.1)
       gensync: 1.0.0-beta.2
       json5: 2.2.3
-      semver: 6.3.0
+      semver: 6.3.1
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /@babel/generator/7.22.5:
-    resolution: {integrity: sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==}
+  /@babel/generator@7.23.6:
+    resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==}
     engines: {node: '>=6.9.0'}
     dependencies:
-      '@babel/types': 7.22.5
+      '@babel/types': 7.23.9
       '@jridgewell/gen-mapping': 0.3.3
       '@jridgewell/trace-mapping': 0.3.18
       jsesc: 2.5.2
     dev: true
 
-  /@babel/helper-compilation-targets/7.22.5_@babel+core@7.19.3:
-    resolution: {integrity: sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==}
+  /@babel/helper-compilation-targets@7.23.6:
+    resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==}
     engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0
     dependencies:
-      '@babel/compat-data': 7.22.5
-      '@babel/core': 7.19.3
-      '@babel/helper-validator-option': 7.22.5
-      browserslist: 4.21.9
+      '@babel/compat-data': 7.23.5
+      '@babel/helper-validator-option': 7.23.5
+      browserslist: 4.23.0
       lru-cache: 5.1.1
-      semver: 6.3.0
+      semver: 6.3.1
     dev: true
 
-  /@babel/helper-compilation-targets/7.22.5_@babel+core@7.22.5:
-    resolution: {integrity: sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0
-    dependencies:
-      '@babel/compat-data': 7.22.5
-      '@babel/core': 7.22.5
-      '@babel/helper-validator-option': 7.22.5
-      browserslist: 4.21.9
-      lru-cache: 5.1.1
-      semver: 6.3.0
-    dev: true
-
-  /@babel/helper-environment-visitor/7.22.5:
-    resolution: {integrity: sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==}
+  /@babel/helper-environment-visitor@7.22.20:
+    resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==}
     engines: {node: '>=6.9.0'}
     dev: true
 
-  /@babel/helper-function-name/7.22.5:
-    resolution: {integrity: sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==}
+  /@babel/helper-function-name@7.23.0:
+    resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==}
     engines: {node: '>=6.9.0'}
     dependencies:
-      '@babel/template': 7.22.5
-      '@babel/types': 7.22.5
+      '@babel/template': 7.23.9
+      '@babel/types': 7.23.9
     dev: true
 
-  /@babel/helper-hoist-variables/7.22.5:
+  /@babel/helper-hoist-variables@7.22.5:
     resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==}
     engines: {node: '>=6.9.0'}
     dependencies:
-      '@babel/types': 7.22.5
+      '@babel/types': 7.23.9
     dev: true
 
-  /@babel/helper-module-imports/7.22.5:
-    resolution: {integrity: sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==}
+  /@babel/helper-module-imports@7.22.15:
+    resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==}
     engines: {node: '>=6.9.0'}
     dependencies:
-      '@babel/types': 7.22.5
+      '@babel/types': 7.23.9
     dev: true
 
-  /@babel/helper-module-transforms/7.22.5:
-    resolution: {integrity: sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==}
+  /@babel/helper-module-transforms@7.23.3(@babel/core@7.23.2):
+    resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==}
     engines: {node: '>=6.9.0'}
+    peerDependencies:
+      '@babel/core': ^7.0.0
     dependencies:
-      '@babel/helper-environment-visitor': 7.22.5
-      '@babel/helper-module-imports': 7.22.5
+      '@babel/core': 7.23.2
+      '@babel/helper-environment-visitor': 7.22.20
+      '@babel/helper-module-imports': 7.22.15
       '@babel/helper-simple-access': 7.22.5
-      '@babel/helper-split-export-declaration': 7.22.5
-      '@babel/helper-validator-identifier': 7.22.5
-      '@babel/template': 7.22.5
-      '@babel/traverse': 7.22.5
-      '@babel/types': 7.22.5
-    transitivePeerDependencies:
-      - supports-color
+      '@babel/helper-split-export-declaration': 7.22.6
+      '@babel/helper-validator-identifier': 7.22.20
     dev: true
 
-  /@babel/helper-simple-access/7.22.5:
+  /@babel/helper-module-transforms@7.23.3(@babel/core@7.23.9):
+    resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==}
+    engines: {node: '>=6.9.0'}
+    peerDependencies:
+      '@babel/core': ^7.0.0
+    dependencies:
+      '@babel/core': 7.23.9
+      '@babel/helper-environment-visitor': 7.22.20
+      '@babel/helper-module-imports': 7.22.15
+      '@babel/helper-simple-access': 7.22.5
+      '@babel/helper-split-export-declaration': 7.22.6
+      '@babel/helper-validator-identifier': 7.22.20
+    dev: true
+
+  /@babel/helper-simple-access@7.22.5:
     resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==}
     engines: {node: '>=6.9.0'}
     dependencies:
-      '@babel/types': 7.22.5
+      '@babel/types': 7.23.9
     dev: true
 
-  /@babel/helper-split-export-declaration/7.22.5:
-    resolution: {integrity: sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==}
+  /@babel/helper-split-export-declaration@7.22.6:
+    resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==}
     engines: {node: '>=6.9.0'}
     dependencies:
-      '@babel/types': 7.22.5
+      '@babel/types': 7.23.9
     dev: true
 
-  /@babel/helper-string-parser/7.22.5:
-    resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==}
+  /@babel/helper-string-parser@7.23.4:
+    resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==}
     engines: {node: '>=6.9.0'}
     dev: true
 
-  /@babel/helper-validator-identifier/7.22.5:
-    resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==}
+  /@babel/helper-validator-identifier@7.22.20:
+    resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
     engines: {node: '>=6.9.0'}
     dev: true
 
-  /@babel/helper-validator-option/7.22.5:
-    resolution: {integrity: sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==}
+  /@babel/helper-validator-option@7.23.5:
+    resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==}
     engines: {node: '>=6.9.0'}
     dev: true
 
-  /@babel/helpers/7.22.5:
-    resolution: {integrity: sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==}
+  /@babel/helpers@7.23.9:
+    resolution: {integrity: sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==}
     engines: {node: '>=6.9.0'}
     dependencies:
-      '@babel/template': 7.22.5
-      '@babel/traverse': 7.22.5
-      '@babel/types': 7.22.5
+      '@babel/template': 7.23.9
+      '@babel/traverse': 7.23.9
+      '@babel/types': 7.23.9
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /@babel/highlight/7.22.5:
-    resolution: {integrity: sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==}
+  /@babel/highlight@7.23.4:
+    resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==}
     engines: {node: '>=6.9.0'}
     dependencies:
-      '@babel/helper-validator-identifier': 7.22.5
+      '@babel/helper-validator-identifier': 7.22.20
       chalk: 2.4.2
       js-tokens: 4.0.0
     dev: true
 
-  /@babel/parser/7.22.5:
-    resolution: {integrity: sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==}
+  /@babel/parser@7.23.9:
+    resolution: {integrity: sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==}
     engines: {node: '>=6.0.0'}
     hasBin: true
     dependencies:
-      '@babel/types': 7.22.5
+      '@babel/types': 7.23.9
     dev: true
 
-  /@babel/template/7.22.5:
-    resolution: {integrity: sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==}
+  /@babel/template@7.23.9:
+    resolution: {integrity: sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==}
     engines: {node: '>=6.9.0'}
     dependencies:
-      '@babel/code-frame': 7.22.5
-      '@babel/parser': 7.22.5
-      '@babel/types': 7.22.5
+      '@babel/code-frame': 7.23.5
+      '@babel/parser': 7.23.9
+      '@babel/types': 7.23.9
     dev: true
 
-  /@babel/traverse/7.22.5:
-    resolution: {integrity: sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==}
+  /@babel/traverse@7.23.9:
+    resolution: {integrity: sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==}
     engines: {node: '>=6.9.0'}
     dependencies:
-      '@babel/code-frame': 7.22.5
-      '@babel/generator': 7.22.5
-      '@babel/helper-environment-visitor': 7.22.5
-      '@babel/helper-function-name': 7.22.5
+      '@babel/code-frame': 7.23.5
+      '@babel/generator': 7.23.6
+      '@babel/helper-environment-visitor': 7.22.20
+      '@babel/helper-function-name': 7.23.0
       '@babel/helper-hoist-variables': 7.22.5
-      '@babel/helper-split-export-declaration': 7.22.5
-      '@babel/parser': 7.22.5
-      '@babel/types': 7.22.5
-      debug: 4.3.4
+      '@babel/helper-split-export-declaration': 7.22.6
+      '@babel/parser': 7.23.9
+      '@babel/types': 7.23.9
+      debug: 4.3.4(supports-color@8.1.1)
       globals: 11.12.0
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /@babel/types/7.22.5:
-    resolution: {integrity: sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==}
+  /@babel/types@7.23.9:
+    resolution: {integrity: sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==}
     engines: {node: '>=6.9.0'}
     dependencies:
-      '@babel/helper-string-parser': 7.22.5
-      '@babel/helper-validator-identifier': 7.22.5
+      '@babel/helper-string-parser': 7.23.4
+      '@babel/helper-validator-identifier': 7.22.20
       to-fast-properties: 2.0.0
     dev: true
 
-  /@colors/colors/1.5.0:
+  /@colors/colors@1.5.0:
     resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
     engines: {node: '>=0.1.90'}
     requiresBuild: true
     dev: true
     optional: true
 
-  /@cypress/request/2.88.11:
+  /@cypress/request@2.88.11:
     resolution: {integrity: sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w==}
     engines: {node: '>= 6'}
     dependencies:
@@ -584,28 +593,32 @@
       uuid: 8.3.2
     dev: true
 
-  /@cypress/xvfb/1.2.4_supports-color@8.1.1:
+  /@cypress/xvfb@1.2.4(supports-color@8.1.1):
     resolution: {integrity: sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==}
     dependencies:
-      debug: 3.2.7_supports-color@8.1.1
+      debug: 3.2.7(supports-color@8.1.1)
       lodash.once: 4.1.1
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /@isaacs/cliui/8.0.2:
+  /@gar/promisify@1.1.3:
+    resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
+    dev: true
+
+  /@isaacs/cliui@8.0.2:
     resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
     engines: {node: '>=12'}
     dependencies:
       string-width: 5.1.2
-      string-width-cjs: /string-width/4.2.3
+      string-width-cjs: /string-width@4.2.3
       strip-ansi: 7.1.0
-      strip-ansi-cjs: /strip-ansi/6.0.1
+      strip-ansi-cjs: /strip-ansi@6.0.1
       wrap-ansi: 8.1.0
-      wrap-ansi-cjs: /wrap-ansi/7.0.0
+      wrap-ansi-cjs: /wrap-ansi@7.0.0
     dev: true
 
-  /@jridgewell/gen-mapping/0.3.3:
+  /@jridgewell/gen-mapping@0.3.3:
     resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
     engines: {node: '>=6.0.0'}
     dependencies:
@@ -614,49 +627,57 @@
       '@jridgewell/trace-mapping': 0.3.18
     dev: true
 
-  /@jridgewell/resolve-uri/3.1.0:
+  /@jridgewell/resolve-uri@3.1.0:
     resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==}
     engines: {node: '>=6.0.0'}
     dev: true
 
-  /@jridgewell/set-array/1.1.2:
+  /@jridgewell/set-array@1.1.2:
     resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
     engines: {node: '>=6.0.0'}
     dev: true
 
-  /@jridgewell/source-map/0.3.4:
+  /@jridgewell/source-map@0.3.4:
     resolution: {integrity: sha512-KE/SxsDqNs3rrWwFHcRh15ZLVFrI0YoZtgAdIyIq9k5hUNmiWRXXThPomIxHuL20sLdgzbDFyvkUMna14bvtrw==}
     dev: true
 
-  /@jridgewell/sourcemap-codec/1.4.14:
+  /@jridgewell/sourcemap-codec@1.4.14:
     resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
     dev: true
 
-  /@jridgewell/sourcemap-codec/1.4.15:
+  /@jridgewell/sourcemap-codec@1.4.15:
     resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
     dev: true
 
-  /@jridgewell/trace-mapping/0.3.18:
+  /@jridgewell/trace-mapping@0.3.18:
     resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==}
     dependencies:
       '@jridgewell/resolve-uri': 3.1.0
       '@jridgewell/sourcemap-codec': 1.4.14
     dev: true
 
-  /@nicolo-ribaudo/chokidar-2/2.1.8-no-fsevents.3:
+  /@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3:
     resolution: {integrity: sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==}
     requiresBuild: true
     dev: true
     optional: true
 
-  /@npmcli/fs/3.1.0:
+  /@npmcli/fs@2.1.2:
+    resolution: {integrity: sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==}
+    engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+    dependencies:
+      '@gar/promisify': 1.1.3
+      semver: 7.6.0
+    dev: true
+
+  /@npmcli/fs@3.1.0:
     resolution: {integrity: sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dependencies:
-      semver: 7.3.8
+      semver: 7.6.0
     dev: true
 
-  /@npmcli/git/4.1.0:
+  /@npmcli/git@4.1.0:
     resolution: {integrity: sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dependencies:
@@ -666,13 +687,13 @@
       proc-log: 3.0.0
       promise-inflight: 1.0.1
       promise-retry: 2.0.1
-      semver: 7.3.8
+      semver: 7.6.0
       which: 3.0.1
     transitivePeerDependencies:
       - bluebird
     dev: true
 
-  /@npmcli/installed-package-contents/2.0.2:
+  /@npmcli/installed-package-contents@2.0.2:
     resolution: {integrity: sha512-xACzLPhnfD51GKvTOOuNX2/V4G4mz9/1I2MfDoye9kBM3RYe5g2YbscsaGoTlaWqkxeiapBWyseULVKpSVHtKQ==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     hasBin: true
@@ -681,158 +702,349 @@
       npm-normalize-package-bin: 3.0.1
     dev: true
 
-  /@npmcli/node-gyp/3.0.0:
+  /@npmcli/move-file@2.0.1:
+    resolution: {integrity: sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==}
+    engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+    deprecated: This functionality has been moved to @npmcli/fs
+    dependencies:
+      mkdirp: 1.0.4
+      rimraf: 3.0.2
+    dev: true
+
+  /@npmcli/node-gyp@3.0.0:
     resolution: {integrity: sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dev: true
 
-  /@npmcli/promise-spawn/6.0.2:
+  /@npmcli/promise-spawn@6.0.2:
     resolution: {integrity: sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dependencies:
       which: 3.0.1
     dev: true
 
-  /@npmcli/run-script/6.0.2:
+  /@npmcli/run-script@6.0.2:
     resolution: {integrity: sha512-NCcr1uQo1k5U+SYlnIrbAh3cxy+OQT1VtqiAbxdymSlptbzBb62AjH2xXgjNCoP073hoa1CfCAcwoZ8k96C4nA==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dependencies:
       '@npmcli/node-gyp': 3.0.0
       '@npmcli/promise-spawn': 6.0.2
-      node-gyp: 9.4.0
+      node-gyp: 9.4.1
       read-package-json-fast: 3.0.2
       which: 3.0.1
     transitivePeerDependencies:
+      - bluebird
       - supports-color
     dev: true
 
-  /@pkgjs/parseargs/0.11.0:
+  /@pkgjs/parseargs@0.11.0:
     resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
     engines: {node: '>=14'}
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/plugin-node-resolve/13.1.3_rollup@3.17.2:
-    resolution: {integrity: sha512-BdxNk+LtmElRo5d06MGY4zoepyrXX1tkzX2hrnPEZ53k78GuOMWLqmJDGIIOPwVRIFZrLQOo+Yr6KtCuLIA0AQ==}
-    engines: {node: '>= 10.0.0'}
+  /@rollup/plugin-node-resolve@15.2.3(rollup@4.12.0):
+    resolution: {integrity: sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==}
+    engines: {node: '>=14.0.0'}
     peerDependencies:
-      rollup: ^2.42.0
+      rollup: ^2.78.0||^3.0.0||^4.0.0
+    peerDependenciesMeta:
+      rollup:
+        optional: true
     dependencies:
-      '@rollup/pluginutils': 3.1.0_rollup@3.17.2
-      '@types/resolve': 1.17.1
-      builtin-modules: 3.3.0
+      '@rollup/pluginutils': 5.1.0(rollup@4.12.0)
+      '@types/resolve': 1.20.2
       deepmerge: 4.3.1
+      is-builtin-module: 3.2.1
       is-module: 1.0.0
       resolve: 1.22.2
-      rollup: 3.17.2
+      rollup: 4.12.0
     dev: true
 
-  /@rollup/pluginutils/3.1.0_rollup@3.17.2:
-    resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==}
-    engines: {node: '>= 8.0.0'}
+  /@rollup/pluginutils@5.1.0(rollup@4.12.0):
+    resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==}
+    engines: {node: '>=14.0.0'}
     peerDependencies:
-      rollup: ^1.20.0||^2.0.0
+      rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
+    peerDependenciesMeta:
+      rollup:
+        optional: true
     dependencies:
-      '@types/estree': 0.0.39
-      estree-walker: 1.0.1
+      '@types/estree': 1.0.5
+      estree-walker: 2.0.2
       picomatch: 2.3.1
-      rollup: 3.17.2
+      rollup: 4.12.0
     dev: true
 
-  /@schematics/angular/15.1.5:
-    resolution: {integrity: sha512-mw5adVNSLX8h6c8F0tNEe11LVOlj100c1PrPggZNVz9nd2fwb32SVFSx+FmOxLVfE1kfnPgsvLpDH23z8SF6bg==}
-    engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
+  /@rollup/rollup-android-arm-eabi@4.12.0:
+    resolution: {integrity: sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==}
+    cpu: [arm]
+    os: [android]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-android-arm64@4.12.0:
+    resolution: {integrity: sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==}
+    cpu: [arm64]
+    os: [android]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-darwin-arm64@4.12.0:
+    resolution: {integrity: sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==}
+    cpu: [arm64]
+    os: [darwin]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-darwin-x64@4.12.0:
+    resolution: {integrity: sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==}
+    cpu: [x64]
+    os: [darwin]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-linux-arm-gnueabihf@4.12.0:
+    resolution: {integrity: sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==}
+    cpu: [arm]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-linux-arm64-gnu@4.12.0:
+    resolution: {integrity: sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==}
+    cpu: [arm64]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-linux-arm64-musl@4.12.0:
+    resolution: {integrity: sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==}
+    cpu: [arm64]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-linux-riscv64-gnu@4.12.0:
+    resolution: {integrity: sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==}
+    cpu: [riscv64]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-linux-x64-gnu@4.12.0:
+    resolution: {integrity: sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==}
+    cpu: [x64]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-linux-x64-musl@4.12.0:
+    resolution: {integrity: sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==}
+    cpu: [x64]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-win32-arm64-msvc@4.12.0:
+    resolution: {integrity: sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==}
+    cpu: [arm64]
+    os: [win32]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-win32-ia32-msvc@4.12.0:
+    resolution: {integrity: sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==}
+    cpu: [ia32]
+    os: [win32]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-win32-x64-msvc@4.12.0:
+    resolution: {integrity: sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==}
+    cpu: [x64]
+    os: [win32]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@schematics/angular@16.2.12:
+    resolution: {integrity: sha512-rc6Dxo7yLnNhECxZyvwv3qL40GvMHw/gMeme8DUGN7zgcUdBJ7LOCURp7EZqOBghMVeeJvLrohitEbs9NhRLBA==}
+    engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
     dependencies:
-      '@angular-devkit/core': 15.1.5
-      '@angular-devkit/schematics': 15.1.5
+      '@angular-devkit/core': 16.2.12
+      '@angular-devkit/schematics': 16.2.12
       jsonc-parser: 3.2.0
     transitivePeerDependencies:
       - chokidar
     dev: true
 
-  /@tootallnate/once/2.0.0:
+  /@sigstore/bundle@1.1.0:
+    resolution: {integrity: sha512-PFutXEy0SmQxYI4texPw3dd2KewuNqv7OuK1ZFtY2fM754yhvG2KdgwIhRnoEE2uHdtdGNQ8s0lb94dW9sELog==}
+    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+    dependencies:
+      '@sigstore/protobuf-specs': 0.2.1
+    dev: true
+
+  /@sigstore/protobuf-specs@0.2.1:
+    resolution: {integrity: sha512-XTWVxnWJu+c1oCshMLwnKvz8ZQJJDVOlciMfgpJBQbThVjKTCG8dwyhgLngBD2KN0ap9F/gOV8rFDEx8uh7R2A==}
+    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+    dev: true
+
+  /@sigstore/sign@1.0.0:
+    resolution: {integrity: sha512-INxFVNQteLtcfGmcoldzV6Je0sbbfh9I16DM4yJPw3j5+TFP8X6uIiA18mvpEa9yyeycAKgPmOA3X9hVdVTPUA==}
+    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+    dependencies:
+      '@sigstore/bundle': 1.1.0
+      '@sigstore/protobuf-specs': 0.2.1
+      make-fetch-happen: 11.1.1
+    transitivePeerDependencies:
+      - supports-color
+    dev: true
+
+  /@sigstore/tuf@1.0.3:
+    resolution: {integrity: sha512-2bRovzs0nJZFlCN3rXirE4gwxCn97JNjMmwpecqlbgV9WcxX7WRuIrgzx/X7Ib7MYRbyUTpBYE0s2x6AmZXnlg==}
+    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+    dependencies:
+      '@sigstore/protobuf-specs': 0.2.1
+      tuf-js: 1.1.7
+    transitivePeerDependencies:
+      - supports-color
+    dev: true
+
+  /@tootallnate/once@2.0.0:
     resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
     engines: {node: '>= 10'}
     dev: true
 
-  /@types/estree/0.0.39:
-    resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
+  /@tufjs/canonical-json@1.0.0:
+    resolution: {integrity: sha512-QTnf++uxunWvG2z3UFNzAoQPHxnSXOwtaI3iJ+AohhV+5vONuArPjJE7aPXPVXfXJsqrVbZBu9b81AJoSd09IQ==}
+    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dev: true
 
-  /@types/flatbuffers/1.10.0:
+  /@tufjs/models@1.0.4:
+    resolution: {integrity: sha512-qaGV9ltJP0EO25YfFUPhxRVK0evXFIAGicsVXuRim4Ed9cjPxYhNnNJ49SFmbeLgtxpslIkX317IgpfcHPVj/A==}
+    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+    dependencies:
+      '@tufjs/canonical-json': 1.0.0
+      minimatch: 9.0.3
+    dev: true
+
+  /@types/babel__core@7.20.5:
+    resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
+    dependencies:
+      '@babel/parser': 7.23.9
+      '@babel/types': 7.23.9
+      '@types/babel__generator': 7.6.8
+      '@types/babel__template': 7.4.4
+      '@types/babel__traverse': 7.20.5
+    dev: true
+
+  /@types/babel__generator@7.6.8:
+    resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==}
+    dependencies:
+      '@babel/types': 7.23.9
+    dev: true
+
+  /@types/babel__template@7.4.4:
+    resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
+    dependencies:
+      '@babel/parser': 7.23.9
+      '@babel/types': 7.23.9
+    dev: true
+
+  /@types/babel__traverse@7.20.5:
+    resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==}
+    dependencies:
+      '@babel/types': 7.23.9
+    dev: true
+
+  /@types/estree@1.0.5:
+    resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
+    dev: true
+
+  /@types/flatbuffers@1.10.0:
     resolution: {integrity: sha512-7btbphLrKvo5yl/5CC2OCxUSMx1wV1wvGT1qDXkSt7yi00/YW7E8k6qzXqJHsp+WU0eoG7r6MTQQXI9lIvd0qA==}
     dev: true
 
-  /@types/jasmine/3.10.3:
+  /@types/jasmine@3.10.3:
     resolution: {integrity: sha512-SWyMrjgdAUHNQmutvDcKablrJhkDLy4wunTme8oYLjKp41GnHGxMRXr2MQMvy/qy8H3LdzwQk9gH4hZ6T++H8g==}
     dev: true
 
-  /@types/node/14.18.53:
+  /@types/node@14.18.53:
     resolution: {integrity: sha512-soGmOpVBUq+gaBMwom1M+krC/NNbWlosh4AtGA03SyWNDiqSKtwp7OulO1M6+mg8YkHMvJ/y0AkCeO8d1hNb7A==}
     dev: true
 
-  /@types/node/17.0.21:
-    resolution: {integrity: sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ==}
-    dev: true
-
-  /@types/resolve/1.17.1:
-    resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
+  /@types/node@20.11.19:
+    resolution: {integrity: sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==}
     dependencies:
-      '@types/node': 17.0.21
+      undici-types: 5.26.5
     dev: true
 
-  /@types/sinonjs__fake-timers/8.1.1:
+  /@types/resolve@1.20.2:
+    resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
+    dev: true
+
+  /@types/sinonjs__fake-timers@8.1.1:
     resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==}
     dev: true
 
-  /@types/sizzle/2.3.3:
+  /@types/sizzle@2.3.3:
     resolution: {integrity: sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==}
     dev: true
 
-  /@types/yauzl/2.10.0:
+  /@types/yauzl@2.10.0:
     resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==}
     requiresBuild: true
     dependencies:
-      '@types/node': 17.0.21
+      '@types/node': 20.11.19
     dev: true
     optional: true
 
-  /@yarnpkg/lockfile/1.1.0:
+  /@yarnpkg/lockfile@1.1.0:
     resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==}
     dev: true
 
-  /abbrev/1.1.1:
+  /abbrev@1.1.1:
     resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
     dev: true
 
-  /acorn/8.9.0:
+  /acorn@8.9.0:
     resolution: {integrity: sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==}
     engines: {node: '>=0.4.0'}
     hasBin: true
     dev: true
 
-  /agent-base/6.0.2:
+  /agent-base@6.0.2:
     resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
     engines: {node: '>= 6.0.0'}
     dependencies:
-      debug: 4.3.4
+      debug: 4.3.4(supports-color@8.1.1)
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /agentkeepalive/4.3.0:
-    resolution: {integrity: sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==}
+  /agentkeepalive@4.5.0:
+    resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==}
     engines: {node: '>= 8.0.0'}
     dependencies:
-      debug: 4.3.4
-      depd: 2.0.0
       humanize-ms: 1.2.1
-    transitivePeerDependencies:
-      - supports-color
     dev: true
 
-  /aggregate-error/3.1.0:
+  /aggregate-error@3.1.0:
     resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
     engines: {node: '>=8'}
     dependencies:
@@ -840,8 +1052,10 @@
       indent-string: 4.0.0
     dev: true
 
-  /ajv-formats/2.1.1:
+  /ajv-formats@2.1.1(ajv@8.12.0):
     resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
+    peerDependencies:
+      ajv: ^8.0.0
     peerDependenciesMeta:
       ajv:
         optional: true
@@ -849,7 +1063,7 @@
       ajv: 8.12.0
     dev: true
 
-  /ajv/8.12.0:
+  /ajv@8.12.0:
     resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==}
     dependencies:
       fast-deep-equal: 3.1.3
@@ -858,48 +1072,48 @@
       uri-js: 4.4.1
     dev: true
 
-  /ansi-colors/4.1.3:
+  /ansi-colors@4.1.3:
     resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
     engines: {node: '>=6'}
     dev: true
 
-  /ansi-escapes/4.3.2:
+  /ansi-escapes@4.3.2:
     resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
     engines: {node: '>=8'}
     dependencies:
       type-fest: 0.21.3
     dev: true
 
-  /ansi-regex/5.0.1:
+  /ansi-regex@5.0.1:
     resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
     engines: {node: '>=8'}
     dev: true
 
-  /ansi-regex/6.0.1:
+  /ansi-regex@6.0.1:
     resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
     engines: {node: '>=12'}
     dev: true
 
-  /ansi-styles/3.2.1:
+  /ansi-styles@3.2.1:
     resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
     engines: {node: '>=4'}
     dependencies:
       color-convert: 1.9.3
     dev: true
 
-  /ansi-styles/4.3.0:
+  /ansi-styles@4.3.0:
     resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
     engines: {node: '>=8'}
     dependencies:
       color-convert: 2.0.1
     dev: true
 
-  /ansi-styles/6.2.1:
+  /ansi-styles@6.2.1:
     resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
     engines: {node: '>=12'}
     dev: true
 
-  /anymatch/3.1.3:
+  /anymatch@3.1.3:
     resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
     engines: {node: '>= 8'}
     dependencies:
@@ -907,15 +1121,15 @@
       picomatch: 2.3.1
     dev: true
 
-  /aproba/2.0.0:
+  /aproba@2.0.0:
     resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==}
     dev: true
 
-  /arch/2.2.0:
+  /arch@2.2.0:
     resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==}
     dev: true
 
-  /are-we-there-yet/3.0.1:
+  /are-we-there-yet@3.0.1:
     resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==}
     engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
     dependencies:
@@ -923,63 +1137,63 @@
       readable-stream: 3.6.2
     dev: true
 
-  /asn1/0.2.6:
+  /asn1@0.2.6:
     resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==}
     dependencies:
       safer-buffer: 2.1.2
     dev: true
 
-  /assert-plus/1.0.0:
+  /assert-plus@1.0.0:
     resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==}
     engines: {node: '>=0.8'}
     dev: true
 
-  /astral-regex/2.0.0:
+  /astral-regex@2.0.0:
     resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
     engines: {node: '>=8'}
     dev: true
 
-  /async/3.2.4:
+  /async@3.2.4:
     resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==}
     dev: true
 
-  /asynckit/0.4.0:
+  /asynckit@0.4.0:
     resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
     dev: true
 
-  /at-least-node/1.0.0:
+  /at-least-node@1.0.0:
     resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
     engines: {node: '>= 4.0.0'}
     dev: true
 
-  /aws-sign2/0.7.0:
+  /aws-sign2@0.7.0:
     resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==}
     dev: true
 
-  /aws4/1.12.0:
+  /aws4@1.12.0:
     resolution: {integrity: sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==}
     dev: true
 
-  /balanced-match/1.0.2:
+  /balanced-match@1.0.2:
     resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
     dev: true
 
-  /base64-js/1.5.1:
+  /base64-js@1.5.1:
     resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
     dev: true
 
-  /bcrypt-pbkdf/1.0.2:
+  /bcrypt-pbkdf@1.0.2:
     resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
     dependencies:
       tweetnacl: 0.14.5
     dev: true
 
-  /binary-extensions/2.2.0:
+  /binary-extensions@2.2.0:
     resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
     engines: {node: '>=8'}
     dev: true
 
-  /bl/4.1.0:
+  /bl@4.1.0:
     resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
     dependencies:
       buffer: 5.7.1
@@ -987,80 +1201,106 @@
       readable-stream: 3.6.2
     dev: true
 
-  /blob-util/2.0.2:
+  /blob-util@2.0.2:
     resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==}
     dev: true
 
-  /bluebird/3.7.2:
+  /bluebird@3.7.2:
     resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
     dev: true
 
-  /brace-expansion/1.1.11:
+  /brace-expansion@1.1.11:
     resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
     dependencies:
       balanced-match: 1.0.2
       concat-map: 0.0.1
     dev: true
 
-  /brace-expansion/2.0.1:
+  /brace-expansion@2.0.1:
     resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
     dependencies:
       balanced-match: 1.0.2
     dev: true
 
-  /braces/3.0.2:
+  /braces@3.0.2:
     resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
     engines: {node: '>=8'}
     dependencies:
       fill-range: 7.0.1
     dev: true
 
-  /browserslist/4.21.9:
-    resolution: {integrity: sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==}
+  /browserslist@4.23.0:
+    resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==}
     engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
     hasBin: true
     dependencies:
-      caniuse-lite: 1.0.30001509
-      electron-to-chromium: 1.4.447
-      node-releases: 2.0.12
-      update-browserslist-db: 1.0.11_browserslist@4.21.9
+      caniuse-lite: 1.0.30001588
+      electron-to-chromium: 1.4.679
+      node-releases: 2.0.14
+      update-browserslist-db: 1.0.13(browserslist@4.23.0)
     dev: true
 
-  /buffer-crc32/0.2.13:
+  /buffer-crc32@0.2.13:
     resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
     dev: true
 
-  /buffer-from/1.1.2:
+  /buffer-from@1.1.2:
     resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
     dev: true
 
-  /buffer/5.7.1:
+  /buffer@5.7.1:
     resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
     dependencies:
       base64-js: 1.5.1
       ieee754: 1.2.1
     dev: true
 
-  /builtin-modules/3.3.0:
+  /builtin-modules@3.3.0:
     resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==}
     engines: {node: '>=6'}
     dev: true
 
-  /builtins/5.0.1:
+  /builtins@5.0.1:
     resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==}
     dependencies:
-      semver: 7.3.8
+      semver: 7.6.0
     dev: true
 
-  /cacache/17.1.3:
-    resolution: {integrity: sha512-jAdjGxmPxZh0IipMdR7fK/4sDSrHMLUV0+GvVUsjwyGNKHsh79kW/otg+GkbXwl6Uzvy9wsvHOX4nUoWldeZMg==}
+  /cacache@16.1.3:
+    resolution: {integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==}
+    engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+    dependencies:
+      '@npmcli/fs': 2.1.2
+      '@npmcli/move-file': 2.0.1
+      chownr: 2.0.0
+      fs-minipass: 2.1.0
+      glob: 8.1.0
+      infer-owner: 1.0.4
+      lru-cache: 7.18.3
+      minipass: 3.3.6
+      minipass-collect: 1.0.2
+      minipass-flush: 1.0.5
+      minipass-pipeline: 1.2.4
+      mkdirp: 1.0.4
+      p-map: 4.0.0
+      promise-inflight: 1.0.1
+      rimraf: 3.0.2
+      ssri: 9.0.1
+      tar: 6.1.15
+      unique-filename: 2.0.1
+    transitivePeerDependencies:
+      - bluebird
+    dev: true
+
+  /cacache@17.1.4:
+    resolution: {integrity: sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dependencies:
       '@npmcli/fs': 3.1.0
       fs-minipass: 3.0.2
-      glob: 10.3.1
+      glob: 10.3.10
       lru-cache: 7.18.3
-      minipass: 5.0.0
+      minipass: 7.0.4
       minipass-collect: 1.0.2
       minipass-flush: 1.0.5
       minipass-pipeline: 1.2.4
@@ -1070,27 +1310,27 @@
       unique-filename: 3.0.0
     dev: true
 
-  /cachedir/2.3.0:
+  /cachedir@2.3.0:
     resolution: {integrity: sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==}
     engines: {node: '>=6'}
     dev: true
 
-  /call-bind/1.0.2:
+  /call-bind@1.0.2:
     resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
     dependencies:
       function-bind: 1.1.1
       get-intrinsic: 1.2.1
     dev: true
 
-  /caniuse-lite/1.0.30001509:
-    resolution: {integrity: sha512-2uDDk+TRiTX5hMcUYT/7CSyzMZxjfGu0vAUjS2g0LSD8UoXOv0LtpH4LxGMemsiPq6LCVIUjNwVM0erkOkGCDA==}
+  /caniuse-lite@1.0.30001588:
+    resolution: {integrity: sha512-+hVY9jE44uKLkH0SrUTqxjxqNTOWHsbnQDIKjwkZ3lNTzUUVdBLBGXtj/q5Mp5u98r3droaZAewQuEDzjQdZlQ==}
     dev: true
 
-  /caseless/0.12.0:
+  /caseless@0.12.0:
     resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
     dev: true
 
-  /chalk/2.4.2:
+  /chalk@2.4.2:
     resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
     engines: {node: '>=4'}
     dependencies:
@@ -1099,7 +1339,7 @@
       supports-color: 5.5.0
     dev: true
 
-  /chalk/4.1.2:
+  /chalk@4.1.2:
     resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
     engines: {node: '>=10'}
     dependencies:
@@ -1107,16 +1347,16 @@
       supports-color: 7.2.0
     dev: true
 
-  /chardet/0.7.0:
+  /chardet@0.7.0:
     resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
     dev: true
 
-  /check-more-types/2.24.0:
+  /check-more-types@2.24.0:
     resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==}
     engines: {node: '>= 0.8.0'}
     dev: true
 
-  /chokidar/3.5.3:
+  /chokidar@3.5.3:
     resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
     engines: {node: '>= 8.10.0'}
     dependencies:
@@ -1131,34 +1371,34 @@
       fsevents: 2.3.2
     dev: true
 
-  /chownr/2.0.0:
+  /chownr@2.0.0:
     resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
     engines: {node: '>=10'}
     dev: true
 
-  /ci-info/3.8.0:
+  /ci-info@3.8.0:
     resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==}
     engines: {node: '>=8'}
     dev: true
 
-  /clean-stack/2.2.0:
+  /clean-stack@2.2.0:
     resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
     engines: {node: '>=6'}
     dev: true
 
-  /cli-cursor/3.1.0:
+  /cli-cursor@3.1.0:
     resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
     engines: {node: '>=8'}
     dependencies:
       restore-cursor: 3.1.0
     dev: true
 
-  /cli-spinners/2.9.0:
+  /cli-spinners@2.9.0:
     resolution: {integrity: sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==}
     engines: {node: '>=6'}
     dev: true
 
-  /cli-table3/0.6.3:
+  /cli-table3@0.6.3:
     resolution: {integrity: sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==}
     engines: {node: 10.* || >= 12.*}
     dependencies:
@@ -1167,7 +1407,7 @@
       '@colors/colors': 1.5.0
     dev: true
 
-  /cli-truncate/2.1.0:
+  /cli-truncate@2.1.0:
     resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==}
     engines: {node: '>=8'}
     dependencies:
@@ -1175,12 +1415,12 @@
       string-width: 4.2.3
     dev: true
 
-  /cli-width/3.0.0:
+  /cli-width@3.0.0:
     resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==}
     engines: {node: '>= 10'}
     dev: true
 
-  /cliui/8.0.1:
+  /cliui@8.0.1:
     resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
     engines: {node: '>=12'}
     dependencies:
@@ -1189,84 +1429,88 @@
       wrap-ansi: 7.0.0
     dev: true
 
-  /clone/1.0.4:
+  /clone@1.0.4:
     resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
     engines: {node: '>=0.8'}
     dev: true
 
-  /color-convert/1.9.3:
+  /color-convert@1.9.3:
     resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
     dependencies:
       color-name: 1.1.3
     dev: true
 
-  /color-convert/2.0.1:
+  /color-convert@2.0.1:
     resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
     engines: {node: '>=7.0.0'}
     dependencies:
       color-name: 1.1.4
     dev: true
 
-  /color-name/1.1.3:
+  /color-name@1.1.3:
     resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
     dev: true
 
-  /color-name/1.1.4:
+  /color-name@1.1.4:
     resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
     dev: true
 
-  /color-support/1.1.3:
+  /color-support@1.1.3:
     resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
     hasBin: true
     dev: true
 
-  /colorette/2.0.20:
+  /colorette@2.0.20:
     resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
     dev: true
 
-  /combined-stream/1.0.8:
+  /combined-stream@1.0.8:
     resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
     engines: {node: '>= 0.8'}
     dependencies:
       delayed-stream: 1.0.0
     dev: true
 
-  /commander/2.20.3:
+  /commander@2.20.3:
     resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
     dev: true
 
-  /commander/4.1.1:
+  /commander@4.1.1:
     resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
     engines: {node: '>= 6'}
     dev: true
 
-  /commander/5.1.0:
+  /commander@5.1.0:
     resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==}
     engines: {node: '>= 6'}
     dev: true
 
-  /common-tags/1.8.2:
+  /common-tags@1.8.2:
     resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==}
     engines: {node: '>=4.0.0'}
     dev: true
 
-  /concat-map/0.0.1:
+  /concat-map@0.0.1:
     resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
     dev: true
 
-  /console-control-strings/1.1.0:
+  /console-control-strings@1.1.0:
     resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
     dev: true
 
-  /convert-source-map/1.9.0:
+  /convert-source-map@1.9.0:
     resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
     dev: true
 
-  /core-util-is/1.0.2:
+  /convert-source-map@2.0.0:
+    resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+    dev: true
+
+  /core-util-is@1.0.2:
     resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
     dev: true
 
-  /cross-spawn/7.0.3:
+  /cross-spawn@7.0.3:
     resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
     engines: {node: '>= 8'}
     dependencies:
@@ -1275,14 +1519,14 @@
       which: 2.0.2
     dev: true
 
-  /cypress/12.3.0:
+  /cypress@12.3.0:
     resolution: {integrity: sha512-ZQNebibi6NBt51TRxRMYKeFvIiQZ01t50HSy7z/JMgRVqBUey3cdjog5MYEbzG6Ktti5ckDt1tfcC47lmFwXkw==}
     engines: {node: ^14.0.0 || ^16.0.0 || >=18.0.0}
     hasBin: true
     requiresBuild: true
     dependencies:
       '@cypress/request': 2.88.11
-      '@cypress/xvfb': 1.2.4_supports-color@8.1.1
+      '@cypress/xvfb': 1.2.4(supports-color@8.1.1)
       '@types/node': 14.18.53
       '@types/sinonjs__fake-timers': 8.1.1
       '@types/sizzle': 2.3.3
@@ -1298,19 +1542,19 @@
       commander: 5.1.0
       common-tags: 1.8.2
       dayjs: 1.11.9
-      debug: 4.3.4_supports-color@8.1.1
+      debug: 4.3.4(supports-color@8.1.1)
       enquirer: 2.3.6
       eventemitter2: 6.4.7
       execa: 4.1.0
       executable: 4.1.1
-      extract-zip: 2.0.1_supports-color@8.1.1
+      extract-zip: 2.0.1(supports-color@8.1.1)
       figures: 3.2.0
       fs-extra: 9.1.0
       getos: 3.2.1
       is-ci: 3.0.1
       is-installed-globally: 0.4.0
       lazy-ass: 1.6.0
-      listr2: 3.14.0_enquirer@2.3.6
+      listr2: 3.14.0(enquirer@2.3.6)
       lodash: 4.17.21
       log-symbols: 4.1.0
       minimist: 1.2.8
@@ -1325,18 +1569,18 @@
       yauzl: 2.10.0
     dev: true
 
-  /dashdash/1.14.1:
+  /dashdash@1.14.1:
     resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==}
     engines: {node: '>=0.10'}
     dependencies:
       assert-plus: 1.0.0
     dev: true
 
-  /dayjs/1.11.9:
+  /dayjs@1.11.9:
     resolution: {integrity: sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==}
     dev: true
 
-  /debug/3.2.7_supports-color@8.1.1:
+  /debug@3.2.7(supports-color@8.1.1):
     resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
     peerDependencies:
       supports-color: '*'
@@ -1348,19 +1592,7 @@
       supports-color: 8.1.1
     dev: true
 
-  /debug/4.3.4:
-    resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
-    engines: {node: '>=6.0'}
-    peerDependencies:
-      supports-color: '*'
-    peerDependenciesMeta:
-      supports-color:
-        optional: true
-    dependencies:
-      ms: 2.1.2
-    dev: true
-
-  /debug/4.3.4_supports-color@8.1.1:
+  /debug@4.3.4(supports-color@8.1.1):
     resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
     engines: {node: '>=6.0'}
     peerDependencies:
@@ -1373,65 +1605,55 @@
       supports-color: 8.1.1
     dev: true
 
-  /deepmerge/4.3.1:
+  /deepmerge@4.3.1:
     resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
     engines: {node: '>=0.10.0'}
     dev: true
 
-  /defaults/1.0.4:
+  /defaults@1.0.4:
     resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
     dependencies:
       clone: 1.0.4
     dev: true
 
-  /define-lazy-prop/2.0.0:
+  /define-lazy-prop@2.0.0:
     resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
     engines: {node: '>=8'}
     dev: true
 
-  /delayed-stream/1.0.0:
+  /delayed-stream@1.0.0:
     resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
     engines: {node: '>=0.4.0'}
     dev: true
 
-  /delegates/1.0.0:
+  /delegates@1.0.0:
     resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
     dev: true
 
-  /depd/2.0.0:
-    resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
-    engines: {node: '>= 0.8'}
-    dev: true
-
-  /dependency-graph/0.11.0:
-    resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==}
-    engines: {node: '>= 0.6.0'}
-    dev: true
-
-  /eastasianwidth/0.2.0:
+  /eastasianwidth@0.2.0:
     resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
     dev: true
 
-  /ecc-jsbn/0.1.2:
+  /ecc-jsbn@0.1.2:
     resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==}
     dependencies:
       jsbn: 0.1.1
       safer-buffer: 2.1.2
     dev: true
 
-  /electron-to-chromium/1.4.447:
-    resolution: {integrity: sha512-sxX0LXh+uL41hSJsujAN86PjhrV/6c79XmpY0TvjZStV6VxIgarf8SRkUoUTuYmFcZQTemsoqo8qXOGw5npWfw==}
+  /electron-to-chromium@1.4.679:
+    resolution: {integrity: sha512-NhQMsz5k0d6m9z3qAxnsOR/ebal4NAGsrNVRwcDo4Kc/zQ7KdsTKZUxZoygHcVRb0QDW3waEDIcE3isZ79RP6g==}
     dev: true
 
-  /emoji-regex/8.0.0:
+  /emoji-regex@8.0.0:
     resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
     dev: true
 
-  /emoji-regex/9.2.2:
+  /emoji-regex@9.2.2:
     resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
     dev: true
 
-  /encoding/0.1.13:
+  /encoding@0.1.13:
     resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
     requiresBuild: true
     dependencies:
@@ -1439,47 +1661,47 @@
     dev: true
     optional: true
 
-  /end-of-stream/1.4.4:
+  /end-of-stream@1.4.4:
     resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
     dependencies:
       once: 1.4.0
     dev: true
 
-  /enquirer/2.3.6:
+  /enquirer@2.3.6:
     resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==}
     engines: {node: '>=8.6'}
     dependencies:
       ansi-colors: 4.1.3
     dev: true
 
-  /env-paths/2.2.1:
+  /env-paths@2.2.1:
     resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
     engines: {node: '>=6'}
     dev: true
 
-  /err-code/2.0.3:
+  /err-code@2.0.3:
     resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
     dev: true
 
-  /escalade/3.1.1:
+  /escalade@3.1.1:
     resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
     engines: {node: '>=6'}
     dev: true
 
-  /escape-string-regexp/1.0.5:
+  /escape-string-regexp@1.0.5:
     resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
     engines: {node: '>=0.8.0'}
     dev: true
 
-  /estree-walker/1.0.1:
-    resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==}
+  /estree-walker@2.0.2:
+    resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
     dev: true
 
-  /eventemitter2/6.4.7:
+  /eventemitter2@6.4.7:
     resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==}
     dev: true
 
-  /execa/4.1.0:
+  /execa@4.1.0:
     resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==}
     engines: {node: '>=10'}
     dependencies:
@@ -1494,22 +1716,22 @@
       strip-final-newline: 2.0.0
     dev: true
 
-  /executable/4.1.1:
+  /executable@4.1.1:
     resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==}
     engines: {node: '>=4'}
     dependencies:
       pify: 2.3.0
     dev: true
 
-  /exponential-backoff/3.1.1:
+  /exponential-backoff@3.1.1:
     resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==}
     dev: true
 
-  /extend/3.0.2:
+  /extend@3.0.2:
     resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
     dev: true
 
-  /external-editor/3.1.0:
+  /external-editor@3.1.0:
     resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
     engines: {node: '>=4'}
     dependencies:
@@ -1518,12 +1740,12 @@
       tmp: 0.0.33
     dev: true
 
-  /extract-zip/2.0.1_supports-color@8.1.1:
+  /extract-zip@2.0.1(supports-color@8.1.1):
     resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==}
     engines: {node: '>= 10.17.0'}
     hasBin: true
     dependencies:
-      debug: 4.3.4_supports-color@8.1.1
+      debug: 4.3.4(supports-color@8.1.1)
       get-stream: 5.2.0
       yauzl: 2.10.0
     optionalDependencies:
@@ -1532,36 +1754,36 @@
       - supports-color
     dev: true
 
-  /extsprintf/1.3.0:
+  /extsprintf@1.3.0:
     resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==}
     engines: {'0': node >=0.6.0}
     dev: true
 
-  /fast-deep-equal/3.1.3:
+  /fast-deep-equal@3.1.3:
     resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
     dev: true
 
-  /fd-slicer/1.1.0:
+  /fd-slicer@1.1.0:
     resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
     dependencies:
       pend: 1.2.0
     dev: true
 
-  /figures/3.2.0:
+  /figures@3.2.0:
     resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
     engines: {node: '>=8'}
     dependencies:
       escape-string-regexp: 1.0.5
     dev: true
 
-  /fill-range/7.0.1:
+  /fill-range@7.0.1:
     resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
     engines: {node: '>=8'}
     dependencies:
       to-regex-range: 5.0.1
     dev: true
 
-  /foreground-child/3.1.1:
+  /foreground-child@3.1.1:
     resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==}
     engines: {node: '>=14'}
     dependencies:
@@ -1569,11 +1791,11 @@
       signal-exit: 4.0.2
     dev: true
 
-  /forever-agent/0.6.1:
+  /forever-agent@0.6.1:
     resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==}
     dev: true
 
-  /form-data/2.3.3:
+  /form-data@2.3.3:
     resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==}
     engines: {node: '>= 0.12'}
     dependencies:
@@ -1582,7 +1804,7 @@
       mime-types: 2.1.35
     dev: true
 
-  /fs-extra/9.1.0:
+  /fs-extra@9.1.0:
     resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
     engines: {node: '>=10'}
     dependencies:
@@ -1592,29 +1814,29 @@
       universalify: 2.0.0
     dev: true
 
-  /fs-minipass/2.1.0:
+  /fs-minipass@2.1.0:
     resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
     engines: {node: '>= 8'}
     dependencies:
       minipass: 3.3.6
     dev: true
 
-  /fs-minipass/3.0.2:
+  /fs-minipass@3.0.2:
     resolution: {integrity: sha512-2GAfyfoaCDRrM6jaOS3UsBts8yJ55VioXdWcOL7dK9zdAuKT71+WBA4ifnNYqVjYv+4SsPxjK0JT4yIIn4cA/g==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dependencies:
       minipass: 5.0.0
     dev: true
 
-  /fs-readdir-recursive/1.1.0:
+  /fs-readdir-recursive@1.1.0:
     resolution: {integrity: sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==}
     dev: true
 
-  /fs.realpath/1.0.0:
+  /fs.realpath@1.0.0:
     resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
     dev: true
 
-  /fsevents/2.3.2:
+  /fsevents@2.3.2:
     resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
     engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
     os: [darwin]
@@ -1622,11 +1844,15 @@
     dev: true
     optional: true
 
-  /function-bind/1.1.1:
+  /function-bind@1.1.1:
     resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
     dev: true
 
-  /gauge/4.0.4:
+  /function-bind@1.1.2:
+    resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+    dev: true
+
+  /gauge@4.0.4:
     resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==}
     engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
     dependencies:
@@ -1640,17 +1866,17 @@
       wide-align: 1.1.5
     dev: true
 
-  /gensync/1.0.0-beta.2:
+  /gensync@1.0.0-beta.2:
     resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
     engines: {node: '>=6.9.0'}
     dev: true
 
-  /get-caller-file/2.0.5:
+  /get-caller-file@2.0.5:
     resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
     engines: {node: 6.* || 8.* || >= 10.*}
     dev: true
 
-  /get-intrinsic/1.2.1:
+  /get-intrinsic@1.2.1:
     resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==}
     dependencies:
       function-bind: 1.1.1
@@ -1659,45 +1885,45 @@
       has-symbols: 1.0.3
     dev: true
 
-  /get-stream/5.2.0:
+  /get-stream@5.2.0:
     resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
     engines: {node: '>=8'}
     dependencies:
       pump: 3.0.0
     dev: true
 
-  /getos/3.2.1:
+  /getos@3.2.1:
     resolution: {integrity: sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==}
     dependencies:
       async: 3.2.4
     dev: true
 
-  /getpass/0.1.7:
+  /getpass@0.1.7:
     resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==}
     dependencies:
       assert-plus: 1.0.0
     dev: true
 
-  /glob-parent/5.1.2:
+  /glob-parent@5.1.2:
     resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
     engines: {node: '>= 6'}
     dependencies:
       is-glob: 4.0.3
     dev: true
 
-  /glob/10.3.1:
-    resolution: {integrity: sha512-9BKYcEeIs7QwlCYs+Y3GBvqAMISufUS0i2ELd11zpZjxI5V9iyRj0HgzB5/cLf2NY4vcYBTYzJ7GIui7j/4DOw==}
+  /glob@10.3.10:
+    resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==}
     engines: {node: '>=16 || 14 >=14.17'}
     hasBin: true
     dependencies:
       foreground-child: 3.1.1
-      jackspeak: 2.2.1
-      minimatch: 9.0.2
-      minipass: 5.0.0
-      path-scurry: 1.10.0
+      jackspeak: 2.3.6
+      minimatch: 9.0.3
+      minipass: 7.0.4
+      path-scurry: 1.10.1
     dev: true
 
-  /glob/7.2.3:
+  /glob@7.2.3:
     resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
     dependencies:
       fs.realpath: 1.0.0
@@ -1708,61 +1934,79 @@
       path-is-absolute: 1.0.1
     dev: true
 
-  /global-dirs/3.0.1:
+  /glob@8.1.0:
+    resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
+    engines: {node: '>=12'}
+    dependencies:
+      fs.realpath: 1.0.0
+      inflight: 1.0.6
+      inherits: 2.0.4
+      minimatch: 5.1.6
+      once: 1.4.0
+    dev: true
+
+  /global-dirs@3.0.1:
     resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==}
     engines: {node: '>=10'}
     dependencies:
       ini: 2.0.0
     dev: true
 
-  /globals/11.12.0:
+  /globals@11.12.0:
     resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
     engines: {node: '>=4'}
     dev: true
 
-  /graceful-fs/4.2.11:
+  /graceful-fs@4.2.11:
     resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
     dev: true
 
-  /has-flag/3.0.0:
+  /has-flag@3.0.0:
     resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
     engines: {node: '>=4'}
     dev: true
 
-  /has-flag/4.0.0:
+  /has-flag@4.0.0:
     resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
     engines: {node: '>=8'}
     dev: true
 
-  /has-proto/1.0.1:
+  /has-proto@1.0.1:
     resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==}
     engines: {node: '>= 0.4'}
     dev: true
 
-  /has-symbols/1.0.3:
+  /has-symbols@1.0.3:
     resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
     engines: {node: '>= 0.4'}
     dev: true
 
-  /has-unicode/2.0.1:
+  /has-unicode@2.0.1:
     resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==}
     dev: true
 
-  /has/1.0.3:
+  /has@1.0.3:
     resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
     engines: {node: '>= 0.4.0'}
     dependencies:
       function-bind: 1.1.1
     dev: true
 
-  /hosted-git-info/6.1.1:
+  /hasown@2.0.1:
+    resolution: {integrity: sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      function-bind: 1.1.2
+    dev: true
+
+  /hosted-git-info@6.1.1:
     resolution: {integrity: sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dependencies:
       lru-cache: 7.18.3
     dev: true
 
-  /html-insert-assets/0.14.3:
+  /html-insert-assets@0.14.3:
     resolution: {integrity: sha512-4st+C8j3KFwzo8nZE8g7lgzuF+8l6+0WxhXKszV0+siYtbP4WZCHO4U2DVnW/9PJ4PSQYUuz/u92pXByDzZdJg==}
     hasBin: true
     dependencies:
@@ -1770,22 +2014,22 @@
       parse5: 6.0.1
     dev: true
 
-  /http-cache-semantics/4.1.1:
+  /http-cache-semantics@4.1.1:
     resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
     dev: true
 
-  /http-proxy-agent/5.0.0:
+  /http-proxy-agent@5.0.0:
     resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
     engines: {node: '>= 6'}
     dependencies:
       '@tootallnate/once': 2.0.0
       agent-base: 6.0.2
-      debug: 4.3.4
+      debug: 4.3.4(supports-color@8.1.1)
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /http-signature/1.3.6:
+  /http-signature@1.3.6:
     resolution: {integrity: sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==}
     engines: {node: '>=0.10'}
     dependencies:
@@ -1794,85 +2038,90 @@
       sshpk: 1.17.0
     dev: true
 
-  /https-proxy-agent/5.0.1:
+  /https-proxy-agent@5.0.1:
     resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
     engines: {node: '>= 6'}
     dependencies:
       agent-base: 6.0.2
-      debug: 4.3.4
+      debug: 4.3.4(supports-color@8.1.1)
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /human-signals/1.1.1:
+  /human-signals@1.1.1:
     resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==}
     engines: {node: '>=8.12.0'}
     dev: true
 
-  /humanize-ms/1.2.1:
+  /humanize-ms@1.2.1:
     resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
     dependencies:
       ms: 2.1.3
     dev: true
 
-  /iconv-lite/0.4.24:
+  /iconv-lite@0.4.24:
     resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
     engines: {node: '>=0.10.0'}
     dependencies:
       safer-buffer: 2.1.2
     dev: true
 
-  /iconv-lite/0.6.3:
+  /iconv-lite@0.6.3:
     resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
     engines: {node: '>=0.10.0'}
+    requiresBuild: true
     dependencies:
       safer-buffer: 2.1.2
     dev: true
     optional: true
 
-  /ieee754/1.2.1:
+  /ieee754@1.2.1:
     resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
     dev: true
 
-  /ignore-walk/6.0.3:
-    resolution: {integrity: sha512-C7FfFoTA+bI10qfeydT8aZbvr91vAEU+2W5BZUlzPec47oNb07SsOfwYrtxuvOYdUApPP/Qlh4DtAO51Ekk2QA==}
+  /ignore-walk@6.0.4:
+    resolution: {integrity: sha512-t7sv42WkwFkyKbivUCglsQW5YWMskWtbEf4MNKX5u/CCWHKSPzN4FtBQGsQZgCLbxOzpVlcbWVK5KB3auIOjSw==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dependencies:
-      minimatch: 9.0.2
+      minimatch: 9.0.3
     dev: true
 
-  /imurmurhash/0.1.4:
+  /imurmurhash@0.1.4:
     resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
     engines: {node: '>=0.8.19'}
     dev: true
 
-  /indent-string/4.0.0:
+  /indent-string@4.0.0:
     resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
     engines: {node: '>=8'}
     dev: true
 
-  /inflight/1.0.6:
+  /infer-owner@1.0.4:
+    resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==}
+    dev: true
+
+  /inflight@1.0.6:
     resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
     dependencies:
       once: 1.4.0
       wrappy: 1.0.2
     dev: true
 
-  /inherits/2.0.4:
+  /inherits@2.0.4:
     resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
     dev: true
 
-  /ini/2.0.0:
+  /ini@2.0.0:
     resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==}
     engines: {node: '>=10'}
     dev: true
 
-  /ini/3.0.1:
-    resolution: {integrity: sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==}
-    engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+  /ini@4.1.1:
+    resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==}
+    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dev: true
 
-  /inquirer/8.2.4:
+  /inquirer@8.2.4:
     resolution: {integrity: sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==}
     engines: {node: '>=12.0.0'}
     dependencies:
@@ -1893,54 +2142,67 @@
       wrap-ansi: 7.0.0
     dev: true
 
-  /ip/2.0.0:
+  /ip@2.0.0:
     resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==}
     dev: true
 
-  /is-binary-path/2.1.0:
+  /is-binary-path@2.1.0:
     resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
     engines: {node: '>=8'}
     dependencies:
       binary-extensions: 2.2.0
     dev: true
 
-  /is-ci/3.0.1:
+  /is-builtin-module@3.2.1:
+    resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==}
+    engines: {node: '>=6'}
+    dependencies:
+      builtin-modules: 3.3.0
+    dev: true
+
+  /is-ci@3.0.1:
     resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==}
     hasBin: true
     dependencies:
       ci-info: 3.8.0
     dev: true
 
-  /is-core-module/2.12.1:
+  /is-core-module@2.12.1:
     resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==}
     dependencies:
       has: 1.0.3
     dev: true
 
-  /is-docker/2.2.1:
+  /is-core-module@2.13.1:
+    resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==}
+    dependencies:
+      hasown: 2.0.1
+    dev: true
+
+  /is-docker@2.2.1:
     resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
     engines: {node: '>=8'}
     hasBin: true
     dev: true
 
-  /is-extglob/2.1.1:
+  /is-extglob@2.1.1:
     resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
     engines: {node: '>=0.10.0'}
     dev: true
 
-  /is-fullwidth-code-point/3.0.0:
+  /is-fullwidth-code-point@3.0.0:
     resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
     engines: {node: '>=8'}
     dev: true
 
-  /is-glob/4.0.3:
+  /is-glob@4.0.3:
     resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
     engines: {node: '>=0.10.0'}
     dependencies:
       is-extglob: 2.1.1
     dev: true
 
-  /is-installed-globally/0.4.0:
+  /is-installed-globally@0.4.0:
     resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==}
     engines: {node: '>=10'}
     dependencies:
@@ -1948,60 +2210,60 @@
       is-path-inside: 3.0.3
     dev: true
 
-  /is-interactive/1.0.0:
+  /is-interactive@1.0.0:
     resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
     engines: {node: '>=8'}
     dev: true
 
-  /is-lambda/1.0.1:
+  /is-lambda@1.0.1:
     resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==}
     dev: true
 
-  /is-module/1.0.0:
+  /is-module@1.0.0:
     resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==}
     dev: true
 
-  /is-number/7.0.0:
+  /is-number@7.0.0:
     resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
     engines: {node: '>=0.12.0'}
     dev: true
 
-  /is-path-inside/3.0.3:
+  /is-path-inside@3.0.3:
     resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
     engines: {node: '>=8'}
     dev: true
 
-  /is-stream/2.0.1:
+  /is-stream@2.0.1:
     resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
     engines: {node: '>=8'}
     dev: true
 
-  /is-typedarray/1.0.0:
+  /is-typedarray@1.0.0:
     resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==}
     dev: true
 
-  /is-unicode-supported/0.1.0:
+  /is-unicode-supported@0.1.0:
     resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
     engines: {node: '>=10'}
     dev: true
 
-  /is-wsl/2.2.0:
+  /is-wsl@2.2.0:
     resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
     engines: {node: '>=8'}
     dependencies:
       is-docker: 2.2.1
     dev: true
 
-  /isexe/2.0.0:
+  /isexe@2.0.0:
     resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
     dev: true
 
-  /isstream/0.1.2:
+  /isstream@0.1.2:
     resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==}
     dev: true
 
-  /jackspeak/2.2.1:
-    resolution: {integrity: sha512-MXbxovZ/Pm42f6cDIDkl3xpwv1AGwObKwfmjs2nQePiy85tP3fatofl3FC1aBsOtP/6fq5SbtgHwWcMsLP+bDw==}
+  /jackspeak@2.3.6:
+    resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==}
     engines: {node: '>=14'}
     dependencies:
       '@isaacs/cliui': 8.0.2
@@ -2009,48 +2271,48 @@
       '@pkgjs/parseargs': 0.11.0
     dev: true
 
-  /js-tokens/4.0.0:
+  /js-tokens@4.0.0:
     resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
     dev: true
 
-  /jsbn/0.1.1:
+  /jsbn@0.1.1:
     resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==}
     dev: true
 
-  /jsesc/2.5.2:
+  /jsesc@2.5.2:
     resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
     engines: {node: '>=4'}
     hasBin: true
     dev: true
 
-  /json-parse-even-better-errors/3.0.0:
+  /json-parse-even-better-errors@3.0.0:
     resolution: {integrity: sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dev: true
 
-  /json-schema-traverse/1.0.0:
+  /json-schema-traverse@1.0.0:
     resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
     dev: true
 
-  /json-schema/0.4.0:
+  /json-schema@0.4.0:
     resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
     dev: true
 
-  /json-stringify-safe/5.0.1:
+  /json-stringify-safe@5.0.1:
     resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
     dev: true
 
-  /json5/2.2.3:
+  /json5@2.2.3:
     resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
     engines: {node: '>=6'}
     hasBin: true
     dev: true
 
-  /jsonc-parser/3.2.0:
+  /jsonc-parser@3.2.0:
     resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
     dev: true
 
-  /jsonfile/6.1.0:
+  /jsonfile@6.1.0:
     resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
     dependencies:
       universalify: 2.0.0
@@ -2058,12 +2320,12 @@
       graceful-fs: 4.2.11
     dev: true
 
-  /jsonparse/1.3.1:
+  /jsonparse@1.3.1:
     resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
     engines: {'0': node >= 0.2.0}
     dev: true
 
-  /jsprim/2.0.2:
+  /jsprim@2.0.2:
     resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==}
     engines: {'0': node >=0.6.0}
     dependencies:
@@ -2073,12 +2335,12 @@
       verror: 1.10.0
     dev: true
 
-  /lazy-ass/1.6.0:
+  /lazy-ass@1.6.0:
     resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==}
     engines: {node: '> 0.8'}
     dev: true
 
-  /listr2/3.14.0_enquirer@2.3.6:
+  /listr2@3.14.0(enquirer@2.3.6):
     resolution: {integrity: sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==}
     engines: {node: '>=10.0.0'}
     peerDependencies:
@@ -2098,15 +2360,15 @@
       wrap-ansi: 7.0.0
     dev: true
 
-  /lodash.once/4.1.1:
+  /lodash.once@4.1.1:
     resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
     dev: true
 
-  /lodash/4.17.21:
+  /lodash@4.17.21:
     resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
     dev: true
 
-  /log-symbols/4.1.0:
+  /log-symbols@4.1.0:
     resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
     engines: {node: '>=10'}
     dependencies:
@@ -2114,7 +2376,7 @@
       is-unicode-supported: 0.1.0
     dev: true
 
-  /log-update/4.0.0:
+  /log-update@4.0.0:
     resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==}
     engines: {node: '>=10'}
     dependencies:
@@ -2124,37 +2386,37 @@
       wrap-ansi: 6.2.0
     dev: true
 
-  /lru-cache/10.0.0:
-    resolution: {integrity: sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw==}
+  /lru-cache@10.2.0:
+    resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==}
     engines: {node: 14 || >=16.14}
     dev: true
 
-  /lru-cache/5.1.1:
+  /lru-cache@5.1.1:
     resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
     dependencies:
       yallist: 3.1.1
     dev: true
 
-  /lru-cache/6.0.0:
+  /lru-cache@6.0.0:
     resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
     engines: {node: '>=10'}
     dependencies:
       yallist: 4.0.0
     dev: true
 
-  /lru-cache/7.18.3:
+  /lru-cache@7.18.3:
     resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
     engines: {node: '>=12'}
     dev: true
 
-  /magic-string/0.27.0:
-    resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==}
+  /magic-string@0.30.1:
+    resolution: {integrity: sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==}
     engines: {node: '>=12'}
     dependencies:
       '@jridgewell/sourcemap-codec': 1.4.15
     dev: true
 
-  /make-dir/2.1.0:
+  /make-dir@2.1.0:
     resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==}
     engines: {node: '>=6'}
     dependencies:
@@ -2162,12 +2424,37 @@
       semver: 5.7.1
     dev: true
 
-  /make-fetch-happen/11.1.1:
+  /make-fetch-happen@10.2.1:
+    resolution: {integrity: sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==}
+    engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+    dependencies:
+      agentkeepalive: 4.5.0
+      cacache: 16.1.3
+      http-cache-semantics: 4.1.1
+      http-proxy-agent: 5.0.0
+      https-proxy-agent: 5.0.1
+      is-lambda: 1.0.1
+      lru-cache: 7.18.3
+      minipass: 3.3.6
+      minipass-collect: 1.0.2
+      minipass-fetch: 2.1.2
+      minipass-flush: 1.0.5
+      minipass-pipeline: 1.2.4
+      negotiator: 0.6.3
+      promise-retry: 2.0.1
+      socks-proxy-agent: 7.0.0
+      ssri: 9.0.1
+    transitivePeerDependencies:
+      - bluebird
+      - supports-color
+    dev: true
+
+  /make-fetch-happen@11.1.1:
     resolution: {integrity: sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dependencies:
-      agentkeepalive: 4.3.0
-      cacache: 17.1.3
+      agentkeepalive: 4.5.0
+      cacache: 17.1.4
       http-cache-semantics: 4.1.1
       http-proxy-agent: 5.0.0
       https-proxy-agent: 5.0.1
@@ -2185,52 +2472,70 @@
       - supports-color
     dev: true
 
-  /merge-stream/2.0.0:
+  /merge-stream@2.0.0:
     resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
     dev: true
 
-  /mime-db/1.52.0:
+  /mime-db@1.52.0:
     resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
     engines: {node: '>= 0.6'}
     dev: true
 
-  /mime-types/2.1.35:
+  /mime-types@2.1.35:
     resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
     engines: {node: '>= 0.6'}
     dependencies:
       mime-db: 1.52.0
     dev: true
 
-  /mimic-fn/2.1.0:
+  /mimic-fn@2.1.0:
     resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
     engines: {node: '>=6'}
     dev: true
 
-  /minimatch/3.1.2:
+  /minimatch@3.1.2:
     resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
     dependencies:
       brace-expansion: 1.1.11
     dev: true
 
-  /minimatch/9.0.2:
-    resolution: {integrity: sha512-PZOT9g5v2ojiTL7r1xF6plNHLtOeTpSlDI007As2NlA2aYBMfVom17yqa6QzhmDP8QOhn7LjHTg7DFCVSSa6yg==}
+  /minimatch@5.1.6:
+    resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
+    engines: {node: '>=10'}
+    dependencies:
+      brace-expansion: 2.0.1
+    dev: true
+
+  /minimatch@9.0.3:
+    resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==}
     engines: {node: '>=16 || 14 >=14.17'}
     dependencies:
       brace-expansion: 2.0.1
     dev: true
 
-  /minimist/1.2.8:
+  /minimist@1.2.8:
     resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
     dev: true
 
-  /minipass-collect/1.0.2:
+  /minipass-collect@1.0.2:
     resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==}
     engines: {node: '>= 8'}
     dependencies:
       minipass: 3.3.6
     dev: true
 
-  /minipass-fetch/3.0.3:
+  /minipass-fetch@2.1.2:
+    resolution: {integrity: sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==}
+    engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+    dependencies:
+      minipass: 3.3.6
+      minipass-sized: 1.0.3
+      minizlib: 2.1.2
+    optionalDependencies:
+      encoding: 0.1.13
+    dev: true
+
+  /minipass-fetch@3.0.3:
     resolution: {integrity: sha512-n5ITsTkDqYkYJZjcRWzZt9qnZKCT7nKCosJhHoj7S7zD+BP4jVbWs+odsniw5TA3E0sLomhTKOKjF86wf11PuQ==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dependencies:
@@ -2241,52 +2546,52 @@
       encoding: 0.1.13
     dev: true
 
-  /minipass-flush/1.0.5:
+  /minipass-flush@1.0.5:
     resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==}
     engines: {node: '>= 8'}
     dependencies:
       minipass: 3.3.6
     dev: true
 
-  /minipass-json-stream/1.0.1:
+  /minipass-json-stream@1.0.1:
     resolution: {integrity: sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==}
     dependencies:
       jsonparse: 1.3.1
       minipass: 3.3.6
     dev: true
 
-  /minipass-pipeline/1.2.4:
+  /minipass-pipeline@1.2.4:
     resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==}
     engines: {node: '>=8'}
     dependencies:
       minipass: 3.3.6
     dev: true
 
-  /minipass-sized/1.0.3:
+  /minipass-sized@1.0.3:
     resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==}
     engines: {node: '>=8'}
     dependencies:
       minipass: 3.3.6
     dev: true
 
-  /minipass/3.3.6:
+  /minipass@3.3.6:
     resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
     engines: {node: '>=8'}
     dependencies:
       yallist: 4.0.0
     dev: true
 
-  /minipass/4.2.8:
-    resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==}
-    engines: {node: '>=8'}
-    dev: true
-
-  /minipass/5.0.0:
+  /minipass@5.0.0:
     resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==}
     engines: {node: '>=8'}
     dev: true
 
-  /minizlib/2.1.2:
+  /minipass@7.0.4:
+    resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==}
+    engines: {node: '>=16 || 14 >=14.17'}
+    dev: true
+
+  /minizlib@2.1.2:
     resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
     engines: {node: '>= 8'}
     dependencies:
@@ -2294,31 +2599,31 @@
       yallist: 4.0.0
     dev: true
 
-  /mkdirp/1.0.4:
+  /mkdirp@1.0.4:
     resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
     engines: {node: '>=10'}
     hasBin: true
     dev: true
 
-  /ms/2.1.2:
+  /ms@2.1.2:
     resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
     dev: true
 
-  /ms/2.1.3:
+  /ms@2.1.3:
     resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
     dev: true
 
-  /mute-stream/0.0.8:
+  /mute-stream@0.0.8:
     resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==}
     dev: true
 
-  /negotiator/0.6.3:
+  /negotiator@0.6.3:
     resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
     engines: {node: '>= 0.6'}
     dev: true
 
-  /node-gyp/9.4.0:
-    resolution: {integrity: sha512-dMXsYP6gc9rRbejLXmTbVRYjAHw7ppswsKyMxuxJxxOHzluIO1rGp9TOQgjFJ+2MCqcOcQTOPB/8Xwhr+7s4Eg==}
+  /node-gyp@9.4.1:
+    resolution: {integrity: sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==}
     engines: {node: ^12.13 || ^14.13 || >=16}
     hasBin: true
     dependencies:
@@ -2326,22 +2631,23 @@
       exponential-backoff: 3.1.1
       glob: 7.2.3
       graceful-fs: 4.2.11
-      make-fetch-happen: 11.1.1
+      make-fetch-happen: 10.2.1
       nopt: 6.0.0
       npmlog: 6.0.2
       rimraf: 3.0.2
-      semver: 7.3.8
+      semver: 7.6.0
       tar: 6.1.15
       which: 2.0.2
     transitivePeerDependencies:
+      - bluebird
       - supports-color
     dev: true
 
-  /node-releases/2.0.12:
-    resolution: {integrity: sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==}
+  /node-releases@2.0.14:
+    resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
     dev: true
 
-  /nopt/6.0.0:
+  /nopt@6.0.0:
     resolution: {integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==}
     engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
     hasBin: true
@@ -2349,68 +2655,68 @@
       abbrev: 1.1.1
     dev: true
 
-  /normalize-package-data/5.0.0:
+  /normalize-package-data@5.0.0:
     resolution: {integrity: sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dependencies:
       hosted-git-info: 6.1.1
-      is-core-module: 2.12.1
-      semver: 7.3.8
+      is-core-module: 2.13.1
+      semver: 7.6.0
       validate-npm-package-license: 3.0.4
     dev: true
 
-  /normalize-path/3.0.0:
+  /normalize-path@3.0.0:
     resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
     engines: {node: '>=0.10.0'}
     dev: true
 
-  /npm-bundled/3.0.0:
+  /npm-bundled@3.0.0:
     resolution: {integrity: sha512-Vq0eyEQy+elFpzsKjMss9kxqb9tG3YHg4dsyWuUENuzvSUWe1TCnW/vV9FkhvBk/brEDoDiVd+M1Btosa6ImdQ==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dependencies:
       npm-normalize-package-bin: 3.0.1
     dev: true
 
-  /npm-install-checks/6.1.1:
+  /npm-install-checks@6.1.1:
     resolution: {integrity: sha512-dH3GmQL4vsPtld59cOn8uY0iOqRmqKvV+DLGwNXV/Q7MDgD2QfOADWd/mFXcIE5LVhYYGjA3baz6W9JneqnuCw==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dependencies:
-      semver: 7.3.8
+      semver: 7.6.0
     dev: true
 
-  /npm-normalize-package-bin/3.0.1:
+  /npm-normalize-package-bin@3.0.1:
     resolution: {integrity: sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dev: true
 
-  /npm-package-arg/10.1.0:
+  /npm-package-arg@10.1.0:
     resolution: {integrity: sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dependencies:
       hosted-git-info: 6.1.1
       proc-log: 3.0.0
-      semver: 7.3.8
+      semver: 7.6.0
       validate-npm-package-name: 5.0.0
     dev: true
 
-  /npm-packlist/7.0.4:
+  /npm-packlist@7.0.4:
     resolution: {integrity: sha512-d6RGEuRrNS5/N84iglPivjaJPxhDbZmlbTwTDX2IbcRHG5bZCdtysYMhwiPvcF4GisXHGn7xsxv+GQ7T/02M5Q==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dependencies:
-      ignore-walk: 6.0.3
+      ignore-walk: 6.0.4
     dev: true
 
-  /npm-pick-manifest/8.0.1:
+  /npm-pick-manifest@8.0.1:
     resolution: {integrity: sha512-mRtvlBjTsJvfCCdmPtiu2bdlx8d/KXtF7yNXNWe7G0Z36qWA9Ny5zXsI2PfBZEv7SXgoxTmNaTzGSbbzDZChoA==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dependencies:
       npm-install-checks: 6.1.1
       npm-normalize-package-bin: 3.0.1
       npm-package-arg: 10.1.0
-      semver: 7.3.8
+      semver: 7.6.0
     dev: true
 
-  /npm-registry-fetch/14.0.5:
+  /npm-registry-fetch@14.0.5:
     resolution: {integrity: sha512-kIDMIo4aBm6xg7jOttupWZamsZRkAqMqwqqbVXnUqstY5+tapvv6bkH/qMR76jdgV+YljEUCyWx3hRYMrJiAgA==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dependencies:
@@ -2425,14 +2731,14 @@
       - supports-color
     dev: true
 
-  /npm-run-path/4.0.1:
+  /npm-run-path@4.0.1:
     resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
     engines: {node: '>=8'}
     dependencies:
       path-key: 3.1.1
     dev: true
 
-  /npmlog/6.0.2:
+  /npmlog@6.0.2:
     resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==}
     engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
     dependencies:
@@ -2442,25 +2748,25 @@
       set-blocking: 2.0.0
     dev: true
 
-  /object-inspect/1.12.3:
+  /object-inspect@1.12.3:
     resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==}
     dev: true
 
-  /once/1.4.0:
+  /once@1.4.0:
     resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
     dependencies:
       wrappy: 1.0.2
     dev: true
 
-  /onetime/5.1.2:
+  /onetime@5.1.2:
     resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
     engines: {node: '>=6'}
     dependencies:
       mimic-fn: 2.1.0
     dev: true
 
-  /open/8.4.0:
-    resolution: {integrity: sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==}
+  /open@8.4.2:
+    resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
     engines: {node: '>=12'}
     dependencies:
       define-lazy-prop: 2.0.0
@@ -2468,7 +2774,7 @@
       is-wsl: 2.2.0
     dev: true
 
-  /ora/5.4.1:
+  /ora@5.4.1:
     resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
     engines: {node: '>=10'}
     dependencies:
@@ -2483,24 +2789,24 @@
       wcwidth: 1.0.1
     dev: true
 
-  /os-tmpdir/1.0.2:
+  /os-tmpdir@1.0.2:
     resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
     engines: {node: '>=0.10.0'}
     dev: true
 
-  /ospath/1.2.2:
+  /ospath@1.2.2:
     resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==}
     dev: true
 
-  /p-map/4.0.0:
+  /p-map@4.0.0:
     resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
     engines: {node: '>=10'}
     dependencies:
       aggregate-error: 3.1.0
     dev: true
 
-  /pacote/15.0.8:
-    resolution: {integrity: sha512-UlcumB/XS6xyyIMwg/WwMAyUmga+RivB5KgkRwA1hZNtrx+0Bt41KxHCvg1kr0pZ/ZeD8qjhW4fph6VaYRCbLw==}
+  /pacote@15.2.0:
+    resolution: {integrity: sha512-rJVZeIwHTUta23sIZgEIM62WYwbmGbThdbnkt81ravBplQv+HjyroqnLRNH2+sLJHcGZmLRmhPwACqhfTcOmnA==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     hasBin: true
     dependencies:
@@ -2508,9 +2814,9 @@
       '@npmcli/installed-package-contents': 2.0.2
       '@npmcli/promise-spawn': 6.0.2
       '@npmcli/run-script': 6.0.2
-      cacache: 17.1.3
+      cacache: 17.1.4
       fs-minipass: 3.0.2
-      minipass: 4.2.8
+      minipass: 5.0.0
       npm-package-arg: 10.1.0
       npm-packlist: 7.0.4
       npm-pick-manifest: 8.0.1
@@ -2519,6 +2825,7 @@
       promise-retry: 2.0.1
       read-package-json: 6.0.4
       read-package-json-fast: 3.0.2
+      sigstore: 1.9.0
       ssri: 10.0.4
       tar: 6.1.15
     transitivePeerDependencies:
@@ -2526,76 +2833,76 @@
       - supports-color
     dev: true
 
-  /parse5/6.0.1:
+  /parse5@6.0.1:
     resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==}
     dev: true
 
-  /path-is-absolute/1.0.1:
+  /path-is-absolute@1.0.1:
     resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
     engines: {node: '>=0.10.0'}
     dev: true
 
-  /path-key/3.1.1:
+  /path-key@3.1.1:
     resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
     engines: {node: '>=8'}
     dev: true
 
-  /path-parse/1.0.7:
+  /path-parse@1.0.7:
     resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
     dev: true
 
-  /path-scurry/1.10.0:
-    resolution: {integrity: sha512-tZFEaRQbMLjwrsmidsGJ6wDMv0iazJWk6SfIKnY4Xru8auXgmJkOBa5DUbYFcFD2Rzk2+KDlIiF0GVXNCbgC7g==}
+  /path-scurry@1.10.1:
+    resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==}
     engines: {node: '>=16 || 14 >=14.17'}
     dependencies:
-      lru-cache: 10.0.0
-      minipass: 5.0.0
+      lru-cache: 10.2.0
+      minipass: 7.0.4
     dev: true
 
-  /pend/1.2.0:
+  /pend@1.2.0:
     resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
     dev: true
 
-  /performance-now/2.1.0:
+  /performance-now@2.1.0:
     resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
     dev: true
 
-  /picocolors/1.0.0:
+  /picocolors@1.0.0:
     resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
     dev: true
 
-  /picomatch/2.3.1:
+  /picomatch@2.3.1:
     resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
     engines: {node: '>=8.6'}
     dev: true
 
-  /pify/2.3.0:
+  /pify@2.3.0:
     resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
     engines: {node: '>=0.10.0'}
     dev: true
 
-  /pify/4.0.1:
+  /pify@4.0.1:
     resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
     engines: {node: '>=6'}
     dev: true
 
-  /prettier/2.6.1:
+  /prettier@2.6.1:
     resolution: {integrity: sha512-8UVbTBYGwN37Bs9LERmxCPjdvPxlEowx2urIL6urHzdb3SDq4B/Z6xLFCblrSnE4iKWcS6ziJ3aOYrc1kz/E2A==}
     engines: {node: '>=10.13.0'}
     hasBin: true
     dev: true
 
-  /pretty-bytes/5.6.0:
+  /pretty-bytes@5.6.0:
     resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==}
     engines: {node: '>=6'}
     dev: true
 
-  /proc-log/3.0.0:
+  /proc-log@3.0.0:
     resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dev: true
 
-  /promise-inflight/1.0.1:
+  /promise-inflight@1.0.1:
     resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==}
     peerDependencies:
       bluebird: '*'
@@ -2604,7 +2911,7 @@
         optional: true
     dev: true
 
-  /promise-retry/2.0.1:
+  /promise-retry@2.0.1:
     resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
     engines: {node: '>=10'}
     dependencies:
@@ -2612,34 +2919,34 @@
       retry: 0.12.0
     dev: true
 
-  /proxy-from-env/1.0.0:
+  /proxy-from-env@1.0.0:
     resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==}
     dev: true
 
-  /psl/1.9.0:
+  /psl@1.9.0:
     resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
     dev: true
 
-  /pump/3.0.0:
+  /pump@3.0.0:
     resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
     dependencies:
       end-of-stream: 1.4.4
       once: 1.4.0
     dev: true
 
-  /punycode/2.3.0:
+  /punycode@2.3.0:
     resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
     engines: {node: '>=6'}
     dev: true
 
-  /qs/6.10.4:
+  /qs@6.10.4:
     resolution: {integrity: sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==}
     engines: {node: '>=0.6'}
     dependencies:
       side-channel: 1.0.4
     dev: true
 
-  /read-package-json-fast/3.0.2:
+  /read-package-json-fast@3.0.2:
     resolution: {integrity: sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dependencies:
@@ -2647,17 +2954,17 @@
       npm-normalize-package-bin: 3.0.1
     dev: true
 
-  /read-package-json/6.0.4:
+  /read-package-json@6.0.4:
     resolution: {integrity: sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dependencies:
-      glob: 10.3.1
+      glob: 10.3.10
       json-parse-even-better-errors: 3.0.0
       normalize-package-data: 5.0.0
       npm-normalize-package-bin: 3.0.1
     dev: true
 
-  /readable-stream/3.6.2:
+  /readable-stream@3.6.2:
     resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
     engines: {node: '>= 6'}
     dependencies:
@@ -2666,49 +2973,40 @@
       util-deprecate: 1.0.2
     dev: true
 
-  /readdirp/3.6.0:
+  /readdirp@3.6.0:
     resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
     engines: {node: '>=8.10.0'}
     dependencies:
       picomatch: 2.3.1
     dev: true
 
-  /reflect-metadata/0.1.13:
-    resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==}
+  /reflect-metadata@0.1.14:
+    resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==}
     dev: true
 
-  /request-progress/3.0.0:
+  /request-progress@3.0.0:
     resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==}
     dependencies:
       throttleit: 1.0.0
     dev: true
 
-  /require-directory/2.1.1:
+  /require-directory@2.1.1:
     resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
     engines: {node: '>=0.10.0'}
     dev: true
 
-  /require-from-string/2.0.2:
+  /require-from-string@2.0.2:
     resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
     engines: {node: '>=0.10.0'}
     dev: true
 
-  /requirejs/2.3.6:
+  /requirejs@2.3.6:
     resolution: {integrity: sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==}
     engines: {node: '>=0.4.0'}
     hasBin: true
     dev: true
 
-  /resolve/1.22.1:
-    resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==}
-    hasBin: true
-    dependencies:
-      is-core-module: 2.12.1
-      path-parse: 1.0.7
-      supports-preserve-symlinks-flag: 1.0.0
-    dev: true
-
-  /resolve/1.22.2:
+  /resolve@1.22.2:
     resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==}
     hasBin: true
     dependencies:
@@ -2717,7 +3015,7 @@
       supports-preserve-symlinks-flag: 1.0.0
     dev: true
 
-  /restore-cursor/3.1.0:
+  /restore-cursor@3.1.0:
     resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
     engines: {node: '>=8'}
     dependencies:
@@ -2725,75 +3023,80 @@
       signal-exit: 3.0.7
     dev: true
 
-  /retry/0.12.0:
+  /retry@0.12.0:
     resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
     engines: {node: '>= 4'}
     dev: true
 
-  /rfdc/1.3.0:
+  /rfdc@1.3.0:
     resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==}
     dev: true
 
-  /rimraf/3.0.2:
+  /rimraf@3.0.2:
     resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
     hasBin: true
     dependencies:
       glob: 7.2.3
     dev: true
 
-  /rollup/3.17.2:
-    resolution: {integrity: sha512-qMNZdlQPCkWodrAZ3qnJtvCAl4vpQ8q77uEujVCCbC/6CLB7Lcmvjq7HyiOSnf4fxTT9XgsE36oLHJBH49xjqA==}
-    engines: {node: '>=14.18.0', npm: '>=8.0.0'}
+  /rollup@4.12.0:
+    resolution: {integrity: sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==}
+    engines: {node: '>=18.0.0', npm: '>=8.0.0'}
     hasBin: true
+    dependencies:
+      '@types/estree': 1.0.5
     optionalDependencies:
+      '@rollup/rollup-android-arm-eabi': 4.12.0
+      '@rollup/rollup-android-arm64': 4.12.0
+      '@rollup/rollup-darwin-arm64': 4.12.0
+      '@rollup/rollup-darwin-x64': 4.12.0
+      '@rollup/rollup-linux-arm-gnueabihf': 4.12.0
+      '@rollup/rollup-linux-arm64-gnu': 4.12.0
+      '@rollup/rollup-linux-arm64-musl': 4.12.0
+      '@rollup/rollup-linux-riscv64-gnu': 4.12.0
+      '@rollup/rollup-linux-x64-gnu': 4.12.0
+      '@rollup/rollup-linux-x64-musl': 4.12.0
+      '@rollup/rollup-win32-arm64-msvc': 4.12.0
+      '@rollup/rollup-win32-ia32-msvc': 4.12.0
+      '@rollup/rollup-win32-x64-msvc': 4.12.0
       fsevents: 2.3.2
     dev: true
 
-  /run-async/2.4.1:
+  /run-async@2.4.1:
     resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==}
     engines: {node: '>=0.12.0'}
     dev: true
 
-  /rxjs/6.6.7:
-    resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==}
-    engines: {npm: '>=2.0.0'}
-    dependencies:
-      tslib: 1.14.1
-    dev: true
-
-  /rxjs/7.5.7:
+  /rxjs@7.5.7:
     resolution: {integrity: sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==}
     dependencies:
       tslib: 2.6.0
+
+  /rxjs@7.8.1:
+    resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
+    dependencies:
+      tslib: 2.6.0
     dev: true
 
-  /safe-buffer/5.2.1:
+  /safe-buffer@5.2.1:
     resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
     dev: true
 
-  /safer-buffer/2.1.2:
+  /safer-buffer@2.1.2:
     resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
     dev: true
 
-  /semver/5.7.1:
+  /semver@5.7.1:
     resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
     hasBin: true
     dev: true
 
-  /semver/6.3.0:
-    resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==}
+  /semver@6.3.1:
+    resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
     hasBin: true
     dev: true
 
-  /semver/7.3.8:
-    resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==}
-    engines: {node: '>=10'}
-    hasBin: true
-    dependencies:
-      lru-cache: 6.0.0
-    dev: true
-
-  /semver/7.5.3:
+  /semver@7.5.3:
     resolution: {integrity: sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==}
     engines: {node: '>=10'}
     hasBin: true
@@ -2801,23 +3104,39 @@
       lru-cache: 6.0.0
     dev: true
 
-  /set-blocking/2.0.0:
+  /semver@7.5.4:
+    resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==}
+    engines: {node: '>=10'}
+    hasBin: true
+    dependencies:
+      lru-cache: 6.0.0
+    dev: true
+
+  /semver@7.6.0:
+    resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==}
+    engines: {node: '>=10'}
+    hasBin: true
+    dependencies:
+      lru-cache: 6.0.0
+    dev: true
+
+  /set-blocking@2.0.0:
     resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
     dev: true
 
-  /shebang-command/2.0.0:
+  /shebang-command@2.0.0:
     resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
     engines: {node: '>=8'}
     dependencies:
       shebang-regex: 3.0.0
     dev: true
 
-  /shebang-regex/3.0.0:
+  /shebang-regex@3.0.0:
     resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
     engines: {node: '>=8'}
     dev: true
 
-  /side-channel/1.0.4:
+  /side-channel@1.0.4:
     resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
     dependencies:
       call-bind: 1.0.2
@@ -2825,21 +3144,35 @@
       object-inspect: 1.12.3
     dev: true
 
-  /signal-exit/3.0.7:
+  /signal-exit@3.0.7:
     resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
     dev: true
 
-  /signal-exit/4.0.2:
+  /signal-exit@4.0.2:
     resolution: {integrity: sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==}
     engines: {node: '>=14'}
     dev: true
 
-  /slash/2.0.0:
+  /sigstore@1.9.0:
+    resolution: {integrity: sha512-0Zjz0oe37d08VeOtBIuB6cRriqXse2e8w+7yIy2XSXjshRKxbc2KkhXjL229jXSxEm7UbcjS76wcJDGQddVI9A==}
+    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+    hasBin: true
+    dependencies:
+      '@sigstore/bundle': 1.1.0
+      '@sigstore/protobuf-specs': 0.2.1
+      '@sigstore/sign': 1.0.0
+      '@sigstore/tuf': 1.0.3
+      make-fetch-happen: 11.1.1
+    transitivePeerDependencies:
+      - supports-color
+    dev: true
+
+  /slash@2.0.0:
     resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==}
     engines: {node: '>=6'}
     dev: true
 
-  /slice-ansi/3.0.0:
+  /slice-ansi@3.0.0:
     resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==}
     engines: {node: '>=8'}
     dependencies:
@@ -2848,7 +3181,7 @@
       is-fullwidth-code-point: 3.0.0
     dev: true
 
-  /slice-ansi/4.0.0:
+  /slice-ansi@4.0.0:
     resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
     engines: {node: '>=10'}
     dependencies:
@@ -2857,23 +3190,23 @@
       is-fullwidth-code-point: 3.0.0
     dev: true
 
-  /smart-buffer/4.2.0:
+  /smart-buffer@4.2.0:
     resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
     engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
     dev: true
 
-  /socks-proxy-agent/7.0.0:
+  /socks-proxy-agent@7.0.0:
     resolution: {integrity: sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==}
     engines: {node: '>= 10'}
     dependencies:
       agent-base: 6.0.2
-      debug: 4.3.4
+      debug: 4.3.4(supports-color@8.1.1)
       socks: 2.7.1
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /socks/2.7.1:
+  /socks@2.7.1:
     resolution: {integrity: sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==}
     engines: {node: '>= 10.13.0', npm: '>= 3.0.0'}
     dependencies:
@@ -2881,46 +3214,46 @@
       smart-buffer: 4.2.0
     dev: true
 
-  /source-map-support/0.5.21:
+  /source-map-support@0.5.21:
     resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
     dependencies:
       buffer-from: 1.1.2
       source-map: 0.6.1
     dev: true
 
-  /source-map/0.6.1:
+  /source-map@0.6.1:
     resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
     engines: {node: '>=0.10.0'}
     dev: true
 
-  /source-map/0.7.4:
+  /source-map@0.7.4:
     resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==}
     engines: {node: '>= 8'}
     dev: true
 
-  /spdx-correct/3.2.0:
+  /spdx-correct@3.2.0:
     resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==}
     dependencies:
       spdx-expression-parse: 3.0.1
       spdx-license-ids: 3.0.13
     dev: true
 
-  /spdx-exceptions/2.3.0:
+  /spdx-exceptions@2.3.0:
     resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==}
     dev: true
 
-  /spdx-expression-parse/3.0.1:
+  /spdx-expression-parse@3.0.1:
     resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==}
     dependencies:
       spdx-exceptions: 2.3.0
       spdx-license-ids: 3.0.13
     dev: true
 
-  /spdx-license-ids/3.0.13:
+  /spdx-license-ids@3.0.13:
     resolution: {integrity: sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==}
     dev: true
 
-  /sshpk/1.17.0:
+  /sshpk@1.17.0:
     resolution: {integrity: sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==}
     engines: {node: '>=0.10.0'}
     hasBin: true
@@ -2936,14 +3269,21 @@
       tweetnacl: 0.14.5
     dev: true
 
-  /ssri/10.0.4:
+  /ssri@10.0.4:
     resolution: {integrity: sha512-12+IR2CB2C28MMAw0Ncqwj5QbTcs0nGIhgJzYWzDkb21vWmfNI83KS4f3Ci6GI98WreIfG7o9UXp3C0qbpA8nQ==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dependencies:
       minipass: 5.0.0
     dev: true
 
-  /string-width/4.2.3:
+  /ssri@9.0.1:
+    resolution: {integrity: sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==}
+    engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+    dependencies:
+      minipass: 3.3.6
+    dev: true
+
+  /string-width@4.2.3:
     resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
     engines: {node: '>=8'}
     dependencies:
@@ -2952,7 +3292,7 @@
       strip-ansi: 6.0.1
     dev: true
 
-  /string-width/5.1.2:
+  /string-width@5.1.2:
     resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
     engines: {node: '>=12'}
     dependencies:
@@ -2961,63 +3301,63 @@
       strip-ansi: 7.1.0
     dev: true
 
-  /string_decoder/1.3.0:
+  /string_decoder@1.3.0:
     resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
     dependencies:
       safe-buffer: 5.2.1
     dev: true
 
-  /strip-ansi/6.0.1:
+  /strip-ansi@6.0.1:
     resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
     engines: {node: '>=8'}
     dependencies:
       ansi-regex: 5.0.1
     dev: true
 
-  /strip-ansi/7.1.0:
+  /strip-ansi@7.1.0:
     resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
     engines: {node: '>=12'}
     dependencies:
       ansi-regex: 6.0.1
     dev: true
 
-  /strip-final-newline/2.0.0:
+  /strip-final-newline@2.0.0:
     resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
     engines: {node: '>=6'}
     dev: true
 
-  /supports-color/5.5.0:
+  /supports-color@5.5.0:
     resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
     engines: {node: '>=4'}
     dependencies:
       has-flag: 3.0.0
     dev: true
 
-  /supports-color/7.2.0:
+  /supports-color@7.2.0:
     resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
     engines: {node: '>=8'}
     dependencies:
       has-flag: 4.0.0
     dev: true
 
-  /supports-color/8.1.1:
+  /supports-color@8.1.1:
     resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
     engines: {node: '>=10'}
     dependencies:
       has-flag: 4.0.0
     dev: true
 
-  /supports-preserve-symlinks-flag/1.0.0:
+  /supports-preserve-symlinks-flag@1.0.0:
     resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
     engines: {node: '>= 0.4'}
     dev: true
 
-  /symbol-observable/4.0.0:
+  /symbol-observable@4.0.0:
     resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==}
     engines: {node: '>=0.10'}
     dev: true
 
-  /tar/6.1.15:
+  /tar@6.1.15:
     resolution: {integrity: sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==}
     engines: {node: '>=10'}
     dependencies:
@@ -3029,7 +3369,7 @@
       yallist: 4.0.0
     dev: true
 
-  /terser/5.16.4:
+  /terser@5.16.4:
     resolution: {integrity: sha512-5yEGuZ3DZradbogeYQ1NaGz7rXVBDWujWlx1PT8efXO6Txn+eWbfKqB2bTDVmFXmePFkoLU6XI8UektMIEA0ug==}
     engines: {node: '>=10'}
     hasBin: true
@@ -3040,41 +3380,41 @@
       source-map-support: 0.5.21
     dev: true
 
-  /throttleit/1.0.0:
+  /throttleit@1.0.0:
     resolution: {integrity: sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g==}
     dev: true
 
-  /through/2.3.8:
+  /through@2.3.8:
     resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
     dev: true
 
-  /tmp/0.0.33:
+  /tmp@0.0.33:
     resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
     engines: {node: '>=0.6.0'}
     dependencies:
       os-tmpdir: 1.0.2
     dev: true
 
-  /tmp/0.2.1:
+  /tmp@0.2.1:
     resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==}
     engines: {node: '>=8.17.0'}
     dependencies:
       rimraf: 3.0.2
     dev: true
 
-  /to-fast-properties/2.0.0:
+  /to-fast-properties@2.0.0:
     resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
     engines: {node: '>=4'}
     dev: true
 
-  /to-regex-range/5.0.1:
+  /to-regex-range@5.0.1:
     resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
     engines: {node: '>=8.0'}
     dependencies:
       is-number: 7.0.0
     dev: true
 
-  /tough-cookie/2.5.0:
+  /tough-cookie@2.5.0:
     resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==}
     engines: {node: '>=0.8'}
     dependencies:
@@ -3082,99 +3422,124 @@
       punycode: 2.3.0
     dev: true
 
-  /tslib/1.14.1:
-    resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
-    dev: true
-
-  /tslib/2.6.0:
+  /tslib@2.6.0:
     resolution: {integrity: sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==}
 
-  /tunnel-agent/0.6.0:
+  /tuf-js@1.1.7:
+    resolution: {integrity: sha512-i3P9Kgw3ytjELUfpuKVDNBJvk4u5bXL6gskv572mcevPbSKCV3zt3djhmlEQ65yERjIbOSncy7U4cQJaB1CBCg==}
+    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+    dependencies:
+      '@tufjs/models': 1.0.4
+      debug: 4.3.4(supports-color@8.1.1)
+      make-fetch-happen: 11.1.1
+    transitivePeerDependencies:
+      - supports-color
+    dev: true
+
+  /tunnel-agent@0.6.0:
     resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
     dependencies:
       safe-buffer: 5.2.1
     dev: true
 
-  /tweetnacl/0.14.5:
+  /tweetnacl@0.14.5:
     resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==}
     dev: true
 
-  /type-fest/0.21.3:
+  /type-fest@0.21.3:
     resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
     engines: {node: '>=10'}
     dev: true
 
-  /typescript/4.8.4:
-    resolution: {integrity: sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==}
-    engines: {node: '>=4.2.0'}
+  /typescript@5.1.6:
+    resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==}
+    engines: {node: '>=14.17'}
     hasBin: true
     dev: true
 
-  /unique-filename/3.0.0:
+  /undici-types@5.26.5:
+    resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
+    dev: true
+
+  /unique-filename@2.0.1:
+    resolution: {integrity: sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==}
+    engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+    dependencies:
+      unique-slug: 3.0.0
+    dev: true
+
+  /unique-filename@3.0.0:
     resolution: {integrity: sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dependencies:
       unique-slug: 4.0.0
     dev: true
 
-  /unique-slug/4.0.0:
+  /unique-slug@3.0.0:
+    resolution: {integrity: sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==}
+    engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+    dependencies:
+      imurmurhash: 0.1.4
+    dev: true
+
+  /unique-slug@4.0.0:
     resolution: {integrity: sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dependencies:
       imurmurhash: 0.1.4
     dev: true
 
-  /universalify/2.0.0:
+  /universalify@2.0.0:
     resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
     engines: {node: '>= 10.0.0'}
     dev: true
 
-  /untildify/4.0.0:
+  /untildify@4.0.0:
     resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==}
     engines: {node: '>=8'}
     dev: true
 
-  /update-browserslist-db/1.0.11_browserslist@4.21.9:
-    resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==}
+  /update-browserslist-db@1.0.13(browserslist@4.23.0):
+    resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==}
     hasBin: true
     peerDependencies:
       browserslist: '>= 4.21.0'
     dependencies:
-      browserslist: 4.21.9
+      browserslist: 4.23.0
       escalade: 3.1.1
       picocolors: 1.0.0
     dev: true
 
-  /uri-js/4.4.1:
+  /uri-js@4.4.1:
     resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
     dependencies:
       punycode: 2.3.0
     dev: true
 
-  /util-deprecate/1.0.2:
+  /util-deprecate@1.0.2:
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
     dev: true
 
-  /uuid/8.3.2:
+  /uuid@8.3.2:
     resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
     hasBin: true
     dev: true
 
-  /validate-npm-package-license/3.0.4:
+  /validate-npm-package-license@3.0.4:
     resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
     dependencies:
       spdx-correct: 3.2.0
       spdx-expression-parse: 3.0.1
     dev: true
 
-  /validate-npm-package-name/5.0.0:
+  /validate-npm-package-name@5.0.0:
     resolution: {integrity: sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     dependencies:
       builtins: 5.0.1
     dev: true
 
-  /verror/1.10.0:
+  /verror@1.10.0:
     resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==}
     engines: {'0': node >=0.6.0}
     dependencies:
@@ -3183,13 +3548,13 @@
       extsprintf: 1.3.0
     dev: true
 
-  /wcwidth/1.0.1:
+  /wcwidth@1.0.1:
     resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
     dependencies:
       defaults: 1.0.4
     dev: true
 
-  /which/2.0.2:
+  /which@2.0.2:
     resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
     engines: {node: '>= 8'}
     hasBin: true
@@ -3197,7 +3562,7 @@
       isexe: 2.0.0
     dev: true
 
-  /which/3.0.1:
+  /which@3.0.1:
     resolution: {integrity: sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
     hasBin: true
@@ -3205,13 +3570,13 @@
       isexe: 2.0.0
     dev: true
 
-  /wide-align/1.1.5:
+  /wide-align@1.1.5:
     resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
     dependencies:
       string-width: 4.2.3
     dev: true
 
-  /wrap-ansi/6.2.0:
+  /wrap-ansi@6.2.0:
     resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
     engines: {node: '>=8'}
     dependencies:
@@ -3220,7 +3585,7 @@
       strip-ansi: 6.0.1
     dev: true
 
-  /wrap-ansi/7.0.0:
+  /wrap-ansi@7.0.0:
     resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
     engines: {node: '>=10'}
     dependencies:
@@ -3229,7 +3594,7 @@
       strip-ansi: 6.0.1
     dev: true
 
-  /wrap-ansi/8.1.0:
+  /wrap-ansi@8.1.0:
     resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
     engines: {node: '>=12'}
     dependencies:
@@ -3238,42 +3603,29 @@
       strip-ansi: 7.1.0
     dev: true
 
-  /wrappy/1.0.2:
+  /wrappy@1.0.2:
     resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
     dev: true
 
-  /y18n/5.0.8:
+  /y18n@5.0.8:
     resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
     engines: {node: '>=10'}
     dev: true
 
-  /yallist/3.1.1:
+  /yallist@3.1.1:
     resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
     dev: true
 
-  /yallist/4.0.0:
+  /yallist@4.0.0:
     resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
     dev: true
 
-  /yargs-parser/21.1.1:
+  /yargs-parser@21.1.1:
     resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
     engines: {node: '>=12'}
     dev: true
 
-  /yargs/17.6.2:
-    resolution: {integrity: sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==}
-    engines: {node: '>=12'}
-    dependencies:
-      cliui: 8.0.1
-      escalade: 3.1.1
-      get-caller-file: 2.0.5
-      require-directory: 2.1.1
-      string-width: 4.2.3
-      y18n: 5.0.8
-      yargs-parser: 21.1.1
-    dev: true
-
-  /yargs/17.7.2:
+  /yargs@17.7.2:
     resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
     engines: {node: '>=12'}
     dependencies:
@@ -3286,15 +3638,14 @@
       yargs-parser: 21.1.1
     dev: true
 
-  /yauzl/2.10.0:
+  /yauzl@2.10.0:
     resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
     dependencies:
       buffer-crc32: 0.2.13
       fd-slicer: 1.1.0
     dev: true
 
-  /zone.js/0.11.8:
-    resolution: {integrity: sha512-82bctBg2hKcEJ21humWIkXRlLBBmrc3nN7DFh5LGGhcyycO2S7FN8NmdvlcKaGFDNVL4/9kFLmwmInTavdJERA==}
+  /zone.js@0.13.3:
+    resolution: {integrity: sha512-MKPbmZie6fASC/ps4dkmIhaT5eonHkEt6eAy80K42tAm0G2W+AahLJjbfi6X9NPdciOE9GRFTTM8u2IiF6O3ww==}
     dependencies:
       tslib: 2.6.0
-    dev: true
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index 3cd14a7..7d6e98e 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -1124,7 +1124,7 @@
 		return
 	}
 
-	log.Println("Got actions for match", request.MatchNumber(), "team", request.TeamNumber(), "from", username)
+	log.Println("Got actions for match", request.MatchNumber(), "team", string(request.TeamNumber()), "from", username)
 
 	for i := 0; i < request.ActionsListLength(); i++ {
 
@@ -1195,7 +1195,7 @@
 		return
 	}
 
-	log.Println("Got actions for match", request.MatchNumber(), "team", request.TeamNumber(), "from", username)
+	log.Println("Got actions for match", request.MatchNumber(), "team", string(request.TeamNumber()), "from", username)
 
 	for i := 0; i < request.ActionsListLength(); i++ {
 
diff --git a/scouting/www/app/app.ts b/scouting/www/app/app.ts
index 011c94f..597e5c5 100644
--- a/scouting/www/app/app.ts
+++ b/scouting/www/app/app.ts
@@ -10,7 +10,7 @@
   | 'Pit';
 
 // Ignore the guard for tabs that don't require the user to enter any data.
-const unguardedTabs: Tab[] = ['MatchList'];
+const unguardedTabs: Tab[] = ['MatchList', 'View'];
 
 type TeamInMatch = {
   teamNumber: string;
diff --git a/scouting/www/driver_ranking/package.json b/scouting/www/driver_ranking/package.json
index 83dacf4..38d9358 100644
--- a/scouting/www/driver_ranking/package.json
+++ b/scouting/www/driver_ranking/package.json
@@ -2,6 +2,6 @@
     "name": "@org_frc971/scouting/www/driver_ranking",
     "private": true,
     "dependencies": {
-        "@angular/forms": "15.1.5"
+        "@angular/forms": "v16-lts"
     }
 }
diff --git a/scouting/www/entry/entry.component.ts b/scouting/www/entry/entry.component.ts
index 5b461f8..6da8be8 100644
--- a/scouting/www/entry/entry.component.ts
+++ b/scouting/www/entry/entry.component.ts
@@ -25,7 +25,7 @@
   Action,
 } from '../../webserver/requests/messages/submit_2024_actions_generated';
 import {Match} from '../../webserver/requests/messages/request_all_matches_response_generated';
-import {MatchListRequestor} from '@org_frc971/scouting/www/rpc';
+import {MatchListRequestor} from '../rpc';
 
 type Section =
   | 'Team Selection'
diff --git a/scouting/www/entry/package.json b/scouting/www/entry/package.json
index 4c05778..d37ce10 100644
--- a/scouting/www/entry/package.json
+++ b/scouting/www/entry/package.json
@@ -3,6 +3,6 @@
     "private": true,
     "dependencies": {
         "@org_frc971/scouting/www/counter_button": "workspace:*",
-        "@angular/forms": "15.1.5"
+        "@angular/forms": "v16-lts"
     }
 }
diff --git a/scouting/www/match_list/match_list.component.ts b/scouting/www/match_list/match_list.component.ts
index 0deeb11..8fafdce 100644
--- a/scouting/www/match_list/match_list.component.ts
+++ b/scouting/www/match_list/match_list.component.ts
@@ -7,7 +7,7 @@
   RequestAllMatchesResponse,
 } from '../../webserver/requests/messages/request_all_matches_response_generated';
 
-import {MatchListRequestor} from '@org_frc971/scouting/www/rpc';
+import {MatchListRequestor} from '../rpc';
 
 type TeamInMatch = {
   teamNumber: string;
diff --git a/scouting/www/match_list/package.json b/scouting/www/match_list/package.json
index 284e77b..00977c5 100644
--- a/scouting/www/match_list/package.json
+++ b/scouting/www/match_list/package.json
@@ -3,6 +3,6 @@
     "private": true,
     "dependencies": {
         "@org_frc971/scouting/www/rpc": "workspace:*",
-        "@angular/forms": "15.1.5"
+        "@angular/forms": "v16-lts"
     }
 }
diff --git a/scouting/www/notes/package.json b/scouting/www/notes/package.json
index c5c6afe..f1ad3ae 100644
--- a/scouting/www/notes/package.json
+++ b/scouting/www/notes/package.json
@@ -2,6 +2,6 @@
     "name": "@org_frc971/scouting/www/notes",
     "private": true,
     "dependencies": {
-        "@angular/forms": "15.1.5"
+        "@angular/forms": "v16-lts"
     }
 }
diff --git a/scouting/www/pit_scouting/package.json b/scouting/www/pit_scouting/package.json
index e58fc51..f41150f 100644
--- a/scouting/www/pit_scouting/package.json
+++ b/scouting/www/pit_scouting/package.json
@@ -2,6 +2,6 @@
 	"name": "@org_frc971/scouting/www/pit_scouting",
 	"private": true,
 	"dependencies": {
-			"@angular/forms": "15.1.5"
+			"@angular/forms": "v16-lts"
 	}
 }
diff --git a/scouting/www/shift_schedule/package.json b/scouting/www/shift_schedule/package.json
index 1270235..f2d6d7e 100644
--- a/scouting/www/shift_schedule/package.json
+++ b/scouting/www/shift_schedule/package.json
@@ -2,6 +2,6 @@
     "name": "@org_frc971/scouting/www/shift_schedule",
     "private": true,
     "dependencies": {
-        "@angular/forms": "15.1.5"
+        "@angular/forms": "v16-lts"
     }
 }
diff --git a/scouting/www/view/package.json b/scouting/www/view/package.json
index ef94a11..d214f6f 100644
--- a/scouting/www/view/package.json
+++ b/scouting/www/view/package.json
@@ -2,6 +2,6 @@
     "name": "@org_frc971/scouting/www/view",
     "private": true,
     "dependencies": {
-      "@angular/forms": "15.1.5"
+      "@angular/forms": "v16-lts"
     }
 }
diff --git a/third_party/autocxx/Cargo.lock b/third_party/autocxx/Cargo.lock
index a7872e3..4587dcb 100644
--- a/third_party/autocxx/Cargo.lock
+++ b/third_party/autocxx/Cargo.lock
@@ -1392,9 +1392,9 @@
 
 [[package]]
 name = "rustix"
-version = "0.37.23"
+version = "0.37.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06"
+checksum = "d4eb579851244c2c03e7c24f501c3432bed80b8f720af1d6e5b0e0f01555a035"
 dependencies = [
  "bitflags 1.3.2",
  "errno",
@@ -1561,7 +1561,7 @@
  "cfg-if",
  "fastrand",
  "redox_syscall",
- "rustix 0.37.23",
+ "rustix 0.37.25",
  "windows-sys",
 ]
 
@@ -1590,7 +1590,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237"
 dependencies = [
- "rustix 0.37.23",
+ "rustix 0.37.25",
  "windows-sys",
 ]
 
diff --git a/third_party/autocxx/engine/src/ast_discoverer.rs b/third_party/autocxx/engine/src/ast_discoverer.rs
index f555419..6be9d8f 100644
--- a/third_party/autocxx/engine/src/ast_discoverer.rs
+++ b/third_party/autocxx/engine/src/ast_discoverer.rs
@@ -672,7 +672,7 @@
             }
         };
         discoveries.search_item(&itm, None).unwrap();
-        assert!(discoveries.extern_rust_funs.get(0).unwrap().sig.ident == "bar");
+        assert!(discoveries.extern_rust_funs.first().unwrap().sig.ident == "bar");
     }
 
     #[test]
@@ -686,7 +686,7 @@
             }
         };
         discoveries.search_item(&itm, None).unwrap();
-        assert!(discoveries.extern_rust_funs.get(0).unwrap().sig.ident == "bar");
+        assert!(discoveries.extern_rust_funs.first().unwrap().sig.ident == "bar");
     }
 
     #[test]
@@ -702,7 +702,7 @@
         assert!(
             discoveries
                 .extern_rust_types
-                .get(0)
+                .first()
                 .unwrap()
                 .get_final_ident()
                 == "Bar"
diff --git a/third_party/autocxx/engine/src/conversion/analysis/allocators.rs b/third_party/autocxx/engine/src/conversion/analysis/allocators.rs
index da25a77..aab637c 100644
--- a/third_party/autocxx/engine/src/conversion/analysis/allocators.rs
+++ b/third_party/autocxx/engine/src/conversion/analysis/allocators.rs
@@ -101,11 +101,11 @@
 }
 
 pub(crate) fn get_alloc_name(ty_name: &QualifiedName) -> QualifiedName {
-    get_name(ty_name, "alloc")
+    get_name(ty_name, "autocxx_alloc")
 }
 
 pub(crate) fn get_free_name(ty_name: &QualifiedName) -> QualifiedName {
-    get_name(ty_name, "free")
+    get_name(ty_name, "autocxx_free")
 }
 
 fn get_name(ty_name: &QualifiedName, label: &str) -> QualifiedName {
diff --git a/third_party/autocxx/engine/src/conversion/analysis/depth_first.rs b/third_party/autocxx/engine/src/conversion/analysis/depth_first.rs
index baadbd7..3c6241b 100644
--- a/third_party/autocxx/engine/src/conversion/analysis/depth_first.rs
+++ b/third_party/autocxx/engine/src/conversion/analysis/depth_first.rs
@@ -44,7 +44,7 @@
     type Item = &'a T;
 
     fn next(&mut self) -> Option<Self::Item> {
-        let first_candidate = self.queue.get(0).map(|api| api.name());
+        let first_candidate = self.queue.front().map(|api| api.name());
         while let Some(candidate) = self.queue.pop_front() {
             if !candidate
                 .field_and_base_deps()
@@ -54,7 +54,7 @@
                 return Some(candidate);
             }
             self.queue.push_back(candidate);
-            if self.queue.get(0).map(|api| api.name()) == first_candidate {
+            if self.queue.front().map(|api| api.name()) == first_candidate {
                 panic!(
                     "Failed to find a candidate; there must be a circular dependency. Queue is {}",
                     self.queue
diff --git a/third_party/autocxx/engine/src/conversion/analysis/fun/mod.rs b/third_party/autocxx/engine/src/conversion/analysis/fun/mod.rs
index 415e40a..dc953ad 100644
--- a/third_party/autocxx/engine/src/conversion/analysis/fun/mod.rs
+++ b/third_party/autocxx/engine/src/conversion/analysis/fun/mod.rs
@@ -80,7 +80,7 @@
 
 #[derive(Clone, Debug)]
 pub(crate) enum MethodKind {
-    Normal(ReceiverMutability),
+    Normal,
     Constructor { is_default: bool },
     Static,
     Virtual(ReceiverMutability),
@@ -964,7 +964,7 @@
                     let receiver_mutability =
                         receiver_mutability.expect("Failed to find receiver details");
                     match fun.virtualness {
-                        Virtualness::None => MethodKind::Normal(receiver_mutability),
+                        Virtualness::None => MethodKind::Normal,
                         Virtualness::Virtual => MethodKind::Virtual(receiver_mutability),
                         Virtualness::PureVirtual => MethodKind::PureVirtual(receiver_mutability),
                     }
@@ -1152,7 +1152,7 @@
                     ref impl_for,
                     method_kind:
                         MethodKind::Constructor { .. }
-                        | MethodKind::Normal(..)
+                        | MethodKind::Normal
                         | MethodKind::PureVirtual(..)
                         | MethodKind::Virtual(..),
                     ..
diff --git a/third_party/autocxx/engine/src/conversion/analysis/pod/byvalue_checker.rs b/third_party/autocxx/engine/src/conversion/analysis/pod/byvalue_checker.rs
index 6e2e9b9..d0c828e 100644
--- a/third_party/autocxx/engine/src/conversion/analysis/pod/byvalue_checker.rs
+++ b/third_party/autocxx/engine/src/conversion/analysis/pod/byvalue_checker.rs
@@ -158,6 +158,12 @@
                     ));
                     break;
                 }
+                None if ty_id.get_final_item() == "__BindgenBitfieldUnit" => {
+                    field_safety_problem = PodState::UnsafeToBePod(format!(
+                        "Type {tyname} could not be POD because it is a bitfield"
+                    ));
+                    break;
+                }
                 None => {
                     field_safety_problem = PodState::UnsafeToBePod(format!(
                         "Type {tyname} could not be POD because its dependent type {ty_id} isn't known"
diff --git a/third_party/autocxx/engine/src/conversion/analysis/type_converter.rs b/third_party/autocxx/engine/src/conversion/analysis/type_converter.rs
index 63f7ae9..99cd61d 100644
--- a/third_party/autocxx/engine/src/conversion/analysis/type_converter.rs
+++ b/third_party/autocxx/engine/src/conversion/analysis/type_converter.rs
@@ -251,7 +251,7 @@
                             let i = make_ident(s);
                             parse_quote! { #i }
                         })
-                        .chain(typ.path.segments.into_iter())
+                        .chain(typ.path.segments)
                         .collect();
                 }
             }
diff --git a/third_party/autocxx/engine/src/conversion/codegen_rs/mod.rs b/third_party/autocxx/engine/src/conversion/codegen_rs/mod.rs
index abab612..ee56169 100644
--- a/third_party/autocxx/engine/src/conversion/codegen_rs/mod.rs
+++ b/third_party/autocxx/engine/src/conversion/codegen_rs/mod.rs
@@ -1130,6 +1130,7 @@
         let segs =
             Self::find_output_mod_root(name.get_namespace()).chain(name.get_bindgen_path_idents());
         Item::Use(parse_quote! {
+            #[allow(unused_imports)]
             pub use #(#segs)::*;
         })
     }
diff --git a/third_party/autocxx/engine/src/conversion/mod.rs b/third_party/autocxx/engine/src/conversion/mod.rs
index 61d7d6d..fa24d4d 100644
--- a/third_party/autocxx/engine/src/conversion/mod.rs
+++ b/third_party/autocxx/engine/src/conversion/mod.rs
@@ -114,7 +114,7 @@
             None => Err(ConvertError::NoContent),
             Some((_, items)) => {
                 // Parse the bindgen mod.
-                let items_to_process = items.drain(..).collect();
+                let items_to_process = std::mem::take(items);
                 let parser = ParseBindgen::new(self.config);
                 let apis = parser.parse_items(items_to_process, source_file_contents)?;
                 Self::dump_apis("parsing", &apis);
diff --git a/third_party/autocxx/engine/src/known_types.rs b/third_party/autocxx/engine/src/known_types.rs
index 10199fb..573be4e 100644
--- a/third_party/autocxx/engine/src/known_types.rs
+++ b/third_party/autocxx/engine/src/known_types.rs
@@ -12,7 +12,7 @@
 use once_cell::sync::OnceCell;
 use syn::{parse_quote, TypePath};
 
-//// The behavior of the type.
+/// The behavior of the type.
 #[derive(Debug)]
 enum Behavior {
     CxxContainerPtr,
diff --git a/third_party/autocxx/engine/src/parse_file.rs b/third_party/autocxx/engine/src/parse_file.rs
index 16ea625..9dd620c 100644
--- a/third_party/autocxx/engine/src/parse_file.rs
+++ b/third_party/autocxx/engine/src/parse_file.rs
@@ -21,7 +21,7 @@
 use std::{io::Read, path::PathBuf};
 use std::{panic::UnwindSafe, path::Path, rc::Rc};
 use syn::spanned::Spanned;
-use syn::{token::Brace, Item, ItemMod};
+use syn::Item;
 use thiserror::Error;
 
 /// Errors which may occur when parsing a Rust source file to discover
@@ -119,7 +119,7 @@
                     Segment::Cxx(CxxBridge::from(itm))
                 }
                 Item::Mod(itm) => {
-                    if let Some((brace, items)) = itm.content {
+                    if let Some((_, items)) = itm.content {
                         let mut mod_state = State {
                             auto_allowlist: self.auto_allowlist,
                             ..Default::default()
@@ -137,18 +137,9 @@
                         }
                         self.extra_superclasses.extend(mod_state.extra_superclasses);
                         self.discoveries.extend(mod_state.discoveries);
-                        Segment::Mod(
-                            mod_state.results,
-                            (
-                                brace,
-                                ItemMod {
-                                    content: None,
-                                    ..itm
-                                },
-                            ),
-                        )
+                        Segment::Mod(mod_state.results)
                     } else {
-                        Segment::Other(Item::Mod(itm))
+                        Segment::Other
                     }
                 }
                 Item::Struct(ref its) => {
@@ -189,13 +180,13 @@
                     self.discoveries
                         .search_item(&item, mod_path)
                         .map_err(ParseError::Discovery)?;
-                    Segment::Other(item)
+                    Segment::Other
                 }
                 _ => {
                     self.discoveries
                         .search_item(&item, mod_path)
                         .map_err(ParseError::Discovery)?;
-                    Segment::Other(item)
+                    Segment::Other
                 }
             };
             self.results.push(result);
@@ -283,8 +274,8 @@
 enum Segment {
     Autocxx(IncludeCppEngine),
     Cxx(CxxBridge),
-    Mod(Vec<Segment>, (Brace, ItemMod)),
-    Other(Item),
+    Mod(Vec<Segment>),
+    Other,
 }
 
 pub trait CppBuildable {
@@ -303,7 +294,7 @@
                 .flat_map(|s| -> Box<dyn Iterator<Item = &IncludeCppEngine>> {
                     match s {
                         Segment::Autocxx(includecpp) => Box::new(std::iter::once(includecpp)),
-                        Segment::Mod(segments, _) => Box::new(do_get_autocxxes(segments)),
+                        Segment::Mod(segments) => Box::new(do_get_autocxxes(segments)),
                         _ => Box::new(std::iter::empty()),
                     }
                 })
@@ -331,7 +322,7 @@
                         Segment::Cxx(cxxbridge) => {
                             Box::new(std::iter::once(cxxbridge as &dyn CppBuildable))
                         }
-                        Segment::Mod(segments, _) => Box::new(do_get_cpp_buildables(segments)),
+                        Segment::Mod(segments) => Box::new(do_get_cpp_buildables(segments)),
                         _ => Box::new(std::iter::empty()),
                     }
                 })
@@ -349,7 +340,7 @@
                 .flat_map(|s| -> Box<dyn Iterator<Item = &mut IncludeCppEngine>> {
                     match s {
                         Segment::Autocxx(includecpp) => Box::new(std::iter::once(includecpp)),
-                        Segment::Mod(segments, _) => Box::new(do_get_autocxxes_mut(segments)),
+                        Segment::Mod(segments) => Box::new(do_get_autocxxes_mut(segments)),
                         _ => Box::new(std::iter::empty()),
                     }
                 })
@@ -368,7 +359,7 @@
                 .flat_map(|s| -> Box<dyn Iterator<Item = &PathBuf>> {
                     match s {
                         Segment::Autocxx(includecpp) => Box::new(includecpp.include_dirs()),
-                        Segment::Mod(segments, _) => Box::new(do_get_include_dirs(segments)),
+                        Segment::Mod(segments) => Box::new(do_get_include_dirs(segments)),
                         _ => Box::new(std::iter::empty()),
                     }
                 })
diff --git a/third_party/autocxx/engine/src/types.rs b/third_party/autocxx/engine/src/types.rs
index f7eaae0..3afbf9d 100644
--- a/third_party/autocxx/engine/src/types.rs
+++ b/third_party/autocxx/engine/src/types.rs
@@ -236,6 +236,10 @@
 /// cxx.
 #[derive(Error, Clone, Debug)]
 pub enum InvalidIdentError {
+    #[error("Union are not supported by autocxx (and their bindgen names have __ so are not acceptable to cxx)")]
+    Union,
+    #[error("Bitfields are not supported by autocxx (and their bindgen names have __ so are not acceptable to cxx)")]
+    Bitfield,
     #[error("Names containing __ are reserved by C++ so not acceptable to cxx")]
     TooManyUnderscores,
     #[error("bindgen decided to call this type _bindgen_ty_N because it couldn't deduce the correct name for it. That means we can't generate C++ bindings to it.")]
@@ -251,7 +255,12 @@
 /// where code will be output as part of the `#[cxx::bridge]` mod.
 pub fn validate_ident_ok_for_cxx(id: &str) -> Result<(), InvalidIdentError> {
     validate_ident_ok_for_rust(id)?;
-    if id.contains("__") {
+    // Provide a couple of more specific diagnostics if we can.
+    if id.starts_with("__BindgenBitfieldUnit") {
+        Err(InvalidIdentError::Bitfield)
+    } else if id.starts_with("__BindgenUnionField") {
+        Err(InvalidIdentError::Union)
+    } else if id.contains("__") {
         Err(InvalidIdentError::TooManyUnderscores)
     } else if id.starts_with("_bindgen_ty_") {
         Err(InvalidIdentError::BindgenTy)
diff --git a/third_party/autocxx/gen/cmd/BUILD b/third_party/autocxx/gen/cmd/BUILD
index 60d011c..c05e24d 100644
--- a/third_party/autocxx/gen/cmd/BUILD
+++ b/third_party/autocxx/gen/cmd/BUILD
@@ -22,7 +22,7 @@
     version = "0.16.0",
     deps = [
         "//third_party/autocxx/engine:autocxx_engine",
-        "@crate_index//:clap",
+        "@crate_index//:clap3",
         "@crate_index//:env_logger",
         "@crate_index//:indexmap",
         "@crate_index//:miette",
diff --git a/third_party/autocxx/gen/cmd/Cargo.toml b/third_party/autocxx/gen/cmd/Cargo.toml
index 7a78ba3..55242ff 100644
--- a/third_party/autocxx/gen/cmd/Cargo.toml
+++ b/third_party/autocxx/gen/cmd/Cargo.toml
@@ -23,7 +23,7 @@
 
 [dependencies]
 autocxx-engine = { version = "=0.26.0", path = "../../engine" }
-clap = { version = "3.1.2", features = ["cargo"] }
+clap3 = { package = "clap", version = "3.2.25", features = ["cargo"] }
 proc-macro2 = "1.0"
 env_logger = "0.9.0"
 miette = { version = "5", features = ["fancy"] }
diff --git a/third_party/autocxx/integration-tests/src/lib.rs b/third_party/autocxx/integration-tests/src/lib.rs
index 2335ee5..02d672f 100644
--- a/third_party/autocxx/integration-tests/src/lib.rs
+++ b/third_party/autocxx/integration-tests/src/lib.rs
@@ -459,7 +459,7 @@
     let generated_rs_files = build_results.1;
 
     if let Some(code_checker) = &rust_code_checker {
-        let mut file = File::open(generated_rs_files.get(0).ok_or(TestError::NoRs)?)
+        let mut file = File::open(generated_rs_files.first().ok_or(TestError::NoRs)?)
             .map_err(TestError::RsFileOpen)?;
         let mut content = String::new();
         file.read_to_string(&mut content)
diff --git a/third_party/autocxx/integration-tests/tests/cpprefs_test.rs b/third_party/autocxx/integration-tests/tests/cpprefs_test.rs
index 9cc6d39..820e67a 100644
--- a/third_party/autocxx/integration-tests/tests/cpprefs_test.rs
+++ b/third_party/autocxx/integration-tests/tests/cpprefs_test.rs
@@ -52,6 +52,7 @@
         indoc! {"
         #include <string>
         #include <sstream>
+        #include <cstdint>
 
         class Goat {
             public:
@@ -80,6 +81,7 @@
         indoc! {"
         #include <string>
         #include <sstream>
+        #include <cstdint>
 
         class Goat {
             public:
diff --git a/third_party/autocxx/integration-tests/tests/integration_test.rs b/third_party/autocxx/integration-tests/tests/integration_test.rs
index ec2b1e7..0939cae 100644
--- a/third_party/autocxx/integration-tests/tests/integration_test.rs
+++ b/third_party/autocxx/integration-tests/tests/integration_test.rs
@@ -4490,6 +4490,7 @@
 fn test_typedef_to_std() {
     let hdr = indoc! {"
         #include <string>
+        #include <cstdint>
         typedef std::string my_string;
         inline uint32_t take_str(my_string a) {
             return a.size();
@@ -4523,6 +4524,7 @@
 fn test_typedef_in_pod_struct() {
     let hdr = indoc! {"
         #include <string>
+        #include <cstdint>
         typedef uint32_t my_int;
         struct A {
             my_int a;
@@ -4544,6 +4546,7 @@
 fn test_cint_in_pod_struct() {
     let hdr = indoc! {"
         #include <string>
+        #include <cstdint>
         struct A {
             int a;
         };
@@ -4613,6 +4616,7 @@
 fn test_typedef_to_std_in_struct() {
     let hdr = indoc! {"
         #include <string>
+        #include <cstdint>
         typedef std::string my_string;
         struct A {
             my_string a;
@@ -4998,6 +5002,43 @@
 }
 
 #[test]
+fn test_take_struct_built_array_in_function() {
+    let hdr = indoc! {"
+    #include <cstdint>
+    struct data {
+        char a[4];
+    };
+    uint32_t take_array(char a[4]) {
+        return a[0] + a[2];
+    }
+    "};
+    let rs = quote! {
+        let mut c = ffi::data { a: [ 10, 20, 30, 40 ] };
+        unsafe {
+            assert_eq!(ffi::take_array(c.a.as_mut_ptr()), 40);
+        }
+    };
+    run_test("", hdr, rs, &["take_array"], &["data"]);
+}
+
+#[test]
+fn test_take_array_in_function() {
+    let hdr = indoc! {"
+    #include <cstdint>
+    uint32_t take_array(char a[4]) {
+        return a[0] + a[2];
+    }
+    "};
+    let rs = quote! {
+        let mut a: [i8; 4] = [ 10, 20, 30, 40 ];
+        unsafe {
+            assert_eq!(ffi::take_array(a.as_mut_ptr()), 40);
+        }
+    };
+    run_test("", hdr, rs, &["take_array"], &[]);
+}
+
+#[test]
 fn test_union_ignored() {
     let hdr = indoc! {"
     #include <cstdint>
@@ -7772,6 +7813,58 @@
 }
 
 #[test]
+fn test_pv_subclass_opaque_param() {
+    let hdr = indoc! {"
+    #include <cstdint>
+
+    typedef uint32_t MyUnsupportedType[4];
+
+    struct MySupportedType {
+        uint32_t a;
+    };
+
+    class MySuperType {
+    public:
+        virtual void foo(const MyUnsupportedType* foo, const MySupportedType* bar) const = 0;
+        virtual ~MySuperType() = default;
+    };
+    "};
+    run_test_ex(
+        "",
+        hdr,
+        quote! {
+            MySubType::new_rust_owned(MySubType { a: 3, cpp_peer: Default::default() });
+        },
+        quote! {
+            subclass!("MySuperType",MySubType)
+            extern_cpp_opaque_type!("MyUnsupportedType", crate::ffi2::MyUnsupportedType)
+        },
+        None,
+        None,
+        Some(quote! {
+
+            #[cxx::bridge]
+            pub mod ffi2 {
+                unsafe extern "C++" {
+                    include!("input.h");
+                    type MyUnsupportedType;
+                }
+            }
+            use autocxx::subclass::CppSubclass;
+            use ffi::MySuperType_methods;
+            #[autocxx::subclass::subclass]
+            pub struct MySubType {
+                a: u32
+            }
+            impl MySuperType_methods for MySubType {
+                unsafe fn foo(&self, _foo: *const ffi2::MyUnsupportedType, _bar: *const ffi::MySupportedType) {
+                }
+            }
+        }),
+    );
+}
+
+#[test]
 fn test_pv_subclass_return() {
     let hdr = indoc! {"
     #include <cstdint>
@@ -12241,6 +12334,58 @@
     run_test("", hdr, rs, &["A"], &[]);
 }
 
+#[test]
+fn test_badly_named_alloc() {
+    let hdr = indoc! {"
+        #include <stdarg.h>
+        class A {
+        public:
+            void alloc();
+        };
+    "};
+    let rs = quote! {};
+    run_test("", hdr, rs, &["A"], &[]);
+}
+
+#[test]
+fn test_cpp_union_pod() {
+    let hdr = indoc! {"
+        typedef unsigned long long UInt64_t;
+        struct ManagedPtr_t_;
+        typedef struct ManagedPtr_t_ ManagedPtr_t;
+        
+        typedef int (*ManagedPtr_ManagerFunction_t)(
+                ManagedPtr_t *managedPtr,
+                const ManagedPtr_t *srcPtr,
+                int operation);
+        
+        typedef union {
+            int intValue;
+            void *ptr;
+        } ManagedPtr_t_data_;
+        
+        struct ManagedPtr_t_ {
+            void *pointer;
+            ManagedPtr_t_data_ userData[4];
+            ManagedPtr_ManagerFunction_t manager;
+        };
+        
+        typedef struct CorrelationId_t_ {
+            unsigned int size : 8;
+            unsigned int valueType : 4;
+            unsigned int classId : 16;
+            unsigned int reserved : 4;
+        
+            union {
+                UInt64_t intValue;
+                ManagedPtr_t ptrValue;
+            } value;
+        } CorrelationId_t;
+    "};
+    run_test("", hdr, quote! {}, &["CorrelationId_t_"], &[]);
+    run_test_expect_fail("", hdr, quote! {}, &[], &["CorrelationId_t_"]);
+}
+
 // Yet to test:
 // - Ifdef
 // - Out param pointers
diff --git a/third_party/flatbuffers/build_defs.bzl b/third_party/flatbuffers/build_defs.bzl
index 5f6d71b..da26cec 100644
--- a/third_party/flatbuffers/build_defs.bzl
+++ b/third_party/flatbuffers/build_defs.bzl
@@ -96,7 +96,7 @@
         mnemonic = "Flatc",
         progress_message = "Generating flatbuffer files for %{input}:",
     )
-    return [DefaultInfo(files = depset(outs), runfiles = ctx.runfiles(files = outs)), FlatbufferLibraryInfo(srcs = ctx.files.srcs)]
+    return [DefaultInfo(files = depset(outs)), FlatbufferLibraryInfo(srcs = ctx.files.srcs)]
 
 _flatbuffer_library_compile = rule(
     implementation = _flatbuffer_library_compile_impl,
diff --git a/third_party/flatbuffers/include/flatbuffers/flatbuffer_builder.h b/third_party/flatbuffers/include/flatbuffers/flatbuffer_builder.h
index efa4d89..6f9d7c8 100644
--- a/third_party/flatbuffers/include/flatbuffers/flatbuffer_builder.h
+++ b/third_party/flatbuffers/include/flatbuffers/flatbuffer_builder.h
@@ -787,7 +787,7 @@
   /// where the vector is stored.
   template<typename T>
   Offset<Vector<const T *>> CreateVectorOfStructs(const T *v, size_t len) {
-    StartVector(len * sizeof(T) / AlignOf<T>(), sizeof(T), AlignOf<T>());
+    StartVector(len, sizeof(T), AlignOf<T>());
     if (len > 0) {
       PushBytes(reinterpret_cast<const uint8_t *>(v), sizeof(T) * len);
     }
@@ -1211,7 +1211,7 @@
   // Allocates space for a vector of structures.
   // Must be completed with EndVectorOfStructs().
   template<typename T> T *StartVectorOfStructs(size_t vector_size) {
-    StartVector(vector_size * sizeof(T) / AlignOf<T>(), sizeof(T), AlignOf<T>());
+    StartVector(vector_size, sizeof(T), AlignOf<T>());
     return reinterpret_cast<T *>(buf_.make_space(vector_size * sizeof(T)));
   }
 
diff --git a/third_party/flatbuffers/include/flatbuffers/reflection.h b/third_party/flatbuffers/include/flatbuffers/reflection.h
index 1aa0863..84213c7 100644
--- a/third_party/flatbuffers/include/flatbuffers/reflection.h
+++ b/third_party/flatbuffers/include/flatbuffers/reflection.h
@@ -508,14 +508,19 @@
 // root should point to the root type for this flatbuffer.
 // buf should point to the start of flatbuffer data.
 // length specifies the size of the flatbuffer data.
+// Returns true if the flatbuffer is valid. Returns false if either:
+// * The flatbuffer is incorrectly constructed (e.g., it points to memory
+// locations outside of the current memory buffer).
+// * The flatbuffer is too complex, and the flatbuffer verifier chosen to bail
+// when attempting to traverse the tree of tables.
 bool Verify(const reflection::Schema &schema, const reflection::Object &root,
             const uint8_t *buf, size_t length, uoffset_t max_depth = 64,
-            uoffset_t max_tables = 1000000);
+            uoffset_t max_tables = 3000000);
 
 bool VerifySizePrefixed(const reflection::Schema &schema,
                         const reflection::Object &root, const uint8_t *buf,
                         size_t length, uoffset_t max_depth = 64,
-                        uoffset_t max_tables = 1000000);
+                        uoffset_t max_tables = 3000000);
 
 }  // namespace flatbuffers
 
diff --git a/third_party/flatbuffers/include/flatbuffers/reflection_generated.h b/third_party/flatbuffers/include/flatbuffers/reflection_generated.h
index 6a99e66..b340086 100644
--- a/third_party/flatbuffers/include/flatbuffers/reflection_generated.h
+++ b/third_party/flatbuffers/include/flatbuffers/reflection_generated.h
@@ -523,6 +523,7 @@
   int64_t value = 0;
   flatbuffers::unique_ptr<reflection::TypeT> union_type{};
   std::vector<std::string> documentation{};
+  std::vector<flatbuffers::unique_ptr<reflection::KeyValueT>> attributes{};
   EnumValT() = default;
   EnumValT(const EnumValT &o);
   EnumValT(EnumValT&&) FLATBUFFERS_NOEXCEPT = default;
@@ -603,6 +604,9 @@
   const flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>> *attributes() const {
     return GetPointer<const flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>> *>(VT_ATTRIBUTES);
   }
+  flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>> *mutable_attributes() {
+    return GetPointer<flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>> *>(VT_ATTRIBUTES);
+  }
   void clear_attributes() {
     ClearField(VT_ATTRIBUTES);
   }
@@ -2462,7 +2466,8 @@
       (lhs.name == rhs.name) &&
       (lhs.value == rhs.value) &&
       ((lhs.union_type == rhs.union_type) || (lhs.union_type && rhs.union_type && *lhs.union_type == *rhs.union_type)) &&
-      (lhs.documentation == rhs.documentation);
+      (lhs.documentation == rhs.documentation) &&
+      (lhs.attributes.size() == rhs.attributes.size() && std::equal(lhs.attributes.cbegin(), lhs.attributes.cend(), rhs.attributes.cbegin(), [](flatbuffers::unique_ptr<reflection::KeyValueT> const &a, flatbuffers::unique_ptr<reflection::KeyValueT> const &b) { return (a == b) || (a && b && *a == *b); }));
 }
 
 inline bool operator!=(const EnumValT &lhs, const EnumValT &rhs) {
@@ -2475,6 +2480,8 @@
         value(o.value),
         union_type((o.union_type) ? new reflection::TypeT(*o.union_type) : nullptr),
         documentation(o.documentation) {
+  attributes.reserve(o.attributes.size());
+  for (const auto &attributes_ : o.attributes) { attributes.emplace_back((attributes_) ? new reflection::KeyValueT(*attributes_) : nullptr); }
 }
 
 inline EnumValT &EnumValT::operator=(EnumValT o) FLATBUFFERS_NOEXCEPT {
@@ -2482,6 +2489,7 @@
   std::swap(value, o.value);
   std::swap(union_type, o.union_type);
   std::swap(documentation, o.documentation);
+  std::swap(attributes, o.attributes);
   return *this;
 }
 
@@ -2498,6 +2506,7 @@
   { auto _e = value(); _o->value = _e; }
   { auto _e = union_type(); if (_e) { if(_o->union_type) { _e->UnPackTo(_o->union_type.get(), _resolver); } else { _o->union_type = flatbuffers::unique_ptr<reflection::TypeT>(_e->UnPack(_resolver)); } } else if (_o->union_type) { _o->union_type.reset(); } }
   { auto _e = documentation(); if (_e) { _o->documentation.resize(_e->size()); for (flatbuffers::uoffset_t _i = 0; _i < _e->size(); _i++) { _o->documentation[_i] = _e->Get(_i)->str(); } } else { _o->documentation.resize(0); } }
+  { auto _e = attributes(); if (_e) { _o->attributes.resize(_e->size()); for (flatbuffers::uoffset_t _i = 0; _i < _e->size(); _i++) { if(_o->attributes[_i]) { _e->Get(_i)->UnPackTo(_o->attributes[_i].get(), _resolver); } else { _o->attributes[_i] = flatbuffers::unique_ptr<reflection::KeyValueT>(_e->Get(_i)->UnPack(_resolver)); }; } } else { _o->attributes.resize(0); } }
 }
 
 inline flatbuffers::Offset<EnumVal> EnumVal::Pack(flatbuffers::FlatBufferBuilder &_fbb, const EnumValT* _o, const flatbuffers::rehasher_function_t *_rehasher) {
@@ -2512,12 +2521,14 @@
   auto _value = _o->value;
   auto _union_type = _o->union_type ? CreateType(_fbb, _o->union_type.get(), _rehasher) : 0;
   auto _documentation = _fbb.CreateVectorOfStrings(_o->documentation);
+  auto _attributes = _fbb.CreateVector<flatbuffers::Offset<reflection::KeyValue>> (_o->attributes.size(), [](size_t i, _VectorArgs *__va) { return CreateKeyValue(*__va->__fbb, __va->__o->attributes[i].get(), __va->__rehasher); }, &_va );
   return reflection::CreateEnumVal(
       _fbb,
       _name,
       _value,
       _union_type,
-      _documentation);
+      _documentation,
+      _attributes);
 }
 
 
@@ -3211,21 +3222,24 @@
     { flatbuffers::ET_LONG, 0, -1 },
     { flatbuffers::ET_SEQUENCE, 0, 0 },
     { flatbuffers::ET_SEQUENCE, 0, 1 },
-    { flatbuffers::ET_STRING, 1, -1 }
+    { flatbuffers::ET_STRING, 1, -1 },
+    { flatbuffers::ET_SEQUENCE, 1, 2 }
   };
   static const flatbuffers::TypeFunction type_refs[] = {
     reflection::ObjectTypeTable,
-    reflection::TypeTypeTable
+    reflection::TypeTypeTable,
+    reflection::KeyValueTypeTable
   };
   static const char * const names[] = {
     "name",
     "value",
     "object",
     "union_type",
-    "documentation"
+    "documentation",
+    "attributes"
   };
   static const flatbuffers::TypeTable tt = {
-    flatbuffers::ST_TABLE, 5, type_codes, type_refs, nullptr, nullptr, names
+    flatbuffers::ST_TABLE, 6, type_codes, type_refs, nullptr, nullptr, names
   };
   return &tt;
 }
diff --git a/third_party/flatbuffers/rust/BUILD.bazel b/third_party/flatbuffers/rust/BUILD.bazel
index d2191dc..64a0dd8 100644
--- a/third_party/flatbuffers/rust/BUILD.bazel
+++ b/third_party/flatbuffers/rust/BUILD.bazel
@@ -9,6 +9,7 @@
     version = "2.1.1",
     visibility = ["//visibility:public"],
     deps = [
+        "@crate_index//:arrayvec",
         "@crate_index//:bitflags",
         "@crate_index//:smallvec",
         "@crate_index//:thiserror",
diff --git a/third_party/flatbuffers/rust/flatbuffers/Cargo.toml b/third_party/flatbuffers/rust/flatbuffers/Cargo.toml
index 2cba5b7..bacbcb3 100644
--- a/third_party/flatbuffers/rust/flatbuffers/Cargo.toml
+++ b/third_party/flatbuffers/rust/flatbuffers/Cargo.toml
@@ -19,6 +19,7 @@
 [dependencies]
 bitflags = "1.2.1"
 serde = { version = "1.0", optional = true }
+arrayvec = "0.7.4"
 
 [build-dependencies]
 rustc_version = "0.4.0"
diff --git a/third_party/flatbuffers/rust/flatbuffers/src/builder.rs b/third_party/flatbuffers/rust/flatbuffers/src/builder.rs
index 7d0f408..d5c3e94 100644
--- a/third_party/flatbuffers/rust/flatbuffers/src/builder.rs
+++ b/third_party/flatbuffers/rust/flatbuffers/src/builder.rs
@@ -16,9 +16,13 @@
 
 #[cfg(not(feature = "std"))]
 use alloc::{vec, vec::Vec};
+use arrayvec::ArrayVec;
 use core::cmp::max;
+use core::convert::Infallible;
+use core::fmt::{Debug, Display};
 use core::iter::{DoubleEndedIterator, ExactSizeIterator};
 use core::marker::PhantomData;
+use core::ops::{Add, AddAssign, Deref, DerefMut, Index, IndexMut, Sub, SubAssign};
 use core::ptr::write_bytes;
 
 use crate::endian_scalar::emplace_scalar;
@@ -30,6 +34,90 @@
 use crate::vtable::{field_index_to_field_offset, VTable};
 use crate::vtable_writer::VTableWriter;
 
+/// Trait to implement custom allocation strategies for [`FlatBufferBuilder`].
+///
+/// An implementation can be used with [`FlatBufferBuilder::new_in`], enabling a custom allocation
+/// strategy for the [`FlatBufferBuilder`].
+///
+/// # Safety
+///
+/// The implementation of the allocator must match the defined behavior as described by the
+/// comments.
+pub unsafe trait Allocator: DerefMut<Target = [u8]> {
+    /// A type describing allocation failures
+    type Error: Display + Debug;
+    /// Grows the buffer, with the old contents being moved to the end.
+    ///
+    /// NOTE: While not unsound, an implementation that doesn't grow the
+    /// internal buffer will get stuck in an infinite loop.
+    fn grow_downwards(&mut self) -> Result<(), Self::Error>;
+
+    /// Returns the size of the internal buffer in bytes.
+    fn len(&self) -> usize;
+}
+
+/// Default [`FlatBufferBuilder`] allocator backed by a [`Vec<u8>`].
+#[derive(Default)]
+pub struct DefaultAllocator(Vec<u8>);
+
+impl DefaultAllocator {
+    /// Builds the allocator from an existing buffer.
+    pub fn from_vec(buffer: Vec<u8>) -> Self {
+        Self(buffer)
+    }
+}
+
+impl Deref for DefaultAllocator {
+    type Target = [u8];
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl DerefMut for DefaultAllocator {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
+// SAFETY: The methods are implemented as described by the documentation.
+unsafe impl Allocator for DefaultAllocator {
+    type Error = Infallible;
+    fn grow_downwards(&mut self) -> Result<(), Self::Error> {
+        let old_len = self.0.len();
+        let new_len = max(1, old_len * 2);
+
+        self.0.resize(new_len, 0);
+
+        if new_len == 1 {
+            return Ok(());
+        }
+
+        // calculate the midpoint, and safely copy the old end data to the new
+        // end position:
+        let middle = new_len / 2;
+        {
+            let (left, right) = &mut self.0[..].split_at_mut(middle);
+            right.copy_from_slice(left);
+        }
+        // finally, zero out the old end data.
+        {
+            let ptr = self.0[..middle].as_mut_ptr();
+            // Safety:
+            // ptr is byte aligned and of length middle
+            unsafe {
+                write_bytes(ptr, 0, middle);
+            }
+        }
+        Ok(())
+    }
+
+    fn len(&self) -> usize {
+        self.0.len()
+    }
+}
+
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
 struct FieldLoc {
     off: UOffsetT,
@@ -40,24 +128,25 @@
 /// state. It has an owned `Vec<u8>` that grows as needed (up to the hardcoded
 /// limit of 2GiB, which is set by the FlatBuffers format).
 #[derive(Clone, Debug, Eq, PartialEq)]
-pub struct FlatBufferBuilder<'fbb> {
-    owned_buf: Vec<u8>,
-    head: usize,
+pub struct FlatBufferBuilder<'fbb, A: Allocator = DefaultAllocator> {
+    allocator: A,
+    head: ReverseIndex,
 
-    field_locs: Vec<FieldLoc>,
-    written_vtable_revpos: Vec<UOffsetT>,
+    // TODO(Adam): Make vectors generic.
+    field_locs: ArrayVec<FieldLoc, 100>,
+    written_vtable_revpos: ArrayVec<UOffsetT, 20>,
 
     nested: bool,
     finished: bool,
 
     min_align: usize,
     force_defaults: bool,
-    strings_pool: Vec<WIPOffset<&'fbb str>>,
+    strings_pool: ArrayVec<WIPOffset<&'fbb str>, 20>,
 
     _phantom: PhantomData<&'fbb ()>,
 }
 
-impl<'fbb> FlatBufferBuilder<'fbb> {
+impl<'fbb> FlatBufferBuilder<'fbb, DefaultAllocator> {
     /// Create a FlatBufferBuilder that is ready for writing.
     pub fn new() -> Self {
         Self::with_capacity(0)
@@ -77,30 +166,52 @@
     /// an existing vector.
     pub fn from_vec(buffer: Vec<u8>) -> Self {
         // we need to check the size here because we create the backing buffer
-        // directly, bypassing the typical way of using grow_owned_buf:
+        // directly, bypassing the typical way of using grow_allocator:
         assert!(
             buffer.len() <= FLATBUFFERS_MAX_BUFFER_SIZE,
             "cannot initialize buffer bigger than 2 gigabytes"
         );
-        let head = buffer.len();
+        let allocator = DefaultAllocator::from_vec(buffer);
+        Self::new_in(allocator)
+    }
+
+    /// Destroy the FlatBufferBuilder, returning its internal byte vector
+    /// and the index into it that represents the start of valid data.
+    pub fn collapse(self) -> (Vec<u8>, usize) {
+        let index = self.head.to_forward_index(&self.allocator);
+        (self.allocator.0, index)
+    }
+}
+
+impl<'fbb, A: Allocator> FlatBufferBuilder<'fbb, A> {
+    /// Create a [`FlatBufferBuilder`] that is ready for writing with a custom [`Allocator`].
+    pub fn new_in(allocator: A) -> Self {
+        let head = ReverseIndex::end();
         FlatBufferBuilder {
-            owned_buf: buffer,
+            allocator,
             head,
 
-            field_locs: Vec::new(),
-            written_vtable_revpos: Vec::new(),
+            field_locs: ArrayVec::new(),
+            written_vtable_revpos: ArrayVec::new(),
 
             nested: false,
             finished: false,
 
             min_align: 0,
             force_defaults: false,
-            strings_pool: Vec::new(),
+            strings_pool: ArrayVec::new(),
 
             _phantom: PhantomData,
         }
     }
 
+    /// Destroy the [`FlatBufferBuilder`], returning its [`Allocator`] and the index
+    /// into it that represents the start of valid data.
+    pub fn collapse_in(self) -> (A, usize) {
+        let index = self.head.to_forward_index(&self.allocator);
+        (self.allocator, index)
+    }
+
     /// Reset the FlatBufferBuilder internal state. Use this method after a
     /// call to a `finish` function in order to re-use a FlatBufferBuilder.
     ///
@@ -114,17 +225,11 @@
     /// new object.
     pub fn reset(&mut self) {
         // memset only the part of the buffer that could be dirty:
-        {
-            let to_clear = self.owned_buf.len() - self.head;
-            let ptr = self.owned_buf[self.head..].as_mut_ptr();
-            // Safety:
-            // Verified ptr is valid for `to_clear` above
-            unsafe {
-                write_bytes(ptr, 0, to_clear);
-            }
-        }
+        self.allocator[self.head.range_to_end()]
+            .iter_mut()
+            .for_each(|x| *x = 0);
 
-        self.head = self.owned_buf.len();
+        self.head = ReverseIndex::end();
         self.written_vtable_revpos.clear();
 
         self.nested = false;
@@ -134,12 +239,6 @@
         self.strings_pool.clear();
     }
 
-    /// Destroy the FlatBufferBuilder, returning its internal byte vector
-    /// and the index into it that represents the start of valid data.
-    pub fn collapse(self) -> (Vec<u8>, usize) {
-        (self.owned_buf, self.head)
-    }
-
     /// Push a Push'able value onto the front of the in-progress data.
     ///
     /// This function uses traits to provide a unified API for writing
@@ -150,7 +249,7 @@
         self.align(sz, P::alignment());
         self.make_space(sz);
         {
-            let (dst, rest) = self.owned_buf[self.head..].split_at_mut(sz);
+            let (dst, rest) = self.allocator[self.head.range_to_end()].split_at_mut(sz);
             // Safety:
             // Called make_space above
             unsafe { x.push(dst, rest.len()) };
@@ -254,9 +353,9 @@
             "create_shared_string can not be called when a table or vector is under construction",
         );
 
-        // Saves a ref to owned_buf since rust doesnt like us refrencing it
+        // Saves a ref to allocator since rust doesnt like us refrencing it
         // in the binary_search_by code.
-        let buf = &self.owned_buf;
+        let buf = &self.allocator;
 
         let found = self.strings_pool.binary_search_by(|offset| {
             let ptr = offset.value() as usize;
@@ -324,9 +423,9 @@
         self.ensure_capacity(slice_size + UOffsetT::size());
 
         self.head -= slice_size;
-        let mut written_len = self.owned_buf.len() - self.head;
+        let mut written_len = self.head.distance_to_end();
 
-        let buf = &mut self.owned_buf[self.head..self.head + slice_size];
+        let buf = &mut self.allocator[self.head.range_to(self.head + slice_size)];
         for (item, out) in items.iter().zip(buf.chunks_exact_mut(elem_size)) {
             written_len -= elem_size;
 
@@ -373,7 +472,7 @@
     /// whether it has been finished.
     #[inline]
     pub fn unfinished_data(&self) -> &[u8] {
-        &self.owned_buf[self.head..]
+        &self.allocator[self.head.range_to_end()]
     }
     /// Get the byte slice for the data that has been written after a call to
     /// one of the `finish` functions.
@@ -382,7 +481,7 @@
     #[inline]
     pub fn finished_data(&self) -> &[u8] {
         self.assert_finished("finished_bytes cannot be called when the buffer is not yet finished");
-        &self.owned_buf[self.head..]
+        &self.allocator[self.head.range_to_end()]
     }
     /// Returns a mutable view of a finished buffer and location of where the flatbuffer starts.
     /// Note that modifying the flatbuffer data may corrupt it.
@@ -390,7 +489,8 @@
     /// Panics if the flatbuffer is not finished.
     #[inline]
     pub fn mut_finished_buffer(&mut self) -> (&mut [u8], usize) {
-        (&mut self.owned_buf, self.head)
+        let index = self.head.to_forward_index(&self.allocator);
+        (&mut self.allocator[..], index)
     }
     /// Assert that a field is present in the just-finished Table.
     ///
@@ -405,13 +505,13 @@
         let idx = self.used_space() - tab_revloc.value() as usize;
 
         // Safety:
-        // The value of TableFinishedWIPOffset is the offset from the end of owned_buf
+        // The value of TableFinishedWIPOffset is the offset from the end of the allocator
         // to an SOffsetT pointing to a valid VTable
         //
-        // `self.owned_buf.len() = self.used_space() + self.head`
-        // `self.owned_buf.len() - tab_revloc = self.used_space() - tab_revloc + self.head`
-        // `self.owned_buf.len() - tab_revloc = idx + self.head`
-        let tab = unsafe { Table::new(&self.owned_buf[self.head..], idx) };
+        // `self.allocator.len() = self.used_space() + self.head`
+        // `self.allocator.len() - tab_revloc = self.used_space() - tab_revloc + self.head`
+        // `self.allocator.len() - tab_revloc = idx + self.head`
+        let tab = unsafe { Table::new(&self.allocator[self.head.range_to_end()], idx) };
         let o = tab.vtable().get(slot_byte_loc) as usize;
         assert!(o != 0, "missing required field {}", assert_msg_name);
     }
@@ -444,7 +544,7 @@
 
     #[inline]
     fn used_space(&self) -> usize {
-        self.owned_buf.len() - self.head as usize
+        self.head.distance_to_end()
     }
 
     #[inline]
@@ -517,7 +617,8 @@
         let vt_end_pos = self.head + vtable_byte_len;
         {
             // write the vtable header:
-            let vtfw = &mut VTableWriter::init(&mut self.owned_buf[vt_start_pos..vt_end_pos]);
+            let vtfw =
+                &mut VTableWriter::init(&mut self.allocator[vt_start_pos.range_to(vt_end_pos)]);
             vtfw.write_vtable_byte_length(vtable_byte_len as VOffsetT);
             vtfw.write_object_inline_size(table_object_size as VOffsetT);
 
@@ -527,20 +628,20 @@
                 vtfw.write_field_offset(fl.id, pos);
             }
         }
-        let new_vt_bytes = &self.owned_buf[vt_start_pos..vt_end_pos];
+        let new_vt_bytes = &self.allocator[vt_start_pos.range_to(vt_end_pos)];
         let found = self
             .written_vtable_revpos
             .binary_search_by(|old_vtable_revpos: &UOffsetT| {
-                let old_vtable_pos = self.owned_buf.len() - *old_vtable_revpos as usize;
+                let old_vtable_pos = self.allocator.len() - *old_vtable_revpos as usize;
                 // Safety:
                 // Already written vtables are valid by construction
-                let old_vtable = unsafe { VTable::init(&self.owned_buf, old_vtable_pos) };
+                let old_vtable = unsafe { VTable::init(&self.allocator, old_vtable_pos) };
                 new_vt_bytes.cmp(old_vtable.as_bytes())
             });
         let final_vtable_revpos = match found {
             Ok(i) => {
                 // The new vtable is a duplicate so clear it.
-                VTableWriter::init(&mut self.owned_buf[vt_start_pos..vt_end_pos]).clear();
+                VTableWriter::init(&mut self.allocator[vt_start_pos.range_to(vt_end_pos)]).clear();
                 self.head += vtable_byte_len;
                 self.written_vtable_revpos[i]
             }
@@ -552,17 +653,17 @@
             }
         };
         // Write signed offset from table to its vtable.
-        let table_pos = self.owned_buf.len() - object_revloc_to_vtable.value() as usize;
+        let table_pos = self.allocator.len() - object_revloc_to_vtable.value() as usize;
         if cfg!(debug_assertions) {
             // Safety:
             // Verified slice length
             let tmp_soffset_to_vt = unsafe {
-                read_scalar::<UOffsetT>(&self.owned_buf[table_pos..table_pos + SIZE_UOFFSET])
+                read_scalar::<UOffsetT>(&self.allocator[table_pos..table_pos + SIZE_UOFFSET])
             };
             assert_eq!(tmp_soffset_to_vt, 0xF0F0_F0F0);
         }
 
-        let buf = &mut self.owned_buf[table_pos..table_pos + SIZE_SOFFSET];
+        let buf = &mut self.allocator[table_pos..table_pos + SIZE_SOFFSET];
         // Safety:
         // Verified length of buf above
         unsafe {
@@ -579,39 +680,14 @@
 
     // Only call this when you know it is safe to double the size of the buffer.
     #[inline]
-    fn grow_owned_buf(&mut self) {
-        let old_len = self.owned_buf.len();
-        let new_len = max(1, old_len * 2);
-
+    fn grow_allocator(&mut self) {
         let starting_active_size = self.used_space();
-
-        let diff = new_len - old_len;
-        self.owned_buf.resize(new_len, 0);
-        self.head += diff;
+        self.allocator
+            .grow_downwards()
+            .expect("Flatbuffer allocation failure");
 
         let ending_active_size = self.used_space();
         debug_assert_eq!(starting_active_size, ending_active_size);
-
-        if new_len == 1 {
-            return;
-        }
-
-        // calculate the midpoint, and safely copy the old end data to the new
-        // end position:
-        let middle = new_len / 2;
-        {
-            let (left, right) = &mut self.owned_buf[..].split_at_mut(middle);
-            right.copy_from_slice(left);
-        }
-        // finally, zero out the old end data.
-        {
-            let ptr = self.owned_buf[..middle].as_mut_ptr();
-            // Safety:
-            // ptr is byte aligned and of length middle
-            unsafe {
-                write_bytes(ptr, 0, middle);
-            }
-        }
     }
 
     // with or without a size prefix changes how we load the data, so finish*
@@ -676,13 +752,13 @@
     #[inline]
     fn push_bytes_unprefixed(&mut self, x: &[u8]) -> UOffsetT {
         let n = self.make_space(x.len());
-        self.owned_buf[n..n + x.len()].copy_from_slice(x);
+        self.allocator[n.range_to(n + x.len())].copy_from_slice(x);
 
-        n as UOffsetT
+        n.to_forward_index(&self.allocator) as UOffsetT
     }
 
     #[inline]
-    fn make_space(&mut self, want: usize) -> usize {
+    fn make_space(&mut self, want: usize) -> ReverseIndex {
         self.ensure_capacity(want);
         self.head -= want;
         self.head
@@ -699,13 +775,13 @@
         );
 
         while self.unused_ready_space() < want {
-            self.grow_owned_buf();
+            self.grow_allocator();
         }
         want
     }
     #[inline]
     fn unused_ready_space(&self) -> usize {
-        self.head
+        self.allocator.len() - self.head.distance_to_end()
     }
     #[inline]
     fn assert_nested(&self, fn_name: &'static str) {
@@ -754,3 +830,127 @@
         Self::with_capacity(0)
     }
 }
+
+/// An index that indexes from the reverse of a slice.
+///
+/// Note that while the internal representation is an index
+/// from the end of a buffer, operations like `Add` and `Sub`
+/// behave like a regular index:
+///
+/// # Examples
+///
+/// ```ignore
+/// let buf = [0, 1, 2, 3, 4, 5];
+/// let idx = ReverseIndex::end() - 2;
+/// assert_eq!(&buf[idx.range_to_end()], &[4, 5]);
+/// assert_eq!(idx.to_forward_index(&buf), 4);
+/// ```
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+struct ReverseIndex(usize);
+
+impl ReverseIndex {
+    /// Returns an index set to the end.
+    ///
+    /// Note: Indexing this will result in an out of bounds error.
+    pub fn end() -> Self {
+        Self(0)
+    }
+
+    /// Returns a struct equivalent to the range `self..`
+    pub fn range_to_end(self) -> ReverseIndexRange {
+        ReverseIndexRange(self, ReverseIndex::end())
+    }
+
+    /// Returns a struct equivalent to the range `self..end`
+    pub fn range_to(self, end: ReverseIndex) -> ReverseIndexRange {
+        ReverseIndexRange(self, end)
+    }
+
+    /// Transforms this reverse index into a regular index for the given buffer.
+    pub fn to_forward_index<T>(self, buf: &[T]) -> usize {
+        buf.len() - self.0
+    }
+
+    /// Returns the number of elements until the end of the range.
+    pub fn distance_to_end(&self) -> usize {
+        self.0
+    }
+}
+
+impl Sub<usize> for ReverseIndex {
+    type Output = Self;
+
+    fn sub(self, rhs: usize) -> Self::Output {
+        Self(self.0 + rhs)
+    }
+}
+
+impl SubAssign<usize> for ReverseIndex {
+    fn sub_assign(&mut self, rhs: usize) {
+        *self = *self - rhs;
+    }
+}
+
+impl Add<usize> for ReverseIndex {
+    type Output = Self;
+
+    fn add(self, rhs: usize) -> Self::Output {
+        Self(self.0 - rhs)
+    }
+}
+
+impl AddAssign<usize> for ReverseIndex {
+    fn add_assign(&mut self, rhs: usize) {
+        *self = *self + rhs;
+    }
+}
+impl<T> Index<ReverseIndex> for [T] {
+    type Output = T;
+
+    fn index(&self, index: ReverseIndex) -> &Self::Output {
+        let index = index.to_forward_index(self);
+        &self[index]
+    }
+}
+
+impl<T> IndexMut<ReverseIndex> for [T] {
+    fn index_mut(&mut self, index: ReverseIndex) -> &mut Self::Output {
+        let index = index.to_forward_index(self);
+        &mut self[index]
+    }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+struct ReverseIndexRange(ReverseIndex, ReverseIndex);
+
+impl<T> Index<ReverseIndexRange> for [T] {
+    type Output = [T];
+
+    fn index(&self, index: ReverseIndexRange) -> &Self::Output {
+        let start = index.0.to_forward_index(self);
+        let end = index.1.to_forward_index(self);
+        &self[start..end]
+    }
+}
+
+impl<T> IndexMut<ReverseIndexRange> for [T] {
+    fn index_mut(&mut self, index: ReverseIndexRange) -> &mut Self::Output {
+        let start = index.0.to_forward_index(self);
+        let end = index.1.to_forward_index(self);
+        &mut self[start..end]
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn reverse_index_test() {
+        let buf = [0, 1, 2, 3, 4, 5];
+        let idx = ReverseIndex::end() - 2;
+        assert_eq!(&buf[idx.range_to_end()], &[4, 5]);
+        assert_eq!(&buf[idx.range_to(idx + 1)], &[4]);
+        assert_eq!(idx.to_forward_index(&buf), 4);
+    }
+}
diff --git a/third_party/flatbuffers/rust/flatbuffers/src/lib.rs b/third_party/flatbuffers/rust/flatbuffers/src/lib.rs
index 2741811..324dc1a 100644
--- a/third_party/flatbuffers/rust/flatbuffers/src/lib.rs
+++ b/third_party/flatbuffers/rust/flatbuffers/src/lib.rs
@@ -48,7 +48,7 @@
 mod vtable_writer;
 
 pub use crate::array::{array_init, emplace_scalar_array, Array};
-pub use crate::builder::FlatBufferBuilder;
+pub use crate::builder::{Allocator, DefaultAllocator, FlatBufferBuilder};
 pub use crate::endian_scalar::{emplace_scalar, read_scalar, read_scalar_at, EndianScalar};
 pub use crate::follow::{Follow, FollowStart, FollowWith};
 pub use crate::primitives::*;
diff --git a/third_party/flatbuffers/src/idl_gen_rust.cpp b/third_party/flatbuffers/src/idl_gen_rust.cpp
index 43237b2..824f33e 100644
--- a/third_party/flatbuffers/src/idl_gen_rust.cpp
+++ b/third_party/flatbuffers/src/idl_gen_rust.cpp
@@ -975,7 +975,8 @@
     code_ += "  }";
     // Pack flatbuffers union value
     code_ +=
-        "  pub fn pack(&self, fbb: &mut flatbuffers::FlatBufferBuilder)"
+        "  pub fn pack<'b, A: flatbuffers::Allocator + 'b>(&self, fbb: &mut "
+        "flatbuffers::FlatBufferBuilder<'b, A>)"
         " -> Option<flatbuffers::WIPOffset<flatbuffers::UnionWIPOffset>>"
         " {";
     code_ += "    match self {";
@@ -1704,8 +1705,11 @@
     code_.SetValue("MAYBE_LT",
                    TableBuilderArgsNeedsLifetime(struct_def) ? "<'args>" : "");
     code_ += "  #[allow(unused_mut)]";
-    code_ += "  pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr>(";
-    code_ += "    _fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr>,";
+    code_ +=
+        "  pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr, A: "
+        "flatbuffers::Allocator + 'bldr>(";
+    code_ +=
+        "    _fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr, A>,";
     code_ += "    {{MAYBE_US}}args: &'args {{STRUCT_TY}}Args{{MAYBE_LT}}";
     code_ += "  ) -> flatbuffers::WIPOffset<{{STRUCT_TY}}<'bldr>> {";
 
@@ -2097,15 +2101,20 @@
     }
 
     // Generate a builder struct:
-    code_ += "{{ACCESS_TYPE}} struct {{STRUCT_TY}}Builder<'a: 'b, 'b> {";
-    code_ += "  fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a>,";
+    code_ +=
+        "{{ACCESS_TYPE}} struct {{STRUCT_TY}}Builder<'a: 'b, 'b, A: "
+        "flatbuffers::Allocator + 'a> {";
+    code_ += "  fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a, A>,";
     code_ +=
         "  start_: flatbuffers::WIPOffset<"
         "flatbuffers::TableUnfinishedWIPOffset>,";
     code_ += "}";
 
     // Generate builder functions:
-    code_ += "impl<'a: 'b, 'b> {{STRUCT_TY}}Builder<'a, 'b> {";
+    code_ +=
+        "impl<'a: 'b, 'b, A: flatbuffers::Allocator + 'a> "
+        "{{STRUCT_TY}}Builder<'a, "
+        "'b, A> {";
     ForAllTableFields(struct_def, [&](const FieldDef &field) {
       const bool is_scalar = IsScalar(field.value.type.base_type);
       std::string offset = namer_.LegacyRustFieldOffsetName(field);
@@ -2140,8 +2149,8 @@
     // Struct initializer (all fields required);
     code_ += "  #[inline]";
     code_ +=
-        "  pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>) -> "
-        "{{STRUCT_TY}}Builder<'a, 'b> {";
+        "  pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a, A>) -> "
+        "{{STRUCT_TY}}Builder<'a, 'b, A> {";
     code_.SetValue("NUM_FIELDS", NumToString(struct_def.fields.vec.size()));
     code_ += "    let start = _fbb.start_table();";
     code_ += "    {{STRUCT_TY}}Builder {";
@@ -2244,9 +2253,9 @@
 
     // Generate pack function.
     code_ += "impl {{STRUCT_OTY}} {";
-    code_ += "  pub fn pack<'b>(";
+    code_ += "  pub fn pack<'b, A: flatbuffers::Allocator + 'b>(";
     code_ += "    &self,";
-    code_ += "    _fbb: &mut flatbuffers::FlatBufferBuilder<'b>";
+    code_ += "    _fbb: &mut flatbuffers::FlatBufferBuilder<'b, A>";
     code_ += "  ) -> flatbuffers::WIPOffset<{{STRUCT_TY}}<'b>> {";
     // First we generate variables for each field and then later assemble them
     // using "StructArgs" to more easily manage ownership of the builder.
@@ -2529,8 +2538,10 @@
 
     // Finish a buffer with a given root object:
     code_ += "#[inline]";
-    code_ += "pub fn finish_{{STRUCT_FN}}_buffer<'a, 'b>(";
-    code_ += "    fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>,";
+    code_ +=
+        "pub fn finish_{{STRUCT_FN}}_buffer<'a, 'b, A: "
+        "flatbuffers::Allocator + 'a>(";
+    code_ += "    fbb: &'b mut flatbuffers::FlatBufferBuilder<'a, A>,";
     code_ += "    root: flatbuffers::WIPOffset<{{STRUCT_TY}}<'a>>) {";
     if (parser_.file_identifier_.length()) {
       code_ += "  fbb.finish(root, Some({{STRUCT_CONST}}_IDENTIFIER));";
@@ -2542,8 +2553,8 @@
     code_ += "#[inline]";
     code_ +=
         "pub fn finish_size_prefixed_{{STRUCT_FN}}_buffer"
-        "<'a, 'b>("
-        "fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>, "
+        "<'a, 'b, A: flatbuffers::Allocator + 'a>("
+        "fbb: &'b mut flatbuffers::FlatBufferBuilder<'a, A>, "
         "root: flatbuffers::WIPOffset<{{STRUCT_TY}}<'a>>) {";
     if (parser_.file_identifier_.length()) {
       code_ +=
diff --git a/third_party/flatbuffers/typescript.bzl b/third_party/flatbuffers/typescript.bzl
index f00c550..ccfd2d6 100644
--- a/third_party/flatbuffers/typescript.bzl
+++ b/third_party/flatbuffers/typescript.bzl
@@ -93,7 +93,7 @@
         compatible_with = compatible_with,
         restricted_to = restricted_to,
         target_compatible_with = target_compatible_with,
-        supports_workers = False,
+        supports_workers = 0,
         tsconfig = {
             "compilerOptions": {
                 "declaration": True,
diff --git a/third_party/rules_rollup/0001-Fix-resolving-files.patch b/third_party/rules_rollup/0001-Fix-resolving-files.patch
index a81a656..a57c4bc 100644
--- a/third_party/rules_rollup/0001-Fix-resolving-files.patch
+++ b/third_party/rules_rollup/0001-Fix-resolving-files.patch
@@ -1,19 +1,19 @@
-From fd6dd080ea58fd71c70ce2303873feab1abda760 Mon Sep 17 00:00:00 2001
+From 5c0b6653d0fa500b5ad3a65f8e58c97517f2d6bd Mon Sep 17 00:00:00 2001
 From: Philipp Schrader <philipp.schrader@gmail.com>
-Date: Sun, 19 Feb 2023 14:18:11 -0800
+Date: Wed, 21 Feb 2024 17:26:27 -0800
 Subject: [PATCH] Fix resolving files
 
 I don't really know what the underlying problem is, but returning a
 File instead of a path is causing us grief.
 ---
- rollup/private/rollup_bundle.bzl | 2 +-
+ rollup/private/rollup.bzl | 2 +-
  1 file changed, 1 insertion(+), 1 deletion(-)
 
-diff --git a/rollup/private/rollup_bundle.bzl b/rollup/private/rollup_bundle.bzl
-index 32aaad4..a2061dd 100644
---- a/rollup/private/rollup_bundle.bzl
-+++ b/rollup/private/rollup_bundle.bzl
-@@ -186,7 +186,7 @@ def _resolve_js_input(f, inputs):
+diff --git a/rollup/private/rollup.bzl b/rollup/private/rollup.bzl
+index c1634cf..8cd5407 100644
+--- a/rollup/private/rollup.bzl
++++ b/rollup/private/rollup.bzl
+@@ -96,7 +96,7 @@ def _resolve_js_input(f, inputs):
      for i in inputs:
          if i.extension == "js" or i.extension == "mjs":
              if _no_ext(i) == no_ext:
diff --git a/tools/build_rules/autocxx.bzl b/tools/build_rules/autocxx.bzl
index 6cb74de..5cadf2b 100644
--- a/tools/build_rules/autocxx.bzl
+++ b/tools/build_rules/autocxx.bzl
@@ -1,4 +1,4 @@
-load("@rules_rust//rust:defs.bzl", "rust_library")
+load("@org_frc971//tools/rust:defs.bzl", "rust_library")
 load("@rules_cc//cc:find_cc_toolchain.bzl", "find_cc_toolchain")
 load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "ACTION_NAMES")
 
@@ -241,7 +241,7 @@
 def autocxx_library(
         name,
         visibility = None,
-        target_compatible_with = None,
+        target_compatible_with = ["//tools/platforms/rust:has_support"],
         libs = [],
         srcs = [],
         cxxbridge_srcs = [],
@@ -251,7 +251,8 @@
         testonly = None,
         crate_features = None,
         crate_name = None,
-        gen_debug = None):
+        gen_debug = None,
+        **kwargs):
     """A macro to generate Rust <-> C++ interop code with autocxx.
 
     Creates the following rules:
@@ -343,4 +344,5 @@
         ],
         compile_data = [gen_compile_data_name],
         rustc_env_files = [gen_env_files_name],
+        **kwargs
     )
diff --git a/tools/build_rules/js.bzl b/tools/build_rules/js.bzl
index 4b11ea7..51fe987 100644
--- a/tools/build_rules/js.bzl
+++ b/tools/build_rules/js.bzl
@@ -8,8 +8,8 @@
 load("@npm//:html-insert-assets/package_json.bzl", html_insert_assets_bin = "bin")
 load("//tools/build_rules/js:ng.bzl", "ng_esbuild", "ng_project")
 load("//tools/build_rules/js:ts.bzl", _ts_project = "ts_project")
-load("@aspect_rules_rollup//rollup:defs.bzl", upstream_rollup_bundle = "rollup_bundle")
-load("@aspect_rules_terser//terser:defs.bzl", "terser_minified")
+load("@aspect_rules_rollup//rollup:defs.bzl", upstream_rollup_bundle = "rollup")
+load("@aspect_rules_terser//terser:defs.bzl", terser_minified = "terser")
 load("@aspect_rules_cypress//cypress:defs.bzl", "cypress_module_test")
 
 ts_project = _ts_project
@@ -283,7 +283,7 @@
         **kwargs
     )
 
-def rollup_bundle(name, entry_point, deps = [], visibility = None, **kwargs):
+def rollup_bundle(name, entry_point, node_modules = "//:node_modules", deps = [], visibility = None, **kwargs):
     """Calls the upstream rollup_bundle() and exposes a .min.js file.
 
     Legacy version of rollup_bundle() used to provide the .min.js file. This
@@ -301,6 +301,7 @@
         deps = deps + [
             "//:node_modules/@rollup/plugin-node-resolve",
         ],
+        node_modules = node_modules,
         sourcemap = "false",
         config_file = ":%s__rollup_config.js" % name,
         entry_point = entry_point,
@@ -310,6 +311,7 @@
     terser_minified(
         name = name + "__min",
         srcs = [name + ".js"],
+        node_modules = node_modules,
         tags = [
             "no-remote-cache",
         ],
@@ -354,7 +356,7 @@
     },
 )
 
-def cypress_test(runner, data = None, **kwargs):
+def cypress_test(name, runner, data = None, **kwargs):
     """Runs a cypress test with the specified runner.
 
     Args:
@@ -373,16 +375,24 @@
     # Chrome is located at the runfiles root. So we need to go up one more
     # directory than the workspace root.
     chrome_location = "../" * (package_depth + 1) + "chrome_linux/chrome"
-    config_location = "../" * package_depth + "tools/build_rules/js/cypress.config.js"
+
+    copy_file(
+        name = name + "_config",
+        out = "cypress.config.js",
+        src = "//tools/build_rules/js:cypress.config.js",
+        visibility = ["//visibility:private"],
+    )
 
     data = data or []
-    data.append("//tools/build_rules/js:cypress.config.js")
+    data.append(":%s_config" % name)
     data.append("@xvfb_amd64//:wrapped_bin/Xvfb")
+    data.append("//:node_modules")
 
     cypress_module_test(
+        name = name,
         args = [
             "run",
-            "--config-file=" + config_location,
+            "--config-file=cypress.config.js",
             "--browser=" + chrome_location,
         ],
         browsers = ["@chrome_linux//:all"],
diff --git a/tools/build_rules/js/cypress.config.js b/tools/build_rules/js/cypress.config.js
index c8d6988..e991016 100644
--- a/tools/build_rules/js/cypress.config.js
+++ b/tools/build_rules/js/cypress.config.js
@@ -1,6 +1,6 @@
-const {defineConfig} = require('cypress');
+import {defineConfig} from 'cypress';
 
-module.exports = defineConfig({
+export default defineConfig({
   e2e: {
     specPattern: ['*.cy.js'],
     supportFile: false,
diff --git a/tools/build_rules/js/ts.bzl b/tools/build_rules/js/ts.bzl
index a5b0770..01b8bfe 100644
--- a/tools/build_rules/js/ts.bzl
+++ b/tools/build_rules/js/ts.bzl
@@ -15,6 +15,6 @@
 
         # TODO(phil): Is this a good idea? I don't _actually_ know what this
         # does.
-        supports_workers = False,
+        supports_workers = 0,
         **kwargs
     )
diff --git a/tools/dependency_rewrite b/tools/dependency_rewrite
index 2d2e610..1cd117a 100644
--- a/tools/dependency_rewrite
+++ b/tools/dependency_rewrite
@@ -17,6 +17,7 @@
 rewrite downloads.sourceforge.net/(.*) software.frc971.org/Build-Dependencies/downloads.sourceforge.net/$1
 rewrite cdn.cypress.io/(.*) software.frc971.org/Build-Dependencies/cdn.cypress.io/$1
 rewrite www.googleapis.com/(.*) software.frc971.org/Build-Dependencies/www.googleapis.com/$1
+rewrite www.johnvansickle.com/(.*) software.frc971.org/Build-Dependencies/www.johnvansickle.com/$1
 allow crates.io
 allow golang.org
 allow go.dev
diff --git a/tools/rust/defs.bzl b/tools/rust/defs.bzl
new file mode 100644
index 0000000..88caca5
--- /dev/null
+++ b/tools/rust/defs.bzl
@@ -0,0 +1,106 @@
+load(
+    "@rules_rust//rust:defs.bzl",
+    _rust_binary = "rust_binary",
+    _rust_doc = "rust_doc",
+    _rust_doc_test = "rust_doc_test",
+    _rust_library = "rust_library",
+    _rust_test = "rust_test",
+)
+load("@com_github_google_flatbuffers//:build_defs.bzl", _flatbuffer_rust_library = "flatbuffer_rust_library")
+
+def rust_doc_test(target_compatible_with = ["//tools/platforms/rust:has_support"], tags = [], **kwargs):
+    # TODO(james): Attempting to execute this remotely results
+    # in complaints about overly large files.
+    _rust_doc_test(
+        tags = tags + ["no-remote-exec"],
+        target_compatible_with = target_compatible_with,
+        **kwargs
+    )
+
+def rust_doc(target_compatible_with = ["//tools/platforms/rust:has_support"], rustdoc_flags = ["-Dwarnings"], **kwargs):
+    _rust_doc(
+        target_compatible_with = target_compatible_with,
+        rustdoc_flags = rustdoc_flags,
+        **kwargs
+    )
+
+def rust_binary(target_compatible_with = ["//tools/platforms/rust:has_support"], rustc_flags = [], **kwargs):
+    _rust_binary(
+        target_compatible_with = select({
+            Label("//conditions:default"): target_compatible_with,
+            Label("//tools:has_msan"): ["@platforms//:incompatible"],
+        }),
+        # TODO: Make Rust play happy with pic vs nopic. Details at:
+        # https://github.com/bazelbuild/rules_rust/issues/118
+        rustc_flags = rustc_flags + ["-Crelocation-model=static"],
+        **kwargs
+    )
+
+def rust_test(target_compatible_with = ["//tools/platforms/rust:has_support"], rustc_flags = [], **kwargs):
+    _rust_test(
+        target_compatible_with = select({
+            Label("//conditions:default"): target_compatible_with,
+            Label("//tools:has_msan"): ["@platforms//:incompatible"],
+        }),
+        rustc_flags = rustc_flags + ["-Crelocation-model=static"],
+        **kwargs
+    )
+
+def rust_library(
+        name,
+        target_compatible_with = ["//tools/platforms/rust:has_support"],
+        gen_docs = True,
+        gen_tests = True,
+        gen_doctests = True,
+        **kwargs):
+    test_params = {}
+    doctest_params = {}
+    params = {}
+
+    for (param, value) in kwargs.items():
+        if param.startswith("test_"):
+            test_params[param[5:]] = value
+        elif param.startswith("doctest_"):
+            doctest_params[param[8:]] = value
+        else:
+            params[param] = value
+
+    _rust_library(
+        name = name,
+        target_compatible_with = select({
+            Label("//conditions:default"): target_compatible_with,
+            Label("//tools:has_msan"): ["@platforms//:incompatible"],
+        }),
+        **params
+    )
+
+    if gen_tests:
+        rust_test(
+            name = name + "_test",
+            crate = name,
+            **test_params
+        )
+
+    if gen_docs:
+        rust_doc(
+            name = name + "_doc",
+            crate = name,
+            target_compatible_with = ["//tools/platforms/rust:has_support"],
+            rustdoc_flags = ["--document-private-items", "-Dwarnings"],
+        )
+
+    if gen_doctests:
+        rust_doc_test(
+            name = name + "_doctest",
+            crate = name,
+            **doctest_params
+        )
+
+def flatbuffer_rust_library(target_compatible_with = ["//tools/platforms/rust:has_support"], **kwargs):
+    _flatbuffer_rust_library(
+        target_compatible_with = select({
+            Label("//conditions:default"): target_compatible_with,
+            Label("//tools:has_msan"): ["@platforms//:incompatible"],
+        }),
+        **kwargs
+    )
diff --git a/y2016/control_loops/superstructure/superstructure_lib_test.cc b/y2016/control_loops/superstructure/superstructure_lib_test.cc
index ed2f210..eeea9b5 100644
--- a/y2016/control_loops/superstructure/superstructure_lib_test.cc
+++ b/y2016/control_loops/superstructure/superstructure_lib_test.cc
@@ -208,26 +208,26 @@
     if (superstructure_status_fetcher_->state() == Superstructure::RUNNING ||
         superstructure_status_fetcher_->state() ==
             Superstructure::LANDING_RUNNING) {
-      AOS_CHECK_LE(::std::abs(superstructure_output_fetcher_->voltage_intake()),
-                   Superstructure::kOperatingVoltage);
-      AOS_CHECK_LE(
-          ::std::abs(superstructure_output_fetcher_->voltage_shoulder()),
-          Superstructure::kOperatingVoltage);
-      AOS_CHECK_LE(::std::abs(superstructure_output_fetcher_->voltage_wrist()),
-                   Superstructure::kOperatingVoltage);
+      CHECK_LE(::std::abs(superstructure_output_fetcher_->voltage_intake()),
+               Superstructure::kOperatingVoltage);
+      CHECK_LE(::std::abs(superstructure_output_fetcher_->voltage_shoulder()),
+               Superstructure::kOperatingVoltage);
+      CHECK_LE(::std::abs(superstructure_output_fetcher_->voltage_wrist()),
+               Superstructure::kOperatingVoltage);
     } else {
-      AOS_CHECK_LE(::std::abs(superstructure_output_fetcher_->voltage_intake()),
-                   Superstructure::kZeroingVoltage);
-      AOS_CHECK_LE(
-          ::std::abs(superstructure_output_fetcher_->voltage_shoulder()),
-          Superstructure::kZeroingVoltage);
-      AOS_CHECK_LE(::std::abs(superstructure_output_fetcher_->voltage_wrist()),
-                   Superstructure::kZeroingVoltage);
+      CHECK_LE(::std::abs(superstructure_output_fetcher_->voltage_intake()),
+               Superstructure::kZeroingVoltage);
+      CHECK_LE(::std::abs(superstructure_output_fetcher_->voltage_shoulder()),
+               Superstructure::kZeroingVoltage);
+      CHECK_LE(::std::abs(superstructure_output_fetcher_->voltage_wrist()),
+               Superstructure::kZeroingVoltage);
     }
     if (arm_plant_->X(0, 0) <=
-        Superstructure::kShoulderTransitionToLanded + 1e-4) {
-      AOS_CHECK_GE(superstructure_output_fetcher_->voltage_shoulder(),
-                   Superstructure::kLandingShoulderDownVoltage - 0.00001);
+        Superstructure::kShoulderTransitionToLanded - 1e-4) {
+      CHECK_GE(superstructure_output_fetcher_->voltage_shoulder(),
+               Superstructure::kLandingShoulderDownVoltage - 0.00001)
+          << ": Arm at " << arm_plant_->X(0, 0) << " and transition at "
+          << Superstructure::kShoulderTransitionToLanded;
     }
 
     // Use the plant to generate the next physical state given the voltages to
diff --git a/y2019/control_loops/drivetrain/BUILD b/y2019/control_loops/drivetrain/BUILD
index 65857c6..53cb777 100644
--- a/y2019/control_loops/drivetrain/BUILD
+++ b/y2019/control_loops/drivetrain/BUILD
@@ -133,6 +133,7 @@
         "//frc971/control_loops:pose",
         "//frc971/control_loops/drivetrain:camera",
         "//frc971/control_loops/drivetrain:hybrid_ekf",
+        "//frc971/control_loops/drivetrain/localization:utils",
     ],
 )
 
diff --git a/y2019/control_loops/drivetrain/localizer.h b/y2019/control_loops/drivetrain/localizer.h
index f4c7a8a..3494536 100644
--- a/y2019/control_loops/drivetrain/localizer.h
+++ b/y2019/control_loops/drivetrain/localizer.h
@@ -6,6 +6,7 @@
 
 #include "frc971/control_loops/drivetrain/camera.h"
 #include "frc971/control_loops/drivetrain/hybrid_ekf.h"
+#include "frc971/control_loops/drivetrain/localization/utils.h"
 #include "frc971/control_loops/pose.h"
 
 #if !defined(__clang__) && defined(__GNUC__)
@@ -437,37 +438,8 @@
 
   Eigen::Matrix<Scalar, kNOutputs, kNStates> HMatrix(const Target &target,
                                                      const Pose &camera_pose) {
-    // To calculate dheading/d{x,y,theta}:
-    // heading = arctan2(target_pos - camera_pos) - camera_theta
-    Eigen::Matrix<Scalar, 3, 1> target_pos = target.pose().abs_pos();
-    Eigen::Matrix<Scalar, 3, 1> camera_pos = camera_pose.abs_pos();
-    Scalar diffx = target_pos.x() - camera_pos.x();
-    Scalar diffy = target_pos.y() - camera_pos.y();
-    Scalar norm2 = diffx * diffx + diffy * diffy;
-    Scalar dheadingdx = diffy / norm2;
-    Scalar dheadingdy = -diffx / norm2;
-    Scalar dheadingdtheta = -1.0;
-
-    // To calculate ddistance/d{x,y}:
-    // distance = sqrt(diffx^2 + diffy^2)
-    Scalar distance = ::std::sqrt(norm2);
-    Scalar ddistdx = -diffx / distance;
-    Scalar ddistdy = -diffy / distance;
-
-    // Skew = target.theta - camera.theta - heading
-    //      = target.theta - arctan2(target_pos - camera_pos)
-    Scalar dskewdx = -dheadingdx;
-    Scalar dskewdy = -dheadingdy;
-    Eigen::Matrix<Scalar, kNOutputs, kNStates> H;
-    H.setZero();
-    H(0, 0) = dheadingdx;
-    H(0, 1) = dheadingdy;
-    H(0, 2) = dheadingdtheta;
-    H(1, 0) = ddistdx;
-    H(1, 1) = ddistdy;
-    H(2, 0) = dskewdx;
-    H(2, 1) = dskewdy;
-    return H;
+    return frc971::control_loops::drivetrain::
+        HMatrixForCameraHeadingDistanceSkew(target.pose(), camera_pose);
   }
 
   // A helper function for the fuller version of MatchFrames; this just
diff --git a/y2023/localizer/BUILD b/y2023/localizer/BUILD
index eb0f886..9b35fcf 100644
--- a/y2023/localizer/BUILD
+++ b/y2023/localizer/BUILD
@@ -81,26 +81,15 @@
 )
 
 cc_library(
-    name = "utils",
-    srcs = ["utils.cc"],
-    hdrs = ["utils.h"],
-    visibility = ["//visibility:public"],
-    deps = [
-        "//frc971/vision:target_map_fbs",
-        "@org_tuxfamily_eigen//:eigen",
-    ],
-)
-
-cc_library(
     name = "map_expander_lib",
     srcs = ["map_expander_lib.cc"],
     hdrs = ["map_expander_lib.h"],
     deps = [
         ":relative_scoring_map_fbs",
         ":scoring_map_fbs",
-        ":utils",
         "//aos:flatbuffers",
         "//aos:json_to_flatbuffer",
+        "//frc971/vision:target_map_utils",
     ],
 )
 
@@ -138,7 +127,6 @@
     visibility = ["//visibility:public"],
     deps = [
         ":status_fbs",
-        ":utils",
         ":visualization_fbs",
         "//aos/containers:sized_array",
         "//aos/events:event_loop",
@@ -152,6 +140,7 @@
         "//frc971/control_loops/drivetrain/localization:utils",
         "//frc971/imu_reader:imu_watcher",
         "//frc971/vision:target_map_fbs",
+        "//frc971/vision:target_map_utils",
         "//y2023:constants",
         "//y2023/constants:constants_fbs",
     ],
@@ -164,12 +153,12 @@
     deps = [
         ":localizer",
         ":status_fbs",
-        ":utils",
         "//aos/events:simulated_event_loop",
         "//aos/events/logging:log_writer",
         "//aos/testing:googletest",
         "//frc971/control_loops/drivetrain:drivetrain_test_lib",
         "//frc971/control_loops/drivetrain:localizer_fbs",
+        "//frc971/vision:target_map_utils",
         "//y2023/constants:simulated_constants_sender",
         "//y2023/control_loops/drivetrain:drivetrain_base",
     ],
diff --git a/y2023/localizer/localizer.cc b/y2023/localizer/localizer.cc
index 76784fb..6f9a16b 100644
--- a/y2023/localizer/localizer.cc
+++ b/y2023/localizer/localizer.cc
@@ -5,8 +5,8 @@
 #include "aos/containers/sized_array.h"
 #include "frc971/control_loops/drivetrain/localizer_generated.h"
 #include "frc971/control_loops/pose.h"
+#include "frc971/vision/target_map_utils.h"
 #include "y2023/constants.h"
-#include "y2023/localizer/utils.h"
 
 DEFINE_double(max_pose_error, 1e-6,
               "Throw out target poses with a higher pose error than this");
diff --git a/y2023/localizer/localizer_test.cc b/y2023/localizer/localizer_test.cc
index fb0efa9..c47e46e 100644
--- a/y2023/localizer/localizer_test.cc
+++ b/y2023/localizer/localizer_test.cc
@@ -8,10 +8,10 @@
 #include "frc971/control_loops/drivetrain/localizer_generated.h"
 #include "frc971/control_loops/pose.h"
 #include "frc971/vision/target_map_generated.h"
+#include "frc971/vision/target_map_utils.h"
 #include "y2023/constants/simulated_constants_sender.h"
 #include "y2023/control_loops/drivetrain/drivetrain_base.h"
 #include "y2023/localizer/status_generated.h"
-#include "y2023/localizer/utils.h"
 
 DEFINE_string(output_folder, "",
               "If set, logs all channels to the provided logfile.");
diff --git a/y2023/localizer/map_expander_lib.cc b/y2023/localizer/map_expander_lib.cc
index 5a94985..884b184 100644
--- a/y2023/localizer/map_expander_lib.cc
+++ b/y2023/localizer/map_expander_lib.cc
@@ -1,6 +1,6 @@
 #include "y2023/localizer/map_expander_lib.h"
 
-#include "y2023/localizer/utils.h"
+#include "frc971/vision/target_map_utils.h"
 
 namespace y2023::localizer {
 namespace {
diff --git a/y2023/localizer/utils.h b/y2023/localizer/utils.h
deleted file mode 100644
index 8241cf8..0000000
--- a/y2023/localizer/utils.h
+++ /dev/null
@@ -1,14 +0,0 @@
-#ifndef Y2023_LOCALIZER_UTILS_H_
-#define Y2023_LOCALIZER_UTILS_H_
-
-#include <Eigen/Dense>
-
-#include "frc971/vision/target_map_generated.h"
-
-namespace y2023::localizer {
-// Converts a TargetPoseFbs into a transformation matrix.
-Eigen::Matrix<double, 4, 4> PoseToTransform(
-    const frc971::vision::TargetPoseFbs *pose);
-}  // namespace y2023::localizer
-
-#endif  // Y2023_LOCALIZER_UTILS_H_
diff --git a/y2023/vision/BUILD b/y2023/vision/BUILD
index f583abd..97ce003 100644
--- a/y2023/vision/BUILD
+++ b/y2023/vision/BUILD
@@ -78,9 +78,9 @@
         "//aos/events:shm_event_loop",
         "//frc971/constants:constants_sender_lib",
         "//frc971/control_loops/drivetrain/localization:localizer_output_fbs",
+        "//frc971/vision:target_map_utils",
         "//frc971/vision:vision_fbs",
         "//y2023/localizer",
-        "//y2023/localizer:utils",
         "//y2023/vision:vision_util",
         "@com_google_absl//absl/strings",
     ],
@@ -410,3 +410,32 @@
         "//aos/events:shm_event_loop",
     ],
 )
+
+cc_binary(
+    name = "video_ripper",
+    srcs = [
+        "video_ripper.cc",
+    ],
+    data = [
+        "@ffmpeg",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//aos:init",
+        "//aos/events:simulated_event_loop",
+        "//aos/events/logging:log_reader",
+        "//frc971/vision:vision_fbs",
+        "@com_github_gflags_gflags//:gflags",
+        "@com_github_google_glog//:glog",
+    ],
+)
+
+sh_binary(
+    name = "video_tiler",
+    srcs = ["video_tiler.sh"],
+    data = [
+        "@bazel_tools//tools/bash/runfiles",
+        "@ffmpeg",
+    ],
+)
diff --git a/y2023/vision/localization_verifier.cc b/y2023/vision/localization_verifier.cc
index c16f874..01ef9fe 100644
--- a/y2023/vision/localization_verifier.cc
+++ b/y2023/vision/localization_verifier.cc
@@ -3,9 +3,9 @@
 #include "aos/init.h"
 #include "frc971/constants/constants_sender_lib.h"
 #include "frc971/control_loops/drivetrain/localization/localizer_output_generated.h"
+#include "frc971/vision/target_map_utils.h"
 #include "frc971/vision/vision_generated.h"
 #include "y2023/localizer/localizer.h"
-#include "y2023/localizer/utils.h"
 
 DEFINE_string(config, "aos_config.json", "Path to the config file to use.");
 
@@ -49,7 +49,7 @@
     }
 
     const Localizer::Transform H_camera_target =
-        localizer::PoseToTransform(target_pose);
+        frc971::vision::PoseToTransform(target_pose);
     const Localizer::Transform H_field_robot =
         LocalizerOutputToTransform(*localizer_fetcher->get());
 
diff --git a/y2023/vision/video_ripper.cc b/y2023/vision/video_ripper.cc
new file mode 100644
index 0000000..3c93139
--- /dev/null
+++ b/y2023/vision/video_ripper.cc
@@ -0,0 +1,80 @@
+#include <unistd.h>
+
+#include <cstdlib>
+
+#include "gflags/gflags.h"
+#include "glog/logging.h"
+
+#include "aos/events/logging/log_reader.h"
+#include "aos/events/simulated_event_loop.h"
+#include "aos/init.h"
+#include "aos/scoped/scoped_fd.h"
+#include "frc971/vision/vision_generated.h"
+
+DEFINE_string(channel, "/camera", "Channel name for the image.");
+DEFINE_int32(width, 1280, "Width of the image");
+DEFINE_int32(height, 720, "Height of the image");
+DEFINE_string(ffmpeg_binary, "external/ffmpeg/ffmpeg",
+              "The path to the ffmpeg binary");
+DEFINE_string(output_path, "video_ripper_output.mp4",
+              "The path to output the mp4 video");
+DEFINE_bool(flip, true, "If true, rotate the video 180 deg.");
+
+// Replays a log and dumps the contents of /camera frc971.vision.CameraImage
+// directly to stdout.
+int main(int argc, char *argv[]) {
+  aos::InitGoogle(&argc, &argv);
+  const std::vector<aos::logger::LogFile> logfiles =
+      aos::logger::SortParts(aos::logger::FindLogs(argc, argv));
+  CHECK(!logfiles.empty());
+
+  // Start ffmpeg
+  std::stringstream command;
+  command << FLAGS_ffmpeg_binary;
+  command << " -framerate 30 -f rawvideo -pix_fmt yuyv422";
+  command << " -s " << FLAGS_width << "x" << FLAGS_height;
+  command << " -i pipe:";
+  command << " -c:v libx264 -f mp4";
+  if (FLAGS_flip) {
+    command << " -vf rotate=PI";
+  }
+  command << " \"" << FLAGS_output_path << "\"";
+
+  FILE *stream = popen(command.str().c_str(), "w");
+
+  const std::string replay_node = logfiles.at(0).logger_node;
+  LOG(INFO) << "Replaying as \"" << replay_node << "\"";
+
+  aos::logger::LogReader reader(logfiles, nullptr);
+  aos::SimulatedEventLoopFactory factory(reader.configuration());
+  reader.RegisterWithoutStarting(&factory);
+
+  const aos::Node *node =
+      (replay_node.empty() ||
+       !aos::configuration::MultiNode(reader.configuration()))
+          ? nullptr
+          : aos::configuration::GetNode(reader.configuration(), replay_node);
+
+  std::unique_ptr<aos::EventLoop> event_loop;
+  factory.GetNodeEventLoopFactory(node)->OnStartup([&stream, &event_loop,
+                                                    &reader, node]() {
+    event_loop =
+        reader.event_loop_factory()->MakeEventLoop("video_ripper", node);
+    event_loop->MakeWatcher(
+        FLAGS_channel, [&stream](const frc971::vision::CameraImage &image) {
+          CHECK_EQ(FLAGS_width, image.cols())
+              << "Image width needs to match the images in the logfile";
+          CHECK_EQ(FLAGS_height, image.rows())
+              << "Image width needs to match the images in the logfile";
+
+          const size_t bytes_written =
+              write(fileno(stream), image.data()->data(), image.data()->size());
+          PCHECK(bytes_written == image.data()->size());
+        });
+  });
+
+  reader.event_loop_factory()->Run();
+  reader.Deregister();
+
+  pclose(stream);
+}
diff --git a/y2023/vision/video_tiler.sh b/y2023/vision/video_tiler.sh
new file mode 100755
index 0000000..5ad3e59
--- /dev/null
+++ b/y2023/vision/video_tiler.sh
@@ -0,0 +1,43 @@
+#!/bin/bash
+
+# --- begin runfiles.bash initialization ---
+# Copy-pasted from Bazel's Bash runfiles library (tools/bash/runfiles/runfiles.bash).
+set -euo pipefail
+if [[ ! -d "${RUNFILES_DIR:-/dev/null}" && ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
+  if [[ -f "$0.runfiles_manifest" ]]; then
+    export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest"
+  elif [[ -f "$0.runfiles/MANIFEST" ]]; then
+    export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST"
+  elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
+    export RUNFILES_DIR="$0.runfiles"
+  fi
+fi
+if [[ -f "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
+  source "${RUNFILES_DIR}/bazel_tools/tools/bash/runfiles/runfiles.bash"
+elif [[ -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
+  source "$(grep -m1 "^bazel_tools/tools/bash/runfiles/runfiles.bash " \
+            "$RUNFILES_MANIFEST_FILE" | cut -d ' ' -f 2-)"
+else
+  echo >&2 "ERROR: cannot find @bazel_tools//tools/bash/runfiles:runfiles.bash"
+  exit 1
+fi
+# --- end runfiles.bash initialization ---
+
+
+
+# Copied from http://trac.ffmpeg.org/wiki/Create%20a%20mosaic%20out%20of%20several%20input%20videos
+
+$(rlocation ffmpeg/ffmpeg)\
+	-i $1 -i $2 -i $3 -i $4\
+	-filter_complex "
+		nullsrc=size=2560x1440 [base];
+		[0:v] setpts=PTS-STARTPTS, scale=1280x720 [upperleft];
+		[1:v] setpts=PTS-STARTPTS, scale=1280x720 [upperright];
+		[2:v] setpts=PTS-STARTPTS, scale=1280x720 [lowerleft];
+		[3:v] setpts=PTS-STARTPTS, scale=1280x720 [lowerright];
+		[base][upperleft] overlay=shortest=1 [tmp1];
+		[tmp1][upperright] overlay=shortest=1:x=1280 [tmp2];
+		[tmp2][lowerleft] overlay=shortest=1:y=720 [tmp3];
+		[tmp3][lowerright] overlay=shortest=1:x=1280:y=720
+	" \
+	-c:v libx264 ~/video_tiler_out.mp4
diff --git a/y2024/BUILD b/y2024/BUILD
index 875bf84..212f70e 100644
--- a/y2024/BUILD
+++ b/y2024/BUILD
@@ -1,6 +1,5 @@
 load("//frc971:downloader.bzl", "robot_downloader")
 load("//aos:config.bzl", "aos_config")
-load("//tools/build_rules:template.bzl", "jinja2_template")
 load("//aos/util:config_validator_macro.bzl", "config_validator_test")
 
 config_validator_test(
@@ -19,7 +18,9 @@
         ":aos_config",
         "//aos/starter:roborio_irq_config.json",
         "//y2024/constants:constants.json",
+        "@ctre_phoenix6_api_cpp_athena//:shared_libraries",
         "@ctre_phoenix6_tools_athena//:shared_libraries",
+        "@ctre_phoenix_api_cpp_athena//:shared_libraries",
         "@ctre_phoenix_cci_athena//:shared_libraries",
     ],
     dirs = [
@@ -56,6 +57,8 @@
         "//frc971/vision:intrinsics_calibration",
         "//y2024/vision:viewer",
         "//y2024/constants:constants_sender",
+        "//y2024/localizer:localizer_main",
+        "//y2024/localizer:localizer_logger",
         "//y2024/vision:foxglove_image_converter",
     ],
     data = [
@@ -71,6 +74,8 @@
     ],
     start_binaries = [
         "//aos/events/logging:logger_main",
+        "//frc971/imu_fdcan:can_translator",
+        "//frc971/imu_fdcan:dual_imu_blender",
         "//aos/network:message_bridge_client",
         "//aos/network:message_bridge_server",
         "//aos/network:web_proxy_main",
@@ -100,7 +105,6 @@
     deps = [
         ":config_imu",
         ":config_orin1",
-        ":config_orin2",
         ":config_roborio",
     ],
 )
@@ -111,10 +115,20 @@
     flatbuffers = [
         "//aos/network:message_bridge_client_fbs",
         "//aos/network:message_bridge_server_fbs",
+        "//frc971/imu_fdcan:dual_imu_fbs",
+        "//frc971/imu_fdcan:can_translator_status_fbs",
+        "//frc971/imu_fdcan:dual_imu_blender_status_fbs",
         "//y2024/constants:constants_fbs",
+        "//frc971/control_loops/drivetrain/localization:localizer_output_fbs",
         "//frc971/can_logger:can_logging_fbs",
+        "//y2024/localizer:status_fbs",
+        "//y2024/localizer:visualization_fbs",
         "//aos/network:timestamp_fbs",
         "//aos/network:remote_message_fbs",
+        "//frc971/vision:calibration_fbs",
+        "//frc971/vision:target_map_fbs",
+        "//frc971/vision:vision_fbs",
+        "@com_github_foxglove_schemas//:schemas",
     ],
     target_compatible_with = ["@platforms//os:linux"],
     visibility = ["//visibility:public"],
@@ -132,6 +146,7 @@
         "//aos/network:remote_message_fbs",
         "//aos/network:message_bridge_client_fbs",
         "//aos/network:message_bridge_server_fbs",
+        "//frc971/wpilib:pdp_values_fbs",
         #y2019 stuff shouldn't be here (e.g. target selector)
         "//y2024/constants:constants_fbs",
         "//aos/network:timestamp_fbs",
@@ -139,7 +154,9 @@
         "//frc971/control_loops/drivetrain:drivetrain_can_position_fbs",
         "//y2024/control_loops/superstructure:superstructure_can_position_fbs",
         "//y2024/control_loops/superstructure:superstructure_output_fbs",
+        "//frc971/control_loops/drivetrain:rio_localizer_inputs_fbs",
         "//y2024/control_loops/superstructure:superstructure_position_fbs",
+        "//frc971/control_loops/drivetrain/localization:localizer_output_fbs",
         "//y2024/control_loops/superstructure:superstructure_status_fbs",
         "//frc971/can_logger:can_logging_fbs",
     ],
@@ -152,45 +169,30 @@
     ],
 )
 
-[
-    aos_config(
-        name = "config_" + orin,
-        src = "y2024_" + orin + ".json",
-        flatbuffers = [
-            "//aos/network:message_bridge_client_fbs",
-            "//aos/network:message_bridge_server_fbs",
-            "//aos/network:timestamp_fbs",
-            "//aos/network:remote_message_fbs",
-            "//y2024/constants:constants_fbs",
-            "//frc971/control_loops/drivetrain/localization:localizer_output_fbs",
-            "//frc971/vision:calibration_fbs",
-            "//frc971/vision:target_map_fbs",
-            "//frc971/vision:vision_fbs",
-            "@com_github_foxglove_schemas//:schemas",
-        ],
-        target_compatible_with = ["@platforms//os:linux"],
-        visibility = ["//visibility:public"],
-        deps = [
-            "//aos/events:aos_config",
-            "//frc971/control_loops/drivetrain:aos_config",
-            "//frc971/input:aos_config",
-        ],
-    )
-    for orin in [
-        "orin1",
-        "orin2",
-    ]
-]
-
-[
-    jinja2_template(
-        name = "y2024_orin" + str(num) + ".json",
-        src = "y2024_orin_template.json",
-        parameters = {"NUM": str(num)},
-        target_compatible_with = ["@platforms//os:linux"],
-    )
-    for num in range(1, 3)
-]
+aos_config(
+    name = "config_orin1",
+    src = "y2024_orin1.json",
+    flatbuffers = [
+        "//aos/network:message_bridge_client_fbs",
+        "//aos/network:message_bridge_server_fbs",
+        "//aos/network:timestamp_fbs",
+        "//aos/network:remote_message_fbs",
+        "//y2024/constants:constants_fbs",
+        "//frc971/control_loops/drivetrain/localization:localizer_output_fbs",
+        "//frc971/vision:calibration_fbs",
+        "//y2024/localizer:visualization_fbs",
+        "//frc971/vision:target_map_fbs",
+        "//frc971/vision:vision_fbs",
+        "@com_github_foxglove_schemas//:schemas",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//aos/events:aos_config",
+        "//frc971/control_loops/drivetrain:aos_config",
+        "//frc971/input:aos_config",
+    ],
+)
 
 cc_library(
     name = "constants",
@@ -210,9 +212,14 @@
         "//frc971/shooter_interpolation:interpolation",
         "//frc971/zeroing:absolute_encoder",
         "//frc971/zeroing:pot_and_absolute_encoder",
+        "//y2024/constants:constants_fbs",
         "//y2024/control_loops/drivetrain:polydrivetrain_plants",
+        "//y2024/control_loops/superstructure/altitude:altitude_plants",
+        "//y2024/control_loops/superstructure/catapult:catapult_plants",
         "//y2024/control_loops/superstructure/climber:climber_plants",
+        "//y2024/control_loops/superstructure/extend:extend_plants",
         "//y2024/control_loops/superstructure/intake_pivot:intake_pivot_plants",
+        "//y2024/control_loops/superstructure/turret:turret_plants",
         "@com_github_google_glog//:glog",
         "@com_google_absl//absl/base",
     ],
@@ -282,6 +289,7 @@
         "//frc971/autonomous:auto_fbs",
         "//frc971/autonomous:base_autonomous_actor",
         "//frc971/control_loops:profiled_subsystem_fbs",
+        "//frc971/control_loops:static_zeroing_single_dof_profiled_subsystem",
         "//frc971/input:action_joystick_input",
         "//frc971/input:drivetrain_input",
         "//frc971/input:joystick_input",
diff --git a/y2024/autonomous/BUILD b/y2024/autonomous/BUILD
index 611f3cd..f69d9fc 100644
--- a/y2024/autonomous/BUILD
+++ b/y2024/autonomous/BUILD
@@ -49,6 +49,7 @@
         "//aos/logging",
         "//aos/util:phased_loop",
         "//frc971/autonomous:base_autonomous_actor",
+        "//frc971/autonomous:user_button_localized_autonomous_actor",
         "//frc971/control_loops:control_loops_fbs",
         "//frc971/control_loops:profiled_subsystem_fbs",
         "//frc971/control_loops/drivetrain:drivetrain_config",
diff --git a/y2024/autonomous/autonomous_actor.cc b/y2024/autonomous/autonomous_actor.cc
index 6b1d9a5..3c7f2fa 100644
--- a/y2024/autonomous/autonomous_actor.cc
+++ b/y2024/autonomous/autonomous_actor.cc
@@ -34,74 +34,24 @@
 namespace chrono = ::std::chrono;
 
 AutonomousActor::AutonomousActor(::aos::EventLoop *event_loop)
-    : frc971::autonomous::BaseAutonomousActor(
+    : frc971::autonomous::UserButtonLocalizedAutonomousActor(
           event_loop,
           control_loops::drivetrain::GetDrivetrainConfig(event_loop)),
       localizer_control_sender_(
           event_loop->MakeSender<
               ::frc971::control_loops::drivetrain::LocalizerControl>(
               "/drivetrain")),
-      joystick_state_fetcher_(
-          event_loop->MakeFetcher<aos::JoystickState>("/aos")),
-      robot_state_fetcher_(event_loop->MakeFetcher<aos::RobotState>("/aos")),
-      auto_splines_(),
       superstructure_goal_sender_(
-          event_loop->MakeSender<::y2024::control_loops::superstructure::Goal>(
-              "/superstructure")),
+          event_loop
+              ->MakeSender<::y2024::control_loops::superstructure::GoalStatic>(
+                  "/superstructure")),
       superstructure_status_fetcher_(
           event_loop
               ->MakeFetcher<::y2024::control_loops::superstructure::Status>(
-                  "/superstructure")) {
-  drivetrain_status_fetcher_.Fetch();
-  replan_timer_ = event_loop->AddTimer([this]() { Replan(); });
-
-  event_loop->OnRun([this, event_loop]() {
-    replan_timer_->Schedule(event_loop->monotonic_now());
-    button_poll_->Schedule(event_loop->monotonic_now(),
-                           chrono::milliseconds(50));
-  });
-
-  // TODO(james): Really need to refactor this code since we keep using it.
-  button_poll_ = event_loop->AddTimer([this]() {
-    const aos::monotonic_clock::time_point now =
-        this->event_loop()->context().monotonic_event_time;
-    if (robot_state_fetcher_.Fetch()) {
-      if (robot_state_fetcher_->user_button()) {
-        user_indicated_safe_to_reset_ = true;
-        MaybeSendStartingPosition();
-      }
-    }
-    if (joystick_state_fetcher_.Fetch()) {
-      if (joystick_state_fetcher_->has_alliance() &&
-          (joystick_state_fetcher_->alliance() != alliance_)) {
-        alliance_ = joystick_state_fetcher_->alliance();
-        is_planned_ = false;
-        // Only kick the planning out by 2 seconds. If we end up enabled in
-        // that second, then we will kick it out further based on the code
-        // below.
-        replan_timer_->Schedule(now + std::chrono::seconds(2));
-      }
-      if (joystick_state_fetcher_->enabled()) {
-        if (!is_planned_) {
-          // Only replan once we've been disabled for 5 seconds.
-          replan_timer_->Schedule(now + std::chrono::seconds(5));
-        }
-      }
-    }
-  });
-}
+                  "/superstructure")),
+      auto_splines_() {}
 
 void AutonomousActor::Replan() {
-  if (!drivetrain_status_fetcher_.Fetch()) {
-    replan_timer_->Schedule(event_loop()->monotonic_now() + chrono::seconds(1));
-    AOS_LOG(INFO, "Drivetrain not up, replanning in 1 second");
-    return;
-  }
-
-  if (alliance_ == aos::Alliance::kInvalid) {
-    return;
-  }
-  sent_starting_position_ = false;
   if (FLAGS_spline_auto) {
     test_spline_ =
         PlanSpline(std::bind(&AutonomousSplines::TestSpline, &auto_splines_,
@@ -116,42 +66,8 @@
   MaybeSendStartingPosition();
 }
 
-void AutonomousActor::MaybeSendStartingPosition() {
-  if (is_planned_ && user_indicated_safe_to_reset_ &&
-      !sent_starting_position_) {
-    CHECK(starting_position_);
-    SendStartingPosition(starting_position_.value());
-  }
-}
-
-void AutonomousActor::Reset() {
-  InitializeEncoders();
-  ResetDrivetrain();
-
-  joystick_state_fetcher_.Fetch();
-  CHECK(joystick_state_fetcher_.get() != nullptr)
-      << "Expect at least one JoystickState message before running auto...";
-  alliance_ = joystick_state_fetcher_->alliance();
-  preloaded_ = false;
-}
-
-bool AutonomousActor::RunAction(
+bool AutonomousActor::Run(
     const ::frc971::autonomous::AutonomousActionParams *params) {
-  Reset();
-
-  AOS_LOG(INFO, "Params are %d\n", params->mode());
-
-  if (!user_indicated_safe_to_reset_) {
-    AOS_LOG(WARNING, "Didn't send starting position prior to starting auto.");
-    CHECK(starting_position_);
-    SendStartingPosition(starting_position_.value());
-  }
-  // Clear this so that we don't accidentally resend things as soon as we
-  // replan later.
-  user_indicated_safe_to_reset_ = false;
-  is_planned_ = false;
-  starting_position_.reset();
-
   AOS_LOG(INFO, "Params are %d\n", params->mode());
   if (alliance_ == aos::Alliance::kInvalid) {
     AOS_LOG(INFO, "Aborting autonomous due to invalid alliance selection.");
@@ -165,15 +81,6 @@
   return true;
 }
 
-void AutonomousActor::SplineAuto() {
-  CHECK(test_spline_);
-
-  if (!test_spline_->WaitForPlan()) return;
-  test_spline_->Start();
-
-  if (!test_spline_->WaitForSplineDistanceRemaining(0.02)) return;
-}
-
 void AutonomousActor::SendStartingPosition(const Eigen::Vector3d &start) {
   // Set up the starting position for the blue alliance.
 
@@ -192,4 +99,84 @@
     AOS_LOG(ERROR, "Failed to reset localizer.\n");
   }
 }
+
+void AutonomousActor::Reset() {
+  set_intake_goal(control_loops::superstructure::IntakeGoal::NONE);
+  set_note_goal(control_loops::superstructure::NoteGoal::NONE);
+  set_auto_aim(false);
+  set_fire(false);
+  set_preloaded(false);
+  SendSuperstructureGoal();
+}
+
+void AutonomousActor::SplineAuto() {
+  CHECK(test_spline_);
+
+  if (!test_spline_->WaitForPlan()) return;
+  test_spline_->Start();
+
+  if (!test_spline_->WaitForSplineDistanceRemaining(0.02)) return;
+}
+
+void AutonomousActor::SendSuperstructureGoal() {
+  aos::Sender<control_loops::superstructure::GoalStatic>::StaticBuilder
+      goal_builder = superstructure_goal_sender_.MakeStaticBuilder();
+
+  goal_builder->set_intake_goal(intake_goal_);
+  goal_builder->set_note_goal(note_goal_);
+  goal_builder->set_fire(fire_);
+
+  control_loops::superstructure::ShooterGoalStatic *shooter_goal =
+      goal_builder->add_shooter_goal();
+
+  shooter_goal->set_auto_aim(auto_aim_);
+  shooter_goal->set_preloaded(preloaded_);
+
+  goal_builder.CheckOk(goal_builder.Send());
+}
+
+void AutonomousActor::Intake() {
+  set_intake_goal(control_loops::superstructure::IntakeGoal::INTAKE);
+  set_note_goal(control_loops::superstructure::NoteGoal::CATAPULT);
+  SendSuperstructureGoal();
+}
+
+void AutonomousActor::Aim() {
+  set_auto_aim(true);
+  SendSuperstructureGoal();
+}
+
+void AutonomousActor::Shoot() {
+  set_fire(true);
+  SendSuperstructureGoal();
+}
+
+[[nodiscard]] bool AutonomousActor::WaitForPreloaded() {
+  set_preloaded(true);
+  SendSuperstructureGoal();
+
+  ::aos::time::PhasedLoop phased_loop(frc971::controls::kLoopFrequency,
+                                      event_loop()->monotonic_now(),
+                                      aos::common::actions::kLoopOffset);
+
+  bool loaded = false;
+  while (!loaded) {
+    if (ShouldCancel()) {
+      return false;
+    }
+
+    phased_loop.SleepUntilNext();
+    superstructure_status_fetcher_.Fetch();
+    CHECK(superstructure_status_fetcher_.get() != nullptr);
+
+    loaded = (superstructure_status_fetcher_->shooter()->catapult_state() ==
+              control_loops::superstructure::CatapultState::LOADED);
+  }
+
+  set_preloaded(false);
+  SendSuperstructureGoal();
+
+  return true;
+}
+
 }  // namespace y2024::autonomous
diff --git a/y2024/autonomous/autonomous_actor.h b/y2024/autonomous/autonomous_actor.h
index c3d7d79..6796c94 100644
--- a/y2024/autonomous/autonomous_actor.h
+++ b/y2024/autonomous/autonomous_actor.h
@@ -3,62 +3,70 @@
 
 #include "aos/actions/actions.h"
 #include "aos/actions/actor.h"
-#include "frc971/autonomous/base_autonomous_actor.h"
+#include "frc971/autonomous/user_button_localized_autonomous_actor.h"
 #include "frc971/control_loops/control_loops_generated.h"
 #include "frc971/control_loops/drivetrain/drivetrain_config.h"
 #include "frc971/control_loops/drivetrain/localizer_generated.h"
 #include "y2024/autonomous/auto_splines.h"
-#include "y2024/control_loops/superstructure/superstructure_goal_generated.h"
-#include "y2024/control_loops/superstructure/superstructure_status_generated.h"
+#include "y2024/control_loops/superstructure/superstructure_goal_static.h"
+#include "y2024/control_loops/superstructure/superstructure_status_static.h"
 
 namespace y2024::autonomous {
 
-class AutonomousActor : public ::frc971::autonomous::BaseAutonomousActor {
+class AutonomousActor
+    : public ::frc971::autonomous::UserButtonLocalizedAutonomousActor {
  public:
   explicit AutonomousActor(::aos::EventLoop *event_loop);
 
-  bool RunAction(
-      const ::frc971::autonomous::AutonomousActionParams *params) override;
-
  private:
+  void set_intake_goal(control_loops::superstructure::IntakeGoal intake_goal) {
+    intake_goal_ = intake_goal;
+  }
+  void set_note_goal(control_loops::superstructure::NoteGoal note_goal) {
+    note_goal_ = note_goal;
+  }
+  void set_auto_aim(bool auto_aim) { auto_aim_ = auto_aim; }
+  void set_fire(bool fire) { fire_ = fire; }
+  void set_preloaded(bool preloaded) { preloaded_ = preloaded; }
+
+  bool Run(const ::frc971::autonomous::AutonomousActionParams *params) override;
+  void Replan() override;
+  void SendStartingPosition(const Eigen::Vector3d &start) override;
+  void Reset() override;
+
+  void SplineAuto();
   void SendSuperstructureGoal();
 
-  void Reset();
+  void Intake();
+  void Aim();
+  void Shoot();
 
-  void SendStartingPosition(const Eigen::Vector3d &start);
-  void MaybeSendStartingPosition();
-  void SplineAuto();
-  void Replan();
+  [[nodiscard]] bool WaitForPreloaded();
 
   aos::Sender<frc971::control_loops::drivetrain::LocalizerControl>
       localizer_control_sender_;
-  aos::Fetcher<aos::JoystickState> joystick_state_fetcher_;
-  aos::Fetcher<aos::RobotState> robot_state_fetcher_;
 
-  aos::TimerHandler *replan_timer_;
-  aos::TimerHandler *button_poll_;
+  aos::Sender<control_loops::superstructure::GoalStatic>
+      superstructure_goal_sender_;
 
-  aos::Alliance alliance_ = aos::Alliance::kInvalid;
-  AutonomousSplines auto_splines_;
-  bool user_indicated_safe_to_reset_ = false;
-  bool sent_starting_position_ = false;
-
-  bool is_planned_ = false;
-
-  std::optional<Eigen::Vector3d> starting_position_;
-
-  bool preloaded_ = false;
-
-  aos::Sender<control_loops::superstructure::Goal> superstructure_goal_sender_;
   aos::Fetcher<y2024::control_loops::superstructure::Status>
       superstructure_status_fetcher_;
 
+  AutonomousSplines auto_splines_;
+
   std::optional<SplineHandle> test_spline_;
 
-  // List of arm angles from arm::PointsList
-  const ::std::vector<::Eigen::Matrix<double, 3, 1>> points_;
+  control_loops::superstructure::IntakeGoal intake_goal_ =
+      control_loops::superstructure::IntakeGoal::NONE;
+
+  control_loops::superstructure::NoteGoal note_goal_ =
+      control_loops::superstructure::NoteGoal::NONE;
+
+  bool auto_aim_ = false;
+  bool fire_ = false;
+  bool preloaded_ = false;
 };
 
 }  // namespace y2024::autonomous
 
-#endif  // Y2024_AUTONOMOUS_AUTONOMOUS_ACTOR_H_
+#endif  // Y2024_AUTONOMOUS_AUTONOMOUS_ACTOR_H_
\ No newline at end of file
diff --git a/y2024/constants.h b/y2024/constants.h
index 8eac0c9..9923660 100644
--- a/y2024/constants.h
+++ b/y2024/constants.h
@@ -8,11 +8,17 @@
 #include "frc971/constants.h"
 #include "frc971/control_loops/pose.h"
 #include "frc971/control_loops/static_zeroing_single_dof_profiled_subsystem.h"
+#include "frc971/shooter_interpolation/interpolation.h"
 #include "frc971/zeroing/absolute_encoder.h"
 #include "frc971/zeroing/pot_and_absolute_encoder.h"
+#include "y2024/constants/constants_generated.h"
 #include "y2024/control_loops/drivetrain/drivetrain_dog_motor_plant.h"
+#include "y2024/control_loops/superstructure/altitude/altitude_plant.h"
+#include "y2024/control_loops/superstructure/catapult/catapult_plant.h"
 #include "y2024/control_loops/superstructure/climber/climber_plant.h"
+#include "y2024/control_loops/superstructure/extend/extend_plant.h"
 #include "y2024/control_loops/superstructure/intake_pivot/intake_pivot_plant.h"
+#include "y2024/control_loops/superstructure/turret/turret_plant.h"
 
 namespace y2024::constants {
 
@@ -55,16 +61,7 @@
     return 4096.0;
   }
 
-  static constexpr double kIntakePivotEncoderRatio() {
-    return (16.0 / 64.0) * (18.0 / 62.0);
-  }
-
-  static constexpr double kIntakePivotPotRatio() { return 16.0 / 64.0; }
-
-  static constexpr double kIntakePivotPotRadiansPerVolt() {
-    return kIntakePivotPotRatio() * (3.0 /*turns*/ / 5.0 /*volts*/) *
-           (2 * M_PI /*radians*/);
-  }
+  static constexpr double kIntakePivotEncoderRatio() { return (15.0 / 24.0); }
 
   static constexpr double kMaxIntakePivotEncoderPulsesPerSecond() {
     return control_loops::superstructure::intake_pivot::kFreeSpeed /
@@ -74,20 +71,23 @@
            kIntakePivotEncoderCountsPerRevolution();
   }
 
-  // TODO(Filip): Update climber values once we have them.
   static constexpr double kClimberEncoderCountsPerRevolution() {
     return 4096.0;
   }
 
-  static constexpr double kClimberEncoderRatio() {
-    return (16.0 / 64.0) * (18.0 / 62.0);
+  static constexpr double kClimberEncoderRatio() { return (16.0 / 60.0); }
+
+  static constexpr double kClimberPotMetersPerRevolution() {
+    return 16 * 0.25 * 0.0254;
   }
 
-  static constexpr double kClimberPotRatio() { return 16.0 / 64.0; }
+  static constexpr double kClimberEncoderMetersPerRadian() {
+    return kClimberEncoderRatio() * kClimberPotMetersPerRevolution() / 2.0 /
+           M_PI;
+  }
 
-  static constexpr double kClimberPotRadiansPerVolt() {
-    return kClimberPotRatio() * (3.0 /*turns*/ / 5.0 /*volts*/) *
-           (2 * M_PI /*radians*/);
+  static constexpr double kClimberPotMetersPerVolt() {
+    return kClimberPotMetersPerRevolution() * (10.0 /*turns*/ / 5.0 /*volts*/);
   }
 
   static constexpr double kMaxClimberEncoderPulsesPerSecond() {
@@ -96,12 +96,151 @@
            kClimberEncoderRatio() * kClimberEncoderCountsPerRevolution();
   }
 
+  static constexpr double kExtendEncoderCountsPerRevolution() { return 4096.0; }
+  // TODO: (niko) add the gear ratios for the intake once we have them
+  static constexpr double kCatapultEncoderCountsPerRevolution() {
+    return 4096.0;
+  }
+
+  static constexpr double kCatapultEncoderRatio() { return 12.0 / 24.0; }
+
+  static constexpr double kCatapultPotRatio() { return 12.0 / 24.0; }
+
+  static constexpr double kCatapultPotRadiansPerVolt() {
+    return kCatapultPotRatio() * (3.0 /*turns*/ / 5.0 /*volts*/) *
+           (2 * M_PI /*radians*/);
+  }
+
+  static constexpr double kMaxCatapultEncoderPulsesPerSecond() {
+    return control_loops::superstructure::catapult::kFreeSpeed / (2.0 * M_PI) *
+           control_loops::superstructure::catapult::kOutputRatio /
+           kCatapultEncoderRatio() * kCatapultEncoderCountsPerRevolution();
+  }
+
+  static constexpr double kExtendEncoderRatio() { return 1.0; }
+
+  static constexpr double kExtendPotMetersPerRevolution() {
+    return 36 * 0.005 * kExtendEncoderRatio();
+  }
+  static constexpr double kExtendEncoderMetersPerRadian() {
+    return kExtendPotMetersPerRevolution() / 2.0 / M_PI;
+  }
+  static constexpr double kExtendPotMetersPerVolt() {
+    return kExtendPotMetersPerRevolution() * (5.0 /*turns*/ / 5.0 /*volts*/);
+  }
+  static constexpr double kMaxExtendEncoderPulsesPerSecond() {
+    return control_loops::superstructure::extend::kFreeSpeed / (2.0 * M_PI) *
+           control_loops::superstructure::extend::kOutputRatio /
+           kExtendEncoderRatio() * kExtendEncoderCountsPerRevolution();
+  }
+
+  static constexpr double kTurretEncoderCountsPerRevolution() { return 4096.0; }
+
+  static constexpr double kTurretPotRatio() {
+    return (22.0 / 100.0) * (28.0 / 48.0) * (36.0 / 24.0);
+  }
+
+  static constexpr double kTurretEncoderRatio() { return 22.0 / 100.0; }
+
+  static constexpr double kTurretPotRadiansPerVolt() {
+    return kTurretPotRatio() * (10.0 /*turns*/ / 5.0 /*volts*/) *
+           (2 * M_PI /*radians*/);
+  }
+  static constexpr double kMaxTurretEncoderPulsesPerSecond() {
+    return control_loops::superstructure::turret::kFreeSpeed / (2.0 * M_PI) *
+           control_loops::superstructure::turret::kOutputRatio /
+           kTurretEncoderRatio() * kTurretEncoderCountsPerRevolution();
+  }
+
+  static constexpr double kAltitudeEncoderCountsPerRevolution() {
+    return 4096.0;
+  }
+
+  static constexpr double kAltitudeEncoderRatio() { return 16.0 / 162.0; }
+
+  static constexpr double kAltitudePotRatio() { return 16.0 / 162.0; }
+
+  static constexpr double kAltitudePotRadiansPerVolt() {
+    return kAltitudePotRatio() * (10.0 /*turns*/ / 5.0 /*volts*/) *
+           (2 * M_PI /*radians*/);
+  }
+  static constexpr double kMaxAltitudeEncoderPulsesPerSecond() {
+    return control_loops::superstructure::altitude::kFreeSpeed / (2.0 * M_PI) *
+           control_loops::superstructure::altitude::kOutputRatio /
+           kAltitudeEncoderRatio() * kAltitudeEncoderCountsPerRevolution();
+  }
+
+  // 20 -> 28 reduction to a 0.5" radius roller
+  static constexpr double kTransferRollerOutputRatio = (20.0 / 28.0) * 0.0127;
+  // 20 -> 34 reduction, and the 34 is on a 0.625" radius roller
+  static constexpr double kIntakeRollerOutputRatio = (20.0 / 34.0) * 0.015875;
+  // 20 -> 28 reduction to a 0.5" radius roller
+  static constexpr double kExtendRollerOutputRatio = (20.0 / 28.0) * 0.0127;
+
+  struct ShotParams {
+    // Measured in radians
+    double shot_altitude_angle = 0.0;
+    double shot_catapult_angle = 0.0;
+
+    // Muzzle velocity (m/s) of the game piece as it is released from the
+    // catapult.
+    double shot_velocity = 0.0;
+
+    // Speed over ground to use for shooting on the fly
+    double shot_speed_over_ground = 0.0;
+
+    static ShotParams BlendY(double coefficient, ShotParams a1, ShotParams a2) {
+      using ::frc971::shooter_interpolation::Blend;
+      return ShotParams{
+          .shot_altitude_angle = Blend(coefficient, a1.shot_altitude_angle,
+                                       a2.shot_altitude_angle),
+          .shot_catapult_angle = Blend(coefficient, a1.shot_catapult_angle,
+                                       a2.shot_catapult_angle),
+          .shot_velocity =
+              Blend(coefficient, a1.shot_velocity, a2.shot_velocity),
+          .shot_speed_over_ground =
+              Blend(coefficient, a1.shot_speed_over_ground,
+                    a2.shot_speed_over_ground),
+      };
+    }
+
+    static ShotParams FromFlatbuffer(const y2024::ShotParams *shot_params) {
+      return ShotParams{
+          .shot_altitude_angle = shot_params->shot_altitude_angle(),
+          .shot_catapult_angle = shot_params->shot_catapult_angle(),
+          .shot_velocity = shot_params->shot_velocity(),
+          .shot_speed_over_ground = shot_params->shot_speed_over_ground()};
+    }
+  };
+
+  static frc971::shooter_interpolation::InterpolationTable<ShotParams>
+  InterpolationTableFromFlatbuffer(
+      const flatbuffers::Vector<
+          flatbuffers::Offset<y2024::InterpolationTablePoint>> *table) {
+    std::vector<std::pair<double, ShotParams>> interpolation_table;
+
+    for (const InterpolationTablePoint *point : *table) {
+      interpolation_table.emplace_back(
+          point->distance_from_goal(),
+          ShotParams::FromFlatbuffer(point->shot_params()));
+    }
+
+    return frc971::shooter_interpolation::InterpolationTable<ShotParams>(
+        interpolation_table);
+  }
+
   struct PotAndAbsEncoderConstants {
     ::frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemParams<
         ::frc971::zeroing::PotAndAbsoluteEncoderZeroingEstimator>
         subsystem_params;
     double potentiometer_offset;
   };
+
+  struct AbsoluteEncoderConstants {
+    ::frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemParams<
+        ::frc971::zeroing::AbsoluteEncoderZeroingEstimator>
+        subsystem_params;
+  };
 };
 
 // Creates and returns a Values instance for the constants.
diff --git a/y2024/constants/7971.json b/y2024/constants/7971.json
index 9e6bafb..be70754 100644
--- a/y2024/constants/7971.json
+++ b/y2024/constants/7971.json
@@ -1,17 +1,32 @@
 {% from 'y2024/constants/common.jinja2' import intake_pivot_zero %}
 {% from 'y2024/constants/common.jinja2' import climber_zero %}
+{% from 'y2024/constants/common.jinja2' import catapult_zero %}
+{% from 'y2024/constants/common.jinja2' import altitude_zero %}
+{% from 'y2024/constants/common.jinja2' import turret_zero %}
+{% from 'y2024/constants/common.jinja2' import extend_zero %}
 
 {
-  "robot": {
-    "intake_constants": {
-      {% set _ = intake_pivot_zero.update(
-          {
-              "measured_absolute_position" : 0.0
-          }
-      ) %}
-      "zeroing_constants": {{ intake_pivot_zero | tojson(indent=2)}},
-      "potentiometer_offset": 0.0
+  "cameras": [
+    {
+      "calibration": {% include 'y2024/constants/calib_files/calibration_imu-7971-0_cam-24-01_2024-03-02_19-44-12.098903651.json' %}
     },
+    {
+      "calibration": {% include 'y2024/constants/calib_files/calibration_orin-7971-1-1_cam-24-02_2024-03-02_20-09-18.022901293.json' %}
+    },
+    {
+      "calibration": {% include 'y2024/constants/calib_files/calibration_orin-7971-c-1_cam-24-03_2024-03-02_20-09-18.016217514.json' %}
+    },
+    {
+      "calibration": {% include 'y2024/constants/calib_files/calibration_orin-7971-1-0_cam-24-04_2024-03-02_20-09-18.016860291.json' %}
+    }
+  ],
+  "robot": {
+    {% set _ = intake_pivot_zero.update(
+      {
+          "measured_absolute_position" : 0.0
+      }
+    ) %}
+    "intake_constants":  {{ intake_pivot_zero | tojson(indent=2)}},
     "climber_constants": {
       {% set _ = climber_zero.update(
           {
@@ -20,6 +35,42 @@
       ) %}
       "zeroing_constants": {{ climber_zero | tojson(indent=2)}},
       "potentiometer_offset": 0.0
+    },
+    "catapult_constants": {
+      {% set _ = catapult_zero.update(
+          {
+              "measured_absolute_position" : 0.0
+          }
+      ) %}
+      "zeroing_constants": {{ catapult_zero | tojson(indent=2)}},
+      "potentiometer_offset": 0.0
+    },
+    "altitude_constants": {
+      {% set _ = altitude_zero.update(
+          {
+              "measured_absolute_position" : 0.0
+          }
+      ) %}
+      "zeroing_constants": {{ altitude_zero | tojson(indent=2)}},
+      "potentiometer_offset": 0.0
+    },
+    "turret_constants": {
+      {% set _ = turret_zero.update(
+          {
+              "measured_absolute_position" : 0.0
+          }
+      ) %}
+      "zeroing_constants": {{ turret_zero | tojson(indent=2)}},
+      "potentiometer_offset": 0.0
+    },
+    "extend_constants": {
+      {% set _ = extend_zero.update(
+          {
+              "measured_absolute_position" : 0.0
+          }
+      ) %}
+      "zeroing_constants": {{ extend_zero | tojson(indent=2)}},
+      "potentiometer_offset": 0.0
     }
   },
   {% include 'y2024/constants/common.json' %}
diff --git a/y2024/constants/971.json b/y2024/constants/971.json
index 641783f..5cee276 100644
--- a/y2024/constants/971.json
+++ b/y2024/constants/971.json
@@ -1,30 +1,76 @@
 {% from 'y2024/constants/common.jinja2' import intake_pivot_zero %}
 {% from 'y2024/constants/common.jinja2' import climber_zero %}
+{% from 'y2024/constants/common.jinja2' import catapult_zero %}
+{% from 'y2024/constants/common.jinja2' import altitude_zero %}
+{% from 'y2024/constants/common.jinja2' import turret_zero %}
+{% from 'y2024/constants/common.jinja2' import extend_zero %}
 
 {
   "cameras": [
     {
-      "calibration": {% include 'y2024/constants/calib_files/calibration_orin-971-1_cam-24-00.json' %}
+      "calibration": {% include 'y2024/constants/calib_files/calibration_orin1-971-0_cam-24-05_2024-03-01_11-01-05.102438041.json' %}
+    },
+    {
+      "calibration": {% include 'y2024/constants/calib_files/calibration_orin1-971-1_cam-24-06_2024-03-01_11-01-20.409861949.json' %}
+    },
+    {
+      "calibration": {% include 'y2024/constants/calib_files/calibration_imu-971-0_cam-24-07_2024-03-01_11-01-32.895328333.json' %}
+    },
+    {
+      "calibration": {% include 'y2024/constants/calib_files/calibration_imu-971-1_cam-24-08_2024-03-01_11-02-11.982641320.json' %}
     }
   ],
   "robot": {
-    "intake_constants": {
-      {% set _ = intake_pivot_zero.update(
-          {
-              "measured_absolute_position" : 0.0
-          }
-      ) %}
-      "zeroing_constants": {{ intake_pivot_zero | tojson(indent=2)}},
-      "potentiometer_offset": 0.0
-    },
+    {% set _ = intake_pivot_zero.update(
+      {
+          "measured_absolute_position" : 3.2990161941868
+      }
+    ) %}
+    "intake_constants":  {{ intake_pivot_zero | tojson(indent=2)}},
     "climber_constants": {
       {% set _ = climber_zero.update(
           {
-              "measured_absolute_position" : 0.0
+              "measured_absolute_position" : 0.00260967415741875
           }
       ) %}
       "zeroing_constants": {{ climber_zero | tojson(indent=2)}},
-      "potentiometer_offset": 0.0
+      "potentiometer_offset": {{ -0.935529777248618 + 1.83632555414775 + 0.0431080619919798 - 0.493015437796464 }}
+    },
+    "catapult_constants": {
+      {% set _ = catapult_zero.update(
+          {
+              "measured_absolute_position" : 0.741253220327565
+          }
+      ) %}
+      "zeroing_constants": {{ catapult_zero | tojson(indent=2)}},
+      "potentiometer_offset": {{ 9.41595277209342 }}
+    },
+    "altitude_constants": {
+      {% set _ = altitude_zero.update(
+          {
+              "measured_absolute_position" : 0.130841088837793
+          }
+      ) %}
+      "zeroing_constants": {{ altitude_zero | tojson(indent=2)}},
+      "potentiometer_offset": -0.15316323147786
+    },
+    "turret_constants": {
+      {% set _ = turret_zero.update(
+          {
+              "measured_absolute_position" : 0.138686395993591
+          }
+      ) %}
+      "zeroing_constants": {{ turret_zero | tojson(indent=2)}},
+      "potentiometer_offset": {{ -6.47164779835404 }}
+    },
+    "extend_constants": {
+      {% set _ = extend_zero.update(
+          {
+              "measured_absolute_position" : 0.0314256815130559
+          }
+      ) %}
+      "zeroing_constants": {{ extend_zero | tojson(indent=2)}},
+      "potentiometer_offset": {{ -0.2574404033256 + 0.0170793439542 - 0.177097393974999 }}
     }
   },
   {% include 'y2024/constants/common.json' %}
diff --git a/y2024/constants/9971.json b/y2024/constants/9971.json
index 9e6bafb..ec59033 100644
--- a/y2024/constants/9971.json
+++ b/y2024/constants/9971.json
@@ -1,17 +1,23 @@
 {% from 'y2024/constants/common.jinja2' import intake_pivot_zero %}
 {% from 'y2024/constants/common.jinja2' import climber_zero %}
+{% from 'y2024/constants/common.jinja2' import catapult_zero %}
+{% from 'y2024/constants/common.jinja2' import altitude_zero %}
+{% from 'y2024/constants/common.jinja2' import turret_zero %}
+{% from 'y2024/constants/common.jinja2' import extend_zero %}
 
 {
+  "cameras": [
+    {
+      "calibration": {% include 'y2024/constants/calib_files/calibration_orin-971-1_cam-24-00.json' %}
+    }
+  ],
   "robot": {
-    "intake_constants": {
-      {% set _ = intake_pivot_zero.update(
-          {
-              "measured_absolute_position" : 0.0
-          }
-      ) %}
-      "zeroing_constants": {{ intake_pivot_zero | tojson(indent=2)}},
-      "potentiometer_offset": 0.0
-    },
+    {% set _ = intake_pivot_zero.update(
+      {
+          "measured_absolute_position" : 0.0
+      }
+    ) %}
+    "intake_constants":  {{ intake_pivot_zero | tojson(indent=2)}},
     "climber_constants": {
       {% set _ = climber_zero.update(
           {
@@ -20,6 +26,42 @@
       ) %}
       "zeroing_constants": {{ climber_zero | tojson(indent=2)}},
       "potentiometer_offset": 0.0
+    },
+    "catapult_constants": {
+      {% set _ = catapult_zero.update(
+          {
+              "measured_absolute_position" : 0.0
+          }
+      ) %}
+      "zeroing_constants": {{ catapult_zero | tojson(indent=2)}},
+      "potentiometer_offset": 0.0
+    },
+    "altitude_constants": {
+      {% set _ = altitude_zero.update(
+          {
+              "measured_absolute_position" : 0.0
+          }
+      ) %}
+      "zeroing_constants": {{ altitude_zero | tojson(indent=2)}},
+      "potentiometer_offset": 0.0
+    },
+    "turret_constants": {
+      {% set _ = turret_zero.update(
+          {
+              "measured_absolute_position" : 0.0
+          }
+      ) %}
+      "zeroing_constants": {{ turret_zero | tojson(indent=2)}},
+      "potentiometer_offset": 0.0
+    },
+    "extend_constants": {
+      {% set _ = extend_zero.update(
+          {
+              "measured_absolute_position" : 0.0
+          }
+      ) %}
+      "zeroing_constants": {{ extend_zero | tojson(indent=2)}},
+      "potentiometer_offset": 0.0
     }
   },
   {% include 'y2024/constants/common.json' %}
diff --git a/y2024/constants/BUILD b/y2024/constants/BUILD
index 1f25bcb..b07851e 100644
--- a/y2024/constants/BUILD
+++ b/y2024/constants/BUILD
@@ -1,5 +1,6 @@
 load("//aos/flatbuffers:generate.bzl", "static_flatbuffer")
 load("//tools/build_rules:template.bzl", "jinja2_template")
+load("//y2024/constants:validator.bzl", "constants_json")
 
 cc_library(
     name = "simulated_constants_sender",
@@ -17,14 +18,19 @@
 )
 
 jinja2_template(
-    name = "test_constants.json",
+    name = "test_constants_unvalidated.json",
     src = "test_constants.jinja2.json",
     includes = glob([
         "test_data/*.json",
     ]) + [
         "//y2024/control_loops/superstructure/intake_pivot:intake_pivot_json",
         "//y2024/control_loops/superstructure/climber:climber_json",
+        "//y2024/control_loops/superstructure/catapult:catapult_json",
+        "//y2024/control_loops/superstructure/altitude:altitude_json",
+        "//y2024/control_loops/superstructure/extend:extend_json",
+        "//y2024/control_loops/superstructure/turret:turret_json",
         "//y2024/control_loops/drivetrain:drivetrain_config.json",
+        "//y2024/constants/calib_files",
         "common.json",
         "common.jinja2",
         "//y2024/vision/maps",
@@ -34,7 +40,7 @@
 )
 
 jinja2_template(
-    name = "constants.json",
+    name = "constants_unvalidated.json",
     src = "constants.jinja2.json",
     includes = [
         "7971.json",
@@ -44,8 +50,12 @@
         "common.json",
         "//y2024/constants/calib_files",
         "//y2024/control_loops/drivetrain:drivetrain_config.json",
+        "//y2024/control_loops/superstructure/altitude:altitude_json",
+        "//y2024/control_loops/superstructure/catapult:catapult_json",
         "//y2024/control_loops/superstructure/climber:climber_json",
+        "//y2024/control_loops/superstructure/extend:extend_json",
         "//y2024/control_loops/superstructure/intake_pivot:intake_pivot_json",
+        "//y2024/control_loops/superstructure/turret:turret_json",
         "//y2024/vision/maps",
     ],
     parameters = {},
@@ -86,15 +96,27 @@
     ],
 )
 
-cc_test(
-    name = "constants_validator_test",
-    srcs = ["constants_validator_test.cc"],
-    data = [":constants.json"],
+cc_binary(
+    name = "constants_formatter",
+    srcs = ["constants_formatter.cc"],
+    data = [":constants_unvalidated.json"],
     visibility = ["//visibility:public"],
     deps = [
         ":constants_list_fbs",
+        "//aos:init",
         "//aos:json_to_flatbuffer",
-        "//aos/testing:googletest",
         "@com_github_google_glog//:glog",
     ],
 )
+
+constants_json(
+    name = "constants_json",
+    src = ":constants_unvalidated.json",
+    out = "constants.json",
+)
+
+constants_json(
+    name = "test_constants_json",
+    src = ":test_constants_unvalidated.json",
+    out = "test_constants.json",
+)
diff --git a/y2024/constants/calib_files/calibration_imu-7971-0_cam-24-01_2024-03-02_19-44-12.098903651.json b/y2024/constants/calib_files/calibration_imu-7971-0_cam-24-01_2024-03-02_19-44-12.098903651.json
new file mode 100755
index 0000000..13301a0
--- /dev/null
+++ b/y2024/constants/calib_files/calibration_imu-7971-0_cam-24-01_2024-03-02_19-44-12.098903651.json
@@ -0,0 +1,45 @@
+{
+ "node_name": "imu",
+ "team_number": 7971,
+ "intrinsics": [
+  646.870789,
+  0.0,
+  731.468811,
+  0.0,
+  646.616333,
+  570.003723,
+  0.0,
+  0.0,
+  1.0
+ ],
+ "fixed_extrinsics": {
+  "data": [
+   1.0,
+   -0.0,
+   0.0,
+   -0.131843,
+   0.0,
+   0.0,
+   1.0,
+   0.061756,
+   -0.0,
+   -1.0,
+   0.0,
+   -0.05456,
+   0.0,
+   0.0,
+   0.0,
+   1.0
+  ]
+ },
+ "dist_coeffs": [
+  -0.249077,
+  0.063132,
+  0.000082,
+  0.00026,
+  -0.006916
+ ],
+ "calibration_timestamp": 1707365495566609408,
+ "camera_id": "24-01",
+ "camera_number": 0
+}
\ No newline at end of file
diff --git a/y2024/constants/calib_files/calibration_imu-971-0_cam-24-07_2024-03-01_11-01-32.895328333.json b/y2024/constants/calib_files/calibration_imu-971-0_cam-24-07_2024-03-01_11-01-32.895328333.json
new file mode 100755
index 0000000..d013e2a
--- /dev/null
+++ b/y2024/constants/calib_files/calibration_imu-971-0_cam-24-07_2024-03-01_11-01-32.895328333.json
@@ -0,0 +1,46 @@
+{
+ "node_name": "imu",
+ "team_number": 971,
+ "intrinsics": [
+  647.822815,
+  0.0,
+  715.37616,
+  0.0,
+  647.799316,
+  494.638641,
+  0.0,
+  0.0,
+  1.0
+ ],
+ "fixed_extrinsics": {
+  "data": [
+   1.0,
+   -0.0,
+   0.0,
+   0.111049,
+   0.0,
+   0.258819,
+   0.965926,
+   0.263806,
+   -0.0,
+   -0.965926,
+   0.258819,
+   0.347685,
+   0.0,
+   0.0,
+   0.0,
+   1.0
+  ]
+ },
+ "dist_coeffs": [
+  -0.2423,
+  0.057169,
+  0.000302,
+  0.000016,
+  -0.005638
+ ],
+ "calibration_timestamp": 1708833147338466592,
+ "camera_id": "24-07",
+ "camera_number": 0,
+ "reprojection_error": 1.362672
+}
\ No newline at end of file
diff --git a/y2024/constants/calib_files/calibration_imu-971-1_cam-24-08_2024-03-01_11-02-11.982641320.json b/y2024/constants/calib_files/calibration_imu-971-1_cam-24-08_2024-03-01_11-02-11.982641320.json
new file mode 100755
index 0000000..fbe79d5
--- /dev/null
+++ b/y2024/constants/calib_files/calibration_imu-971-1_cam-24-08_2024-03-01_11-02-11.982641320.json
@@ -0,0 +1,46 @@
+{
+ "node_name": "imu",
+ "team_number": 971,
+ "intrinsics": [
+  645.963562,
+  0.0,
+  751.21698,
+  0.0,
+  645.34906,
+  605.204102,
+  0.0,
+  0.0,
+  1.0
+ ],
+ "fixed_extrinsics": {
+  "data": [
+   0.0,
+   -0.258819,
+   -0.965926,
+   -0.323293,
+   1.0,
+   0.0,
+   -0.0,
+   0.268249,
+   0.0,
+   -0.965926,
+   0.258819,
+   0.471129,
+   0.0,
+   0.0,
+   0.0,
+   1.0
+  ]
+ },
+ "dist_coeffs": [
+  -0.248733,
+  0.06221,
+  -0.000901,
+  0.000128,
+  -0.006595
+ ],
+ "calibration_timestamp": 1708820514420797344,
+ "camera_id": "24-08",
+ "camera_number": 1,
+ "reprojection_error": 1.591953
+}
\ No newline at end of file
diff --git a/y2024/constants/calib_files/calibration_orin-7971-1-0_cam-24-04_2024-03-02_20-09-18.016860291.json b/y2024/constants/calib_files/calibration_orin-7971-1-0_cam-24-04_2024-03-02_20-09-18.016860291.json
new file mode 100755
index 0000000..d60cc2a
--- /dev/null
+++ b/y2024/constants/calib_files/calibration_orin-7971-1-0_cam-24-04_2024-03-02_20-09-18.016860291.json
@@ -0,0 +1,45 @@
+{
+ "node_name": "orin1",
+ "team_number": 7971,
+ "intrinsics": [
+  642.80365,
+  0.0,
+  718.017517,
+  0.0,
+  642.83667,
+  555.022461,
+  0.0,
+  0.0,
+  1.0
+ ],
+ "fixed_extrinsics": {
+  "data": [
+   -0.845268,
+   0.031126,
+   0.533435,
+   0.494822,
+   -0.525295,
+   0.134521,
+   -0.84022,
+   -1.212857,
+   -0.097911,
+   -0.990422,
+   -0.097356,
+   -0.319412,
+   0.0,
+   0.0,
+   0.0,
+   1.0
+  ]
+ },
+ "dist_coeffs": [
+  -0.239969,
+  0.055889,
+  0.000086,
+  0.000099,
+  -0.005468
+ ],
+ "calibration_timestamp": 1709438958016860291,
+ "camera_id": "24-04",
+ "camera_number": 0
+}
\ No newline at end of file
diff --git a/y2024/constants/calib_files/calibration_orin-7971-1-1_cam-24-02_2024-03-02_20-09-18.022901293.json b/y2024/constants/calib_files/calibration_orin-7971-1-1_cam-24-02_2024-03-02_20-09-18.022901293.json
new file mode 100755
index 0000000..ed7b2f3
--- /dev/null
+++ b/y2024/constants/calib_files/calibration_orin-7971-1-1_cam-24-02_2024-03-02_20-09-18.022901293.json
@@ -0,0 +1,45 @@
+{
+ "node_name": "orin1",
+ "team_number": 7971,
+ "intrinsics": [
+  644.604858,
+  0.0,
+  752.152954,
+  0.0,
+  644.477173,
+  558.911682,
+  0.0,
+  0.0,
+  1.0
+ ],
+ "fixed_extrinsics": {
+  "data": [
+   -0.07336,
+   0.085699,
+   0.993617,
+   0.608349,
+   -0.989433,
+   0.118683,
+   -0.083288,
+   -0.939084,
+   -0.125063,
+   -0.989227,
+   0.076087,
+   -0.35071,
+   0.0,
+   0.0,
+   0.0,
+   1.0
+  ]
+ },
+ "dist_coeffs": [
+  -0.242432,
+  0.057303,
+  -0.000057,
+  0.000015,
+  -0.005636
+ ],
+ "calibration_timestamp": 1709438958022901293,
+ "camera_id": "24-02",
+ "camera_number": 1
+}
\ No newline at end of file
diff --git a/y2024/constants/calib_files/calibration_orin-7971-c-1_cam-24-03_2024-03-02_20-09-18.016217514.json b/y2024/constants/calib_files/calibration_orin-7971-c-1_cam-24-03_2024-03-02_20-09-18.016217514.json
new file mode 100755
index 0000000..c0847a8
--- /dev/null
+++ b/y2024/constants/calib_files/calibration_orin-7971-c-1_cam-24-03_2024-03-02_20-09-18.016217514.json
@@ -0,0 +1,45 @@
+{
+ "node_name": "imu",
+ "team_number": 7971,
+ "intrinsics": [
+  648.13446,
+  0.0,
+  759.733093,
+  0.0,
+  648.40332,
+  557.951538,
+  0.0,
+  0.0,
+  1.0
+ ],
+ "fixed_extrinsics": {
+  "data": [
+   0.646787,
+   0.04307,
+   0.761453,
+   0.280984,
+   -0.762291,
+   0.067995,
+   0.643653,
+   -0.238549,
+   -0.024053,
+   -0.996756,
+   0.07681,
+   -0.200534,
+   0.0,
+   0.0,
+   0.0,
+   1.0
+  ]
+ },
+ "dist_coeffs": [
+  -0.258663,
+  0.071646,
+  0.000113,
+  -0.000061,
+  -0.00879
+ ],
+ "calibration_timestamp": 1709438958016217514,
+ "camera_id": "24-03",
+ "camera_number": 1
+}
\ No newline at end of file
diff --git a/y2024/constants/calib_files/calibration_orin-971-1-0_cam-24-10_2024-02-24_16-44-05.975708672.json b/y2024/constants/calib_files/calibration_orin-971-1-0_cam-24-10_2024-02-24_16-44-05.975708672.json
new file mode 100755
index 0000000..0912038
--- /dev/null
+++ b/y2024/constants/calib_files/calibration_orin-971-1-0_cam-24-10_2024-02-24_16-44-05.975708672.json
@@ -0,0 +1,26 @@
+{
+ "node_name": "orin1",
+ "team_number": 971,
+ "intrinsics": [
+  646.04834,
+  0.0,
+  703.327576,
+  0.0,
+  645.444458,
+  527.86261,
+  0.0,
+  0.0,
+  1.0
+ ],
+ "dist_coeffs": [
+  -0.251594,
+  0.064935,
+  0.000479,
+  0.000036,
+  -0.007207
+ ],
+ "calibration_timestamp": 1708821845975708672,
+ "camera_id": "24-10",
+ "camera_number": 0,
+ "reprojection_error": 1.523209
+}
diff --git a/y2024/constants/calib_files/calibration_orin-971-1-1_cam-24-09_2024-02-24_16-10-16.872521280.json b/y2024/constants/calib_files/calibration_orin-971-1-1_cam-24-09_2024-02-24_16-10-16.872521280.json
new file mode 100755
index 0000000..9eed9bf
--- /dev/null
+++ b/y2024/constants/calib_files/calibration_orin-971-1-1_cam-24-09_2024-02-24_16-10-16.872521280.json
@@ -0,0 +1,26 @@
+{
+ "node_name": "orin1",
+ "team_number": 971,
+ "intrinsics": [
+  648.187805,
+  0.0,
+  736.903137,
+  0.0,
+  648.028687,
+  557.169861,
+  0.0,
+  0.0,
+  1.0
+ ],
+ "dist_coeffs": [
+  -0.265564,
+  0.078084,
+  -0.000231,
+  0.000386,
+  -0.010425
+ ],
+ "calibration_timestamp": 1708819816872521280,
+ "camera_id": "24-09",
+ "camera_number": 1,
+ "reprojection_error": 1.881098
+}
\ No newline at end of file
diff --git a/y2024/constants/calib_files/calibration_orin-971-1-1_cam-24-11_2024-02-24_16-44-06.986729504.json b/y2024/constants/calib_files/calibration_orin-971-1-1_cam-24-11_2024-02-24_16-44-06.986729504.json
new file mode 100755
index 0000000..4957b75
--- /dev/null
+++ b/y2024/constants/calib_files/calibration_orin-971-1-1_cam-24-11_2024-02-24_16-44-06.986729504.json
@@ -0,0 +1,26 @@
+{
+ "node_name": "orin1",
+ "team_number": 971,
+ "intrinsics": [
+  649.866699,
+  0.0,
+  709.355713,
+  0.0,
+  648.893066,
+  576.101868,
+  0.0,
+  0.0,
+  1.0
+ ],
+ "dist_coeffs": [
+  -0.248092,
+  0.060938,
+  0.000313,
+  0.00009,
+  -0.006163
+ ],
+ "calibration_timestamp": 1708821846986729504,
+ "camera_id": "24-11",
+ "camera_number": 1,
+ "reprojection_error": 1.450069
+}
\ No newline at end of file
diff --git a/y2024/constants/calib_files/calibration_orin-971-1-1_cam-24-12_2024-02-24_19-52-39.488095264.json b/y2024/constants/calib_files/calibration_orin-971-1-1_cam-24-12_2024-02-24_19-52-39.488095264.json
new file mode 100755
index 0000000..0fda16d
--- /dev/null
+++ b/y2024/constants/calib_files/calibration_orin-971-1-1_cam-24-12_2024-02-24_19-52-39.488095264.json
@@ -0,0 +1,26 @@
+{
+ "node_name": "orin1",
+ "team_number": 971,
+ "intrinsics": [
+  647.19928,
+  0.0,
+  690.698181,
+  0.0,
+  646.449158,
+  530.162842,
+  0.0,
+  0.0,
+  1.0
+ ],
+ "dist_coeffs": [
+  -0.249799,
+  0.062593,
+  0.00003,
+  0.000366,
+  -0.006532
+ ],
+ "calibration_timestamp": 1708833159488095264,
+ "camera_id": "24-12",
+ "camera_number": 1,
+ "reprojection_error": 1.23409
+}
\ No newline at end of file
diff --git a/y2024/constants/calib_files/calibration_orin1-971-0_cam-24-05_2024-03-01_11-01-05.102438041.json b/y2024/constants/calib_files/calibration_orin1-971-0_cam-24-05_2024-03-01_11-01-05.102438041.json
new file mode 100755
index 0000000..317e453
--- /dev/null
+++ b/y2024/constants/calib_files/calibration_orin1-971-0_cam-24-05_2024-03-01_11-01-05.102438041.json
@@ -0,0 +1,46 @@
+{
+ "node_name": "orin1",
+ "team_number": 971,
+ "intrinsics": [
+  648.360168,
+  0.0,
+  729.818665,
+  0.0,
+  648.210327,
+  641.988037,
+  0.0,
+  0.0,
+  1.0
+ ],
+ "fixed_extrinsics": {
+  "data": [
+   0.0,
+   0.0,
+   1.0,
+   0.284397,
+   -1.0,
+   0.0,
+   0.0,
+   0.226771,
+   0.0,
+   -1.0,
+   0.0,
+   0.442951,
+   0.0,
+   0.0,
+   0.0,
+   1.0
+  ]
+ },
+ "dist_coeffs": [
+  -0.255473,
+  0.068444,
+  0.000028,
+  -0.000078,
+  -0.008004
+ ],
+ "calibration_timestamp": 409227793683328,
+ "camera_id": "24-05",
+ "camera_number": 0,
+ "reprojection_error": 1.058851
+}
\ No newline at end of file
diff --git a/y2024/constants/calib_files/calibration_orin1-971-1_cam-24-06_2024-03-01_11-01-20.409861949.json b/y2024/constants/calib_files/calibration_orin1-971-1_cam-24-06_2024-03-01_11-01-20.409861949.json
new file mode 100755
index 0000000..0eb10db
--- /dev/null
+++ b/y2024/constants/calib_files/calibration_orin1-971-1_cam-24-06_2024-03-01_11-01-20.409861949.json
@@ -0,0 +1,46 @@
+{
+ "node_name": "orin1",
+ "team_number": 971,
+ "intrinsics": [
+  648.644104,
+  0.0,
+  755.677979,
+  0.0,
+  648.522644,
+  597.744812,
+  0.0,
+  0.0,
+  1.0
+ ],
+ "fixed_extrinsics": {
+  "data": [
+   -1.0,
+   0.0,
+   0.0,
+   0.111049,
+   -0.0,
+   -0.258819,
+   -0.965926,
+   -0.263806,
+   0.0,
+   -0.965926,
+   0.258819,
+   0.347685,
+   0.0,
+   0.0,
+   0.0,
+   1.0
+  ]
+ },
+ "dist_coeffs": [
+  -0.25182,
+  0.063137,
+  0.000118,
+  0.000005,
+  -0.006342
+ ],
+ "calibration_timestamp": 409229245444672,
+ "camera_id": "24-06",
+ "camera_number": 1,
+ "reprojection_error": 1.344104
+}
\ No newline at end of file
diff --git a/y2024/constants/common.jinja2 b/y2024/constants/common.jinja2
index d6db5e5..27f28ac 100644
--- a/y2024/constants/common.jinja2
+++ b/y2024/constants/common.jinja2
@@ -1,27 +1,82 @@
 {% set pi = 3.14159265 %}
 
-{# we do this here so we keep the encoder ratio in plaintext and also keep the math we're using. #}
-{% set intake_pivot_encoder_ratio = (24.0 / 15.0) %}
-
 {%set zeroing_sample_size = 200 %}
 
+{# Intake #}
+{# we do this here so we keep the encoder ratio in plaintext and also keep the math we're using. #}
+{% set intake_pivot_encoder_ratio = (15.0 / 24.0) %}
+
+{% set intake_upper = 1.75 %}
+{% set intake_lower = -0.195 %}
+
 {%
 set intake_pivot_zero = {
     "average_filter_size": zeroing_sample_size,
     "one_revolution_distance": pi * 2.0 * intake_pivot_encoder_ratio,
+    "middle_position": (intake_upper + intake_lower) / 2,
     "zeroing_threshold": 0.0005,
     "moving_buffer_size": 20,
     "allowable_encoder_error": 0.9
 }
 %}
 
-{# TODO(Filip): Update climber values #}
-{% set climber_encoder_ratio = (1.0 / 1.0) %}
-{% set climber_radius = 0.436496 %}
+{% set extend_encoder_ratio = (1.0 / 1.0) %}
+{% set extend_radius = 36.0 * 0.005 / 2.0 / pi %}
+{%
+set extend_zero = {
+    "average_filter_size": zeroing_sample_size,
+    "one_revolution_distance": pi * 2.0 * extend_encoder_ratio * extend_radius,
+    "zeroing_threshold": 0.0005,
+    "moving_buffer_size": 20,
+    "allowable_encoder_error": 0.9
+}
+%}
+
+{% set climber_encoder_ratio = (16.0 / 60.0) %}
+{% set climber_circumference = 16.0 * 0.25 * 0.0254 %}
 {%
 set climber_zero = {
     "average_filter_size": zeroing_sample_size,
-    "one_revolution_distance": pi * 2.0 * climber_encoder_ratio * climber_radius,
+    "one_revolution_distance": climber_encoder_ratio * climber_circumference,
+    "zeroing_threshold": 0.0005,
+    "moving_buffer_size": 20,
+    "allowable_encoder_error": 0.9
+}
+%}
+
+{# Catapult #}
+{% set catapult_encoder_ratio = (12.0 / 24.0) %}
+
+{%
+set catapult_zero = {
+    "average_filter_size": zeroing_sample_size,
+    "one_revolution_distance": pi * 2.0 * catapult_encoder_ratio,
+    "zeroing_threshold": 0.0005,
+    "moving_buffer_size": 20,
+    "allowable_encoder_error": 0.9
+}
+%}
+
+{# Altitude #}
+{% set altitude_encoder_ratio = (16.0 / 162.0) %}
+
+{%
+set altitude_zero = {
+    "average_filter_size": zeroing_sample_size,
+    "one_revolution_distance": pi * 2.0 * altitude_encoder_ratio,
+    "zeroing_threshold": 0.0005,
+    "moving_buffer_size": 20,
+    "allowable_encoder_error": 0.9
+}
+%}
+
+{# Turret #}
+{% set turret_encoder_ratio = (22.0 / 100.0) %}
+
+{%
+set turret_zero = {
+    "average_filter_size": zeroing_sample_size,
+    "one_revolution_distance": pi * 2.0 * turret_encoder_ratio,
     "zeroing_threshold": 0.0005,
     "moving_buffer_size": 20,
     "allowable_encoder_error": 0.9
diff --git a/y2024/constants/common.json b/y2024/constants/common.json
index 554f1e8..1050815 100644
--- a/y2024/constants/common.json
+++ b/y2024/constants/common.json
@@ -1,21 +1,34 @@
+{% from 'y2024/constants/common.jinja2' import intake_upper, intake_lower %}
+
 "common": {
   "target_map": {% include 'y2024/vision/maps/target_map.json' %},
   "shooter_interpolation_table": [
     {
-        "distance_from_goal": 0.0,
+        "distance_from_goal": 5.0,
         "shot_params": {
             "shot_velocity": 0.0,
-            "shot_angle": 0.0
+            "shot_altitude_angle": 0.0,
+            "shot_catapult_angle": 0.0,
+            "shot_speed_over_ground": 2.0
         }
+    },
+    {
+      "distance_from_goal": 10.0,
+      "shot_params": {
+          "shot_velocity": 0.0,
+          "shot_altitude_angle": 0.0,
+          "shot_catapult_angle": 0.0,
+          "shot_speed_over_ground": 4.0
+      }
     }
   ],
   "intake_roller_voltages": {
-    "spitting": -12.0,
+    "spitting": -4.0,
     "intaking": 12.0
   },
   "intake_pivot_set_points": {
-    "extended": 1.5,
-    "retracted": 0.5
+    "extended": -0.03,
+    "retracted": 1.73
   },
   "intake_pivot": {
     "zeroing_voltage": 3.0,
@@ -25,14 +38,14 @@
       "max_acceleration": 3.0
     },
     "default_profile_params":{
-      "max_velocity": 6.0,
-      "max_acceleration": 30.0
+      "max_velocity": 4.0,
+      "max_acceleration": 10.0
     },
     "range": {
-        "lower_hard": -0.85,
-        "upper_hard": 1.85,
-        "lower": -0.400,
-        "upper": 1.57
+        "lower_hard": -0.2,
+        "upper_hard": 1.80,
+        "lower": {{ intake_lower }},
+        "upper": {{ intake_upper }}
     },
     "loop": {% include 'y2024/control_loops/superstructure/intake_pivot/integral_intake_pivot_plant.json' %}
   },
@@ -40,25 +53,44 @@
   // TODO: (niko) update the stator and supply current limits for the intake
   "current_limits": {
     // Values in amps
-    "intake_pivot_supply_current_limit": 35,
-    "intake_pivot_stator_current_limit": 60,
-    "intake_roller_supply_current_limit": 35,
-    "intake_roller_stator_current_limit": 60,
-    "transfer_roller_supply_current_limit": 35,
-    "transfer_roller_stator_current_limit": 60,
+    "intake_pivot_supply_current_limit": 40,
+    "intake_pivot_stator_current_limit": 100,
+    "intake_roller_supply_current_limit": 20,
+    "intake_roller_stator_current_limit": 50,
+    "transfer_roller_supply_current_limit": 20,
+    "transfer_roller_stator_current_limit": 50,
     "drivetrain_supply_current_limit": 35,
     "drivetrain_stator_current_limit": 60,
-    "climber_supply_current_limit": 35,
-    "climber_stator_current_limit": 60
+    "climber_supply_current_limit": 30,
+    "climber_stator_current_limit": 100,
+    "extend_supply_current_limit": 20,
+    "extend_stator_current_limit": 100,
+    "extend_roller_supply_current_limit": 60,
+    "extend_roller_stator_current_limit": 200,
+    "turret_supply_current_limit": 20,
+    "turret_stator_current_limit": 40,
+    "altitude_supply_current_limit": 10,
+    "altitude_stator_current_limit": 60,
+    "catapult_supply_current_limit": 60,
+    "catapult_stator_current_limit": 250,
+    "retention_roller_stator_current_limit": 20,
+    "slower_retention_roller_stator_current_limit": 2,
+    "shooting_retention_roller_stator_current_limit": -20,
+    "retention_roller_supply_current_limit": 10
   },
   "transfer_roller_voltages": {
     "transfer_in": 12.0,
-    "transfer_out": -12.0
+    "transfer_out": -4.0,
+    "extend_moving": 4.0
+  },
+  "extend_roller_voltages": {
+    "scoring": 12.0,
+    "reversing": -4.0
   },
   "climber_set_points": {
-    "full_extend": 0.8,
-    "half_extend": 0.6,
-    "retract": 0.2
+    "full_extend": -0.005,
+    "stowed": -0.35,
+    "retract": -0.478
   },
   "climber": {
     "zeroing_voltage": 3.0,
@@ -68,15 +100,143 @@
       "max_acceleration": 3.0
     },
     "default_profile_params":{
-      "max_velocity": 6.0,
-      "max_acceleration": 30.0
+      "max_velocity": 0.05,
+      "max_acceleration": 3.0
     },
     "range": {
-        "lower_hard": 0.1,
-        "upper_hard": 2.01,
-        "lower": 0.2,
-        "upper": 2.0
+        "lower_hard": -0.488,
+        "upper_hard": 0.005,
+        "lower": -0.478,
+        "upper": -0.005
     },
     "loop": {% include 'y2024/control_loops/superstructure/climber/integral_climber_plant.json' %}
-  }
+  },
+  "catapult": {
+    "zeroing_voltage": 3.0,
+    "operating_voltage": 12.0,
+    "zeroing_profile_params": {
+      "max_velocity": 1.0,
+      "max_acceleration": 6.0
+    },
+    "default_profile_params":{
+      "max_velocity": 0.025,
+      "max_acceleration": 0.05
+    },
+    "range": {
+        "lower_hard": -0.05,
+        "upper_hard": 4.2,
+        "lower": 0.0,
+        "upper": 2.38
+    },
+    "loop": {% include 'y2024/control_loops/superstructure/catapult/integral_catapult_plant.json' %}
+  },
+  // TODO: (max) update the constants for the shooter subsystems
+  "altitude": {
+    "zeroing_voltage": 3.0,
+    "operating_voltage": 12.0,
+    "zeroing_profile_params": {
+      "max_velocity": 0.5,
+      "max_acceleration": 3.0
+    },
+    "default_profile_params":{
+      "max_velocity": 3.0,
+      "max_acceleration": 5.0
+    },
+    "range": {
+        "lower_hard": -0.01,
+        "upper_hard": 1.66,
+        "lower": 0.0135,
+        "upper": 1.57
+    },
+    "loop": {% include 'y2024/control_loops/superstructure/altitude/integral_altitude_plant.json' %}
+  },
+  "turret": {
+    "zeroing_voltage": 3.0,
+    "operating_voltage": 12.0,
+    "zeroing_profile_params": {
+      "max_velocity": 0.5,
+      "max_acceleration": 3.0
+    },
+    "default_profile_params":{
+      "max_velocity": 2.0,
+      "max_acceleration": 5.0
+    },
+    "range": {
+        "lower_hard": -4.8,
+        "upper_hard": 4.8,
+        "lower": -4.7,
+        "upper": 4.7
+    },
+    "loop": {% include 'y2024/control_loops/superstructure/turret/integral_turret_plant.json' %}
+  },
+  "extend": {
+    "zeroing_voltage": 3.0,
+    "operating_voltage": 12.0,
+    "zeroing_profile_params": {
+      "max_velocity": 0.5,
+      "max_acceleration": 3.0
+    },
+    "default_profile_params":{
+      "max_velocity": 0.1,
+      "max_acceleration": 0.3
+    },
+    "range": {
+        "lower_hard": -0.005,
+        "upper_hard": 0.47,
+        "lower": 0.005,
+        "upper": 0.46
+    },
+    "loop": {% include 'y2024/control_loops/superstructure/extend/integral_extend_plant.json' %}
+  },
+  "shooter_targets": {
+    "red_alliance": {
+        "pos": {
+            "rows": 3,
+            "cols": 1,
+            "storage_order": "ColMajor",
+            // The data field contains the x, y and z
+            // coordinates of the speaker on the red alliance
+            "data": [8.0645, 1.4435, 2.0705]
+        },
+        "theta": 0.0
+    },
+    "blue_alliance": {
+        "pos": {
+            "rows": 3,
+            "cols": 1,
+            "storage_order": "ColMajor",
+            // The data field contains the x, y and z
+            // coordinates of the speaker on the blue alliance
+            "data": [-8.0645, 1.4435, 2.0705]
+        },
+        "theta": 0.0
+    }
+  },
+  "altitude_loading_position": 0.02,
+  "turret_loading_position": 0.58,
+  "catapult_return_position": 0.0,
+  "min_altitude_shooting_angle": 0.55,
+  "max_altitude_shooting_angle": 0.89,
+  "retention_roller_voltages": {
+    "retaining": 1.5,
+    "spitting": 6.0
+  },
+  // TODO(Filip): Update the speaker and amp shooter setpoints
+  "shooter_speaker_set_point": {
+    "turret_position": 0.0,
+    "altitude_position": 0.75,
+    "shot_velocity": 0.0
+  },
+  "shooter_podium_set_point":{
+    "turret_position": 0.0,
+    "altitude_position": 0.0,
+    "shot_velocity": 0.0
+  },
+  "extend_set_points": {
+    "trap": 0.46,
+    "amp": 0.2,
+    "catapult": 0.017,
+    "retracted": 0.017
+  },
+  "turret_avoid_extend_collision_position": 0.0
 }
diff --git a/y2024/constants/constants.fbs b/y2024/constants/constants.fbs
index 40d609f..31e2057 100644
--- a/y2024/constants/constants.fbs
+++ b/y2024/constants/constants.fbs
@@ -2,6 +2,7 @@
 include "frc971/vision/target_map.fbs";
 include "frc971/control_loops/profiled_subsystem.fbs";
 include "frc971/zeroing/constants.fbs";
+include "frc971/math/matrix.fbs";
 include "frc971/control_loops/drivetrain/drivetrain_config.fbs";
 
 namespace y2024;
@@ -12,7 +13,15 @@
 
 table ShotParams {
     shot_velocity: double (id: 0);
-    shot_angle: double (id: 1);
+
+    // Angle of the altitude
+    shot_altitude_angle: double (id: 1);
+
+    // Angle of the catapult
+    shot_catapult_angle: double (id: 2);
+
+    // Speed over ground to use for shooting on the fly
+    shot_speed_over_ground: double (id: 3);
 }
 
 table InterpolationTablePoint {
@@ -42,7 +51,7 @@
 // and retracted, which represents meters for when ClimberGoal is RETRACT
 table ClimberSetPoints {
   full_extend:double (id: 0);
-  half_extend:double (id: 1);
+  stowed:double (id: 1);
   retract:double (id: 2);
 }
 
@@ -63,17 +72,79 @@
   drivetrain_stator_current_limit:double (id: 7);
   climber_supply_current_limit:double (id: 8);
   climber_stator_current_limit:double (id: 9);
+  extend_supply_current_limit:double (id: 10);
+  extend_stator_current_limit:double (id: 11);
+  extend_roller_supply_current_limit:double (id: 12);
+  extend_roller_stator_current_limit:double (id: 13);
+  turret_supply_current_limit:double (id: 14);
+  turret_stator_current_limit:double (id: 15);
+  altitude_supply_current_limit:double (id: 16);
+  altitude_stator_current_limit:double (id: 17);
+  retention_roller_supply_current_limit:double (id: 18);
+  retention_roller_stator_current_limit:double (id: 19);
+  slower_retention_roller_stator_current_limit:double (id: 20);
+  shooting_retention_roller_stator_current_limit:double (id: 23);
+  catapult_supply_current_limit:double (id: 21);
+  catapult_stator_current_limit:double (id: 22);
 }
 
 table TransferRollerVoltages {
+  // Voltage to apply while intaking the game piece.
   transfer_in:double (id: 0);
+  // Voltage to apply while spitting the game piece.
   transfer_out:double (id: 1);
+  // Voltage to apply while the extend is moving and dragging the
+  // game piece out of the rollers.
+  extend_moving:double (id: 2);
+}
+
+table ExtendRollerVoltages {
+  scoring:double (id: 0);
+  reversing:double (id: 1);
+}
+
+// Extend positions
+table ExtendSetPoints {
+  // The position which lets us score on the trap.
+  trap:double (id: 0);
+  // The position which lets us score on the amp.
+  amp:double (id: 1);
+  // The position which lets us transfer into the catapult.
+  catapult:double (id: 2);
+  // The position near 0 where we are ready to intake a note.
+  retracted:double (id: 3);
 }
 
 table RobotConstants {
-  intake_constants:PotAndAbsEncoderConstants (id: 0);
+  intake_constants:frc971.zeroing.AbsoluteEncoderZeroingConstants (id: 0);
   climber_constants:PotAndAbsEncoderConstants (id: 1);
+  catapult_constants:PotAndAbsEncoderConstants (id: 2);
+  altitude_constants:PotAndAbsEncoderConstants (id: 3);
+  turret_constants:PotAndAbsEncoderConstants (id: 4);
+  extend_constants:PotAndAbsEncoderConstants (id: 5);
+}
 
+table ShooterSetPoint {
+  turret_position: double (id: 0);
+  altitude_position: double (id: 1);
+  shot_velocity:double (id: 2);
+}
+
+table Pose {
+  // Pos is a 3x1 matrix which contains the (x, y, z) component of the Pose.
+  pos: frc971.fbs.Matrix (id: 0);
+  theta: double (id: 1);
+}
+
+table ShooterTargets {
+  // The Pose of the red and blue alliance speakers we are aiming at.
+  red_alliance: Pose (id: 0);
+  blue_alliance: Pose (id: 1);
+}
+
+table RetentionRollerVoltages {
+  retaining:double (id: 0);
+  spitting:double (id: 1);
 }
 
 // Common table for constants unrelated to the robot
@@ -88,12 +159,30 @@
   transfer_roller_voltages:TransferRollerVoltages (id: 7);
   climber:frc971.control_loops.StaticZeroingSingleDOFProfiledSubsystemCommonParams (id: 8);
   climber_set_points:ClimberSetPoints (id: 9);
+  turret_loading_position: double (id: 10);
+  catapult_return_position: double (id: 11);
+  catapult:frc971.control_loops.StaticZeroingSingleDOFProfiledSubsystemCommonParams (id: 12);
+  altitude:frc971.control_loops.StaticZeroingSingleDOFProfiledSubsystemCommonParams (id: 13);
+  turret:frc971.control_loops.StaticZeroingSingleDOFProfiledSubsystemCommonParams (id: 14);
+  extend:frc971.control_loops.StaticZeroingSingleDOFProfiledSubsystemCommonParams (id: 15);
+  extend_roller_voltages:ExtendRollerVoltages (id: 16);
+  shooter_targets:ShooterTargets (id: 17);
+  altitude_loading_position: double (id: 18);
+  retention_roller_voltages:RetentionRollerVoltages (id: 19);
+  min_altitude_shooting_angle: double (id: 20);
+  max_altitude_shooting_angle: double (id: 25);
+  shooter_speaker_set_point: ShooterSetPoint (id: 21);
+  shooter_podium_set_point: ShooterSetPoint (id: 22);
+    extend_set_points:ExtendSetPoints (id: 23);
+  // The position to move the turret to when avoiding collision
+  // with the extend when the extend is moving to amp/trap position.
+  turret_avoid_extend_collision_position: double (id: 24);
 }
 
 table Constants {
-  robot:RobotConstants (id: 0);
-  common:Common (id: 1);
-  cameras:[CameraConfiguration] (id: 2);
+  cameras:[CameraConfiguration] (id: 0);
+  robot:RobotConstants (id: 1);
+  common:Common (id: 2);
 }
 
 root_type Constants;
diff --git a/y2024/constants/constants_formatter.cc b/y2024/constants/constants_formatter.cc
new file mode 100644
index 0000000..d857407
--- /dev/null
+++ b/y2024/constants/constants_formatter.cc
@@ -0,0 +1,26 @@
+#include "glog/logging.h"
+
+#include "aos/flatbuffers.h"
+#include "aos/init.h"
+#include "aos/json_to_flatbuffer.h"
+#include "aos/util/file.h"
+#include "y2024/constants/constants_list_generated.h"
+
+int main(int argc, char **argv) {
+  ::aos::InitGoogle(&argc, &argv);
+
+  CHECK(argc == 3) << ": Expected input and output json files to be passed in.";
+
+  aos::FlatbufferDetachedBuffer<y2024::ConstantsList> constants =
+      aos::JsonFileToFlatbuffer<y2024::ConstantsList>(argv[1]);
+
+  // Make sure the file is valid json before we output a formatted version.
+  CHECK(constants.message().constants() != nullptr)
+      << ": Failed to parse " << std::string(argv[2]);
+
+  aos::util::WriteStringToFileOrDie(
+      std::string(argv[2]),
+      aos::FlatbufferToJson(constants, {.multi_line = true}));
+
+  return 0;
+}
diff --git a/y2024/constants/constants_validator_test.cc b/y2024/constants/constants_validator_test.cc
deleted file mode 100644
index cd478a8..0000000
--- a/y2024/constants/constants_validator_test.cc
+++ /dev/null
@@ -1,17 +0,0 @@
-#include "glog/logging.h"
-#include "gtest/gtest.h"
-
-#include "aos/json_to_flatbuffer.h"
-#include "y2024/constants/constants_list_generated.h"
-
-namespace y2024::constants::testing {
-class ConstantsValidatorTest : public ::testing::Test {};
-
-TEST_F(ConstantsValidatorTest, CheckConstants) {
-  CHECK_NOTNULL(aos::JsonFileToFlatbuffer<y2024::ConstantsList>(
-                    "y2024/constants/constants.json")
-                    .message()
-                    .constants());
-}
-
-}  // namespace y2024::constants::testing
diff --git a/y2024/constants/test_data/calibration_cam-1.json b/y2024/constants/test_data/calibration_cam-1.json
new file mode 100644
index 0000000..2d81347
--- /dev/null
+++ b/y2024/constants/test_data/calibration_cam-1.json
@@ -0,0 +1,46 @@
+{
+  "node_name": "orin1",
+  "camera_number": 0,
+  "team_number": 7971,
+  "intrinsics": [
+    893.759521,
+    0,
+    645.470764,
+    0,
+    893.222351,
+    388.150269,
+    0,
+    0,
+    1
+  ],
+  "fixed_extrinsics": {
+    "data": [
+      0.0,
+      0.0,
+      1.0,
+      1.0,
+
+      -1.0,
+      0.0,
+      0.0,
+      0.0,
+
+      0.0,
+      -1.0,
+      0.0,
+      0.0,
+
+      0.0,
+      0.0,
+      0.0,
+      1.0
+    ]
+  },
+  "dist_coeffs": [
+    -0.44902,
+    0.248409,
+    -0.000537,
+    -0.000112,
+    -0.076989
+  ]
+}
diff --git a/y2024/constants/test_data/calibration_cam-2.json b/y2024/constants/test_data/calibration_cam-2.json
new file mode 100644
index 0000000..1128799
--- /dev/null
+++ b/y2024/constants/test_data/calibration_cam-2.json
@@ -0,0 +1,46 @@
+{
+  "node_name": "orin1",
+  "camera_number": 1,
+  "team_number": 7971,
+  "intrinsics": [
+    893.759521,
+    0,
+    645.470764,
+    0,
+    893.222351,
+    388.150269,
+    0,
+    0,
+    1
+  ],
+  "fixed_extrinsics": {
+    "data": [
+      1.0,
+      0.0,
+      0.0,
+      1.0,
+
+      0.0,
+      0.0,
+      -1.0,
+      0.0,
+
+      0.0,
+      1.0,
+      0.0,
+      0.0,
+
+      0.0,
+      0.0,
+      0.0,
+      1.0
+    ]
+  },
+  "dist_coeffs": [
+    -0.44902,
+    0.248409,
+    -0.000537,
+    -0.000112,
+    -0.076989
+  ]
+}
diff --git a/y2024/constants/test_data/calibration_cam-3.json b/y2024/constants/test_data/calibration_cam-3.json
new file mode 100644
index 0000000..16e67ec
--- /dev/null
+++ b/y2024/constants/test_data/calibration_cam-3.json
@@ -0,0 +1,46 @@
+{
+  "node_name": "imu",
+  "camera_number": 0,
+  "team_number": 7971,
+  "intrinsics": [
+    893.759521,
+    0,
+    645.470764,
+    0,
+    893.222351,
+    388.150269,
+    0,
+    0,
+    1
+  ],
+  "fixed_extrinsics": {
+    "data": [
+      0.0,
+      1.0,
+      0.0,
+      1.0,
+
+      0.0,
+      0.0,
+      -1.0,
+      0.0,
+
+      -1.0,
+      0.0,
+      0.0,
+      0.0,
+
+      0.0,
+      0.0,
+      0.0,
+      1.0
+    ]
+  },
+  "dist_coeffs": [
+    -0.44902,
+    0.248409,
+    -0.000537,
+    -0.000112,
+    -0.076989
+  ]
+}
diff --git a/y2024/constants/test_data/calibration_cam-4.json b/y2024/constants/test_data/calibration_cam-4.json
new file mode 100644
index 0000000..1e5b623
--- /dev/null
+++ b/y2024/constants/test_data/calibration_cam-4.json
@@ -0,0 +1,46 @@
+{
+  "node_name": "imu",
+  "camera_number": 1,
+  "team_number": 7971,
+  "intrinsics": [
+    893.759521,
+    0,
+    645.470764,
+    0,
+    893.222351,
+    388.150269,
+    0,
+    0,
+    1
+  ],
+  "fixed_extrinsics": {
+    "data": [
+      -1.0,
+      0.0,
+      0.0,
+      1.0,
+
+      0.0,
+      0.0,
+      -1.0,
+      0.0,
+
+      0.0,
+      -1.0,
+      0.0,
+      0.0,
+
+      0.0,
+      0.0,
+      0.0,
+      1.0
+    ]
+  },
+  "dist_coeffs": [
+    -0.44902,
+    0.248409,
+    -0.000537,
+    -0.000112,
+    -0.076989
+  ]
+}
diff --git a/y2024/constants/test_data/test_team.json b/y2024/constants/test_data/test_team.json
index 9e6bafb..b717224 100644
--- a/y2024/constants/test_data/test_team.json
+++ b/y2024/constants/test_data/test_team.json
@@ -1,17 +1,32 @@
 {% from 'y2024/constants/common.jinja2' import intake_pivot_zero %}
 {% from 'y2024/constants/common.jinja2' import climber_zero %}
+{% from 'y2024/constants/common.jinja2' import catapult_zero %}
+{% from 'y2024/constants/common.jinja2' import altitude_zero %}
+{% from 'y2024/constants/common.jinja2' import turret_zero %}
+{% from 'y2024/constants/common.jinja2' import extend_zero %}
 
 {
-  "robot": {
-    "intake_constants": {
-      {% set _ = intake_pivot_zero.update(
-          {
-              "measured_absolute_position" : 0.0
-          }
-      ) %}
-      "zeroing_constants": {{ intake_pivot_zero | tojson(indent=2)}},
-      "potentiometer_offset": 0.0
+  "cameras": [
+    {
+      "calibration": {% include 'y2024/constants/test_data/calibration_cam-1.json' %}
     },
+    {
+      "calibration": {% include 'y2024/constants/test_data/calibration_cam-2.json' %}
+    },
+    {
+      "calibration": {% include 'y2024/constants/test_data/calibration_cam-3.json' %}
+    },
+    {
+      "calibration": {% include 'y2024/constants/test_data/calibration_cam-4.json' %}
+    }
+  ],
+  "robot": {
+    {% set _ = intake_pivot_zero.update(
+      {
+          "measured_absolute_position" : 0.0
+      }
+    ) %}
+    "intake_constants":  {{ intake_pivot_zero | tojson(indent=2)}},
     "climber_constants": {
       {% set _ = climber_zero.update(
           {
@@ -20,6 +35,42 @@
       ) %}
       "zeroing_constants": {{ climber_zero | tojson(indent=2)}},
       "potentiometer_offset": 0.0
+    },
+    "catapult_constants": {
+      {% set _ = catapult_zero.update(
+          {
+              "measured_absolute_position" : 0.0
+          }
+      ) %}
+      "zeroing_constants": {{ catapult_zero | tojson(indent=2)}},
+      "potentiometer_offset": 0.0
+    },
+    "altitude_constants": {
+      {% set _ = altitude_zero.update(
+          {
+              "measured_absolute_position" : 0.0
+          }
+      ) %}
+      "zeroing_constants": {{ altitude_zero | tojson(indent=2)}},
+      "potentiometer_offset": 0.0
+    },
+    "turret_constants": {
+      {% set _ = turret_zero.update(
+          {
+              "measured_absolute_position" : 0.0
+          }
+      ) %}
+      "zeroing_constants": {{ turret_zero | tojson(indent=2)}},
+      "potentiometer_offset": 0.0
+    },
+    "extend_constants": {
+      {% set _ = extend_zero.update(
+          {
+              "measured_absolute_position" : 0.0
+          }
+      ) %}
+      "zeroing_constants": {{ extend_zero | tojson(indent=2)}},
+      "potentiometer_offset": 0.0
     }
   },
   {% include 'y2024/constants/common.json' %}
diff --git a/y2024/constants/validator.bzl b/y2024/constants/validator.bzl
new file mode 100644
index 0000000..6121f58
--- /dev/null
+++ b/y2024/constants/validator.bzl
@@ -0,0 +1,13 @@
+load("@aspect_bazel_lib//lib:run_binary.bzl", "run_binary")
+
+# Validates the constants.json file and outputs a formatted version.
+# TODO(max): Make this generic/template it out into frc971
+def constants_json(name, src, out):
+    run_binary(
+        name = name,
+        tool = "//y2024/constants:constants_formatter",
+        srcs = [src],
+        outs = [out],
+        args = ["$(location %s)" % (src)] + ["$(location %s)" % (out)],
+        visibility = ["//visibility:public"],
+    )
diff --git a/y2024/control_loops/drivetrain/drivetrain_main.cc b/y2024/control_loops/drivetrain/drivetrain_main.cc
index e0b2eb7..82629de 100644
--- a/y2024/control_loops/drivetrain/drivetrain_main.cc
+++ b/y2024/control_loops/drivetrain/drivetrain_main.cc
@@ -18,14 +18,15 @@
   frc971::constants::WaitForConstants<y2024::Constants>(&config.message());
 
   aos::ShmEventLoop event_loop(&config.message());
+  const auto drivetrain_config =
+      ::y2024::control_loops::drivetrain::GetDrivetrainConfig(&event_loop);
+
   std::unique_ptr<::frc971::control_loops::drivetrain::PuppetLocalizer>
       localizer = std::make_unique<
           ::frc971::control_loops::drivetrain::PuppetLocalizer>(
-          &event_loop,
-          ::y2024::control_loops::drivetrain::GetDrivetrainConfig(&event_loop));
+          &event_loop, drivetrain_config);
   std::unique_ptr<DrivetrainLoop> drivetrain = std::make_unique<DrivetrainLoop>(
-      y2024::control_loops::drivetrain::GetDrivetrainConfig(&event_loop),
-      &event_loop, localizer.get());
+      drivetrain_config, &event_loop, localizer.get());
 
   event_loop.Run();
 
diff --git a/y2024/control_loops/python/BUILD b/y2024/control_loops/python/BUILD
index da80d87..9cbfcf8 100644
--- a/y2024/control_loops/python/BUILD
+++ b/y2024/control_loops/python/BUILD
@@ -77,6 +77,67 @@
     ],
 )
 
+py_binary(
+    name = "catapult",
+    srcs = [
+        "catapult.py",
+    ],
+    legacy_create_init = False,
+    target_compatible_with = ["@platforms//cpu:x86_64"],
+    deps = [
+        "//frc971/control_loops/python:angular_system",
+        "//frc971/control_loops/python:controls",
+    ],
+)
+
+py_binary(
+    name = "turret",
+    srcs = [
+        "turret.py",
+    ],
+    legacy_create_init = False,
+    target_compatible_with = ["@platforms//cpu:x86_64"],
+    deps = [
+        ":python_init",
+        "//frc971/control_loops/python:angular_system",
+        "//frc971/control_loops/python:controls",
+        "@pip//glog",
+        "@pip//python_gflags",
+    ],
+)
+
+py_binary(
+    name = "altitude",
+    srcs = [
+        "altitude.py",
+    ],
+    legacy_create_init = False,
+    target_compatible_with = ["@platforms//cpu:x86_64"],
+    deps = [
+        ":python_init",
+        "//frc971/control_loops/python:angular_system",
+        "//frc971/control_loops/python:controls",
+        "@pip//glog",
+        "@pip//python_gflags",
+    ],
+)
+
+py_binary(
+    name = "extend",
+    srcs = [
+        "extend.py",
+    ],
+    legacy_create_init = False,
+    target_compatible_with = ["@platforms//cpu:x86_64"],
+    deps = [
+        ":python_init",
+        "//frc971/control_loops/python:controls",
+        "//frc971/control_loops/python:linear_system",
+        "@pip//glog",
+        "@pip//python_gflags",
+    ],
+)
+
 py_library(
     name = "python_init",
     srcs = ["__init__.py"],
diff --git a/y2024/control_loops/python/altitude.py b/y2024/control_loops/python/altitude.py
new file mode 100644
index 0000000..d302b79
--- /dev/null
+++ b/y2024/control_loops/python/altitude.py
@@ -0,0 +1,58 @@
+#!/usr/bin/python3
+
+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
+
+gflags.DEFINE_bool('hybrid', False, 'If true, make it hybrid.')
+
+kAltitude = angular_system.AngularSystemParams(
+    name='Altitude',
+    motor=control_loop.KrakenFOC(),
+    G=(16.0 / 60.0) * (16.0 / 162.0),
+    # 4340 in^ lb
+    J=1.27,
+    q_pos=0.60,
+    q_vel=8.0,
+    kalman_q_pos=0.12,
+    kalman_q_vel=2.0,
+    kalman_q_voltage=2.0,
+    kalman_r_position=0.05,
+    radius=10.5 * 0.0254)
+
+
+def main(argv):
+    if FLAGS.plot:
+        R = numpy.matrix([[numpy.pi / 2.0], [0.0]])
+        angular_system.PlotKick(kAltitude, R)
+        angular_system.PlotMotion(kAltitude, R)
+        return
+
+    # Write the generated constants out to a file.
+    if len(argv) != 7:
+        glog.fatal(
+            'Expected .h file name and .cc file name for the intake and integral intake.'
+        )
+    else:
+        namespaces = ['y2024', 'control_loops', 'superstructure', 'altitude']
+        angular_system.WriteAngularSystem(kAltitude, argv[1:4], argv[4:7],
+                                          namespaces)
+
+
+if __name__ == '__main__':
+    argv = FLAGS(sys.argv)
+    glog.init()
+    sys.exit(main(argv))
diff --git a/y2024/control_loops/python/catapult.py b/y2024/control_loops/python/catapult.py
new file mode 100644
index 0000000..49c6305
--- /dev/null
+++ b/y2024/control_loops/python/catapult.py
@@ -0,0 +1,85 @@
+#!/usr/bin/python3
+
+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
+
+gflags.DEFINE_bool('hybrid', False, 'If true, make it hybrid.')
+
+
+def AddResistance(motor, resistance):
+    motor.resistance += resistance
+    return motor
+
+
+kCatapultWithGamePiece = angular_system.AngularSystemParams(
+    name='Catapult',
+    # Add the battery series resistance to make it better match.
+    motor=AddResistance(control_loop.NMotor(control_loop.KrakenFOC(), 2),
+                        0.00),
+    G=(14.0 / 60.0) * (12.0 / 24.0),
+    # 208.7328 in^2 lb
+    J=0.065 + 0.04,
+    q_pos=0.80,
+    q_vel=15.0,
+    kalman_q_pos=0.12,
+    kalman_q_vel=2.0,
+    kalman_q_voltage=0.7,
+    kalman_r_position=0.05,
+    radius=12 * 0.0254,
+    delayed_u=1)
+
+kCatapultWithoutGamePiece = angular_system.AngularSystemParams(
+    name='Catapult',
+    # Add the battery series resistance to make it better match.
+    motor=AddResistance(control_loop.NMotor(control_loop.KrakenFOC(), 2),
+                        0.00),
+    G=(14.0 / 60.0) * (12.0 / 24.0),
+    # 135.2928 in^2 lb
+    J=0.06,
+    q_pos=0.80,
+    q_vel=15.0,
+    kalman_q_pos=0.12,
+    kalman_q_vel=2.0,
+    kalman_q_voltage=0.7,
+    kalman_r_position=0.05,
+    radius=12 * 0.0254,
+    delayed_u=1)
+
+
+def main(argv):
+    if FLAGS.plot:
+        R = numpy.matrix([[numpy.pi / 2.0], [0.0]])
+        angular_system.PlotKick(kCatapultWithGamePiece, R)
+        angular_system.PlotMotion(kCatapultWithGamePiece, R)
+        return
+
+    # Write the generated constants out to a file.
+    if len(argv) != 7:
+        glog.fatal(
+            'Expected .h file name and .cc file name for the intake and integral intake.'
+        )
+    else:
+        namespaces = ['y2024', 'control_loops', 'superstructure', 'catapult']
+        angular_system.WriteAngularSystem(
+            [kCatapultWithoutGamePiece, kCatapultWithGamePiece], argv[1:4],
+            argv[4:7], namespaces)
+
+
+if __name__ == '__main__':
+    argv = FLAGS(sys.argv)
+    glog.init()
+    sys.exit(main(argv))
diff --git a/y2024/control_loops/python/climber.py b/y2024/control_loops/python/climber.py
index 10aeff9..997cf28 100644
--- a/y2024/control_loops/python/climber.py
+++ b/y2024/control_loops/python/climber.py
@@ -14,29 +14,30 @@
 except gflags.DuplicateFlagError:
     pass
 
-# TODO(Filip): Update information the climber when design is finalized.
 kClimber = linear_system.LinearSystemParams(
     name='Climber',
-    motor=control_loop.Falcon(),
-    G=(1.0 / 4.0) * (1.0 / 3.0) * (1.0 / 3.0),
-    radius=22 * 0.25 / numpy.pi / 2.0 * 0.0254,
+    motor=control_loop.KrakenFOC(),
+    G=(8. / 60.) * (16. / 60.),
+    radius=16 * 0.25 / numpy.pi / 2.0 * 0.0254,
     mass=2.0,
-    q_pos=0.10,
-    q_vel=1.35,
+    q_pos=0.03,
+    q_vel=2.,
     kalman_q_pos=0.12,
     kalman_q_vel=2.00,
     kalman_q_voltage=35.0,
-    kalman_r_position=0.05)
+    kalman_r_position=0.05,
+)
 
 
 def main(argv):
     if FLAGS.plot:
-        R = numpy.matrix([[0.2], [0.0]])
-        linear_system.PlotKick(kClimber, R, plant_params=kClimber)
+        R = numpy.matrix([[0.4], [0.0]])
         linear_system.PlotMotion(kClimber,
                                  R,
                                  max_velocity=5.0,
                                  plant_params=kClimber)
+        linear_system.PlotKick(kClimber, R, plant_params=kClimber)
+        return
 
     # Write the generated constants out to a file.
     if len(argv) != 7:
@@ -45,7 +46,8 @@
         )
     else:
         namespaces = ['y2024', 'control_loops', 'superstructure', 'climber']
-    linear_system.WriteLinearSystem(kClimber, argv[1:4], argv[4:7], namespaces)
+        linear_system.WriteLinearSystem(kClimber, argv[1:4], argv[4:7],
+                                        namespaces)
 
 
 if __name__ == '__main__':
diff --git a/y2024/control_loops/python/extend.py b/y2024/control_loops/python/extend.py
new file mode 100644
index 0000000..f95c995
--- /dev/null
+++ b/y2024/control_loops/python/extend.py
@@ -0,0 +1,61 @@
+#!/usr/bin/python3
+
+from aos.util.trapezoid_profile import TrapezoidProfile
+from frc971.control_loops.python import control_loop
+from frc971.control_loops.python import linear_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
+
+gflags.DEFINE_bool('hybrid', False, 'If true, make it hybrid.')
+
+kExtend = linear_system.LinearSystemParams(
+    name='Extend',
+    motor=control_loop.KrakenFOC(),
+    G=(14. / 60.) * (32. / 48.),
+    radius=36 * 0.005 / numpy.pi / 2.0,
+    mass=5.0,
+    q_pos=0.20,
+    q_vel=80.0,
+    kalman_q_pos=0.12,
+    kalman_q_vel=2.0,
+    kalman_q_voltage=8.0,
+    kalman_r_position=0.05,
+)
+
+
+def main(argv):
+    if FLAGS.plot:
+        R = numpy.matrix([[0.4], [0.0]])
+        linear_system.PlotKick(kExtend, R)
+        linear_system.PlotMotion(kExtend,
+                                 R,
+                                 max_velocity=2.0,
+                                 max_acceleration=15.0)
+        return
+
+    # Write the generated constants out to a file.
+    if len(argv) != 7:
+        glog.fatal(
+            'Expected .h file name and .cc file name for the extend pivot and integral extend pivot.'
+        )
+    else:
+        namespaces = ['y2024', 'control_loops', 'superstructure', 'extend']
+        linear_system.WriteLinearSystem(kExtend, argv[1:4], argv[4:7],
+                                        namespaces)
+
+
+if __name__ == '__main__':
+    argv = FLAGS(sys.argv)
+    glog.init()
+    sys.exit(main(argv))
diff --git a/y2024/control_loops/python/intake_pivot.py b/y2024/control_loops/python/intake_pivot.py
index f32aa8b..5ea5c3f 100644
--- a/y2024/control_loops/python/intake_pivot.py
+++ b/y2024/control_loops/python/intake_pivot.py
@@ -20,15 +20,16 @@
 kIntakePivot = angular_system.AngularSystemParams(
     name='IntakePivot',
     motor=control_loop.KrakenFOC(),
-    G=(16.0 / 60.0) * (18.0 / 62.0) * (18.0 / 62.0) * (15.0 / 24.0),
-    J=0.34,  # Borrowed from 2022, 0.035 seems too low
-    q_pos=0.40,
-    q_vel=20.0,
+    G=(16. / 60.) * (18. / 62.) * (18. / 62.) * (15. / 24.),
+    J=0.4,
+    q_pos=1.0,
+    q_vel=800.0,
     kalman_q_pos=0.12,
     kalman_q_vel=2.0,
-    kalman_q_voltage=4.0,
+    kalman_q_voltage=1.0,
     kalman_r_position=0.05,
-    radius=6.85 * 0.0254)
+    radius=6.85 * 0.0254,
+    enable_voltage_error=False)
 
 
 def main(argv):
@@ -52,4 +53,4 @@
 if __name__ == '__main__':
     argv = FLAGS(sys.argv)
     glog.init()
-    sys.exit(main(argv))
\ No newline at end of file
+    sys.exit(main(argv))
diff --git a/y2024/control_loops/python/turret.py b/y2024/control_loops/python/turret.py
new file mode 100644
index 0000000..41cc48a
--- /dev/null
+++ b/y2024/control_loops/python/turret.py
@@ -0,0 +1,55 @@
+#!/usr/bin/python3
+
+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
+
+kTurret = angular_system.AngularSystemParams(
+    name='Turret',
+    motor=control_loop.KrakenFOC(),
+    G=(14.0 / 60.0) * (28.0 / 48.0) * (22.0 / 100.0),
+    # 1305 in^2 lb
+    J=0.4,
+    q_pos=0.60,
+    q_vel=10.0,
+    kalman_q_pos=0.12,
+    kalman_q_vel=2.0,
+    kalman_q_voltage=2.0,
+    kalman_r_position=0.05,
+    radius=24 * 0.0254)
+
+
+def main(argv):
+    if FLAGS.plot:
+        R = numpy.matrix([[numpy.pi], [0.0]])
+        angular_system.PlotKick(kTurret, R)
+        angular_system.PlotMotion(kTurret, R)
+
+    # Write the generated constants out to a file.
+    if len(argv) != 7:
+        glog.fatal(
+            'Expected .h file name and .cc file name for the turret and integral turret.'
+        )
+    else:
+        namespaces = ['y2024', 'control_loops', 'superstructure', 'turret']
+        angular_system.WriteAngularSystem(kTurret, argv[1:4], argv[4:7],
+                                          namespaces)
+
+
+if __name__ == '__main__':
+    argv = FLAGS(sys.argv)
+    glog.init()
+    sys.exit(main(argv))
diff --git a/y2024/control_loops/superstructure/BUILD b/y2024/control_loops/superstructure/BUILD
index 28e4d00..6150caa 100644
--- a/y2024/control_loops/superstructure/BUILD
+++ b/y2024/control_loops/superstructure/BUILD
@@ -81,6 +81,8 @@
     data = [
     ],
     deps = [
+        ":collision_avoidance_lib",
+        ":shooter",
         ":superstructure_goal_fbs",
         ":superstructure_output_fbs",
         ":superstructure_position_fbs",
@@ -112,6 +114,36 @@
     ],
 )
 
+cc_library(
+    name = "collision_avoidance_lib",
+    srcs = ["collision_avoidance.cc"],
+    hdrs = ["collision_avoidance.h"],
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = [
+        ":superstructure_goal_fbs",
+        ":superstructure_status_fbs",
+        "//frc971/control_loops:control_loops_fbs",
+        "//frc971/control_loops:profiled_subsystem_fbs",
+        "//frc971/control_loops:static_zeroing_single_dof_profiled_subsystem",
+        "@com_github_google_glog//:glog",
+        "@com_google_absl//absl/functional:bind_front",
+    ],
+)
+
+cc_test(
+    name = "collision_avoidance_test",
+    srcs = ["collision_avoidance_test.cc"],
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = [
+        ":collision_avoidance_lib",
+        ":superstructure_goal_fbs",
+        ":superstructure_status_fbs",
+        "//aos:flatbuffers",
+        "//aos:math",
+        "//aos/testing:googletest",
+    ],
+)
+
 cc_test(
     name = "superstructure_lib_test",
     srcs = [
@@ -140,6 +172,53 @@
     ],
 )
 
+cc_library(
+    name = "shooter",
+    srcs = [
+        "shooter.cc",
+    ],
+    hdrs = [
+        "shooter.h",
+    ],
+    deps = [
+        ":aiming",
+        ":collision_avoidance_lib",
+        ":superstructure_can_position_fbs",
+        ":superstructure_goal_fbs",
+        ":superstructure_position_fbs",
+        ":superstructure_status_fbs",
+        "//frc971/control_loops:static_zeroing_single_dof_profiled_subsystem",
+        "//frc971/control_loops/catapult",
+        "//frc971/control_loops/catapult:catapult_goal_fbs",
+        "//frc971/shooter_interpolation:interpolation",
+        "//frc971/zeroing:pot_and_absolute_encoder",
+        "//y2024:constants",
+        "//y2024/constants:constants_fbs",
+        "//y2024/control_loops/superstructure/altitude:altitude_plants",
+        "//y2024/control_loops/superstructure/catapult:catapult_plants",
+        "//y2024/control_loops/superstructure/turret:turret_plants",
+    ],
+)
+
+cc_library(
+    name = "aiming",
+    srcs = [
+        "aiming.cc",
+    ],
+    hdrs = [
+        "aiming.h",
+    ],
+    deps = [
+        ":superstructure_status_fbs",
+        "//frc971/control_loops:static_zeroing_single_dof_profiled_subsystem",
+        "//frc971/control_loops/aiming",
+        "//frc971/control_loops/drivetrain:drivetrain_status_fbs",
+        "//y2024:constants",
+        "//y2024/constants:constants_fbs",
+        "//y2024/control_loops/drivetrain:drivetrain_base",
+    ],
+)
+
 cc_binary(
     name = "superstructure_replay",
     srcs = ["superstructure_replay.cc"],
diff --git a/y2024/control_loops/superstructure/aiming.cc b/y2024/control_loops/superstructure/aiming.cc
new file mode 100644
index 0000000..5bb1d19
--- /dev/null
+++ b/y2024/control_loops/superstructure/aiming.cc
@@ -0,0 +1,87 @@
+#include "y2024/control_loops/superstructure/aiming.h"
+
+#include "frc971/control_loops/aiming/aiming.h"
+#include "frc971/control_loops/pose.h"
+
+using frc971::control_loops::aiming::RobotState;
+using frc971::control_loops::aiming::ShotConfig;
+using frc971::control_loops::aiming::ShotMode;
+using y2024::control_loops::superstructure::Aimer;
+
+// When the turret is at 0 the note will be leaving the robot at PI.
+static constexpr double kTurretZeroOffset = M_PI;
+
+Aimer::Aimer(aos::EventLoop *event_loop,
+             const y2024::Constants *robot_constants)
+    : event_loop_(event_loop),
+      robot_constants_(CHECK_NOTNULL(robot_constants)),
+      drivetrain_config_(
+          frc971::control_loops::drivetrain::DrivetrainConfig<double>::
+              FromFlatbuffer(*robot_constants_->common()->drivetrain())),
+      interpolation_table_(
+          y2024::constants::Values::InterpolationTableFromFlatbuffer(
+              robot_constants_->common()->shooter_interpolation_table())),
+      joystick_state_fetcher_(
+          event_loop_->MakeFetcher<aos::JoystickState>("/aos")) {}
+
+void Aimer::Update(
+    const frc971::control_loops::drivetrain::Status *status,
+    frc971::control_loops::aiming::ShotMode shot_mode,
+    frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemGoalStatic
+        *turret_goal) {
+  const frc971::control_loops::Pose robot_pose({status->x(), status->y(), 0},
+                                               status->theta());
+  joystick_state_fetcher_.Fetch();
+  CHECK_NOTNULL(joystick_state_fetcher_.get());
+
+  aos::Alliance alliance = joystick_state_fetcher_->alliance();
+
+  const frc971::control_loops::Pose red_alliance_goal(
+      frc971::ToEigenOrDie<3, 1>(*robot_constants_->common()
+                                      ->shooter_targets()
+                                      ->red_alliance()
+                                      ->pos()),
+      robot_constants_->common()->shooter_targets()->red_alliance()->theta());
+
+  const frc971::control_loops::Pose blue_alliance_goal(
+      frc971::ToEigenOrDie<3, 1>(*robot_constants_->common()
+                                      ->shooter_targets()
+                                      ->blue_alliance()
+                                      ->pos()),
+      robot_constants_->common()->shooter_targets()->blue_alliance()->theta());
+
+  const frc971::control_loops::Pose goal =
+      alliance == aos::Alliance::kRed ? red_alliance_goal : blue_alliance_goal;
+
+  const Eigen::Vector2d linear_angular =
+      drivetrain_config_.Tlr_to_la() *
+      Eigen::Vector2d(status->estimated_left_velocity(),
+                      status->estimated_right_velocity());
+  const double xdot = linear_angular(0) * std::cos(status->theta());
+  const double ydot = linear_angular(0) * std::sin(status->theta());
+
+  // Use the previous shot distance to estimate the speed-over-ground of the
+  // note.
+  current_goal_ = frc971::control_loops::aiming::AimerGoal(
+      ShotConfig{goal, shot_mode,
+                 frc971::constants::Range::FromFlatbuffer(
+                     robot_constants_->common()->turret()->range()),
+                 interpolation_table_.Get(current_goal_.target_distance)
+                     .shot_speed_over_ground,
+                 /*wrap_mode=*/0.0, kTurretZeroOffset},
+      RobotState{
+          robot_pose, {xdot, ydot}, linear_angular(1), current_goal_.position});
+
+  turret_goal->set_unsafe_goal(current_goal_.position);
+  turret_goal->set_goal_velocity(current_goal_.velocity);
+}
+
+flatbuffers::Offset<AimerStatus> Aimer::PopulateStatus(
+    flatbuffers::FlatBufferBuilder *fbb) const {
+  AimerStatus::Builder builder(*fbb);
+  builder.add_turret_position(current_goal_.position);
+  builder.add_turret_velocity(current_goal_.velocity);
+  builder.add_target_distance(current_goal_.target_distance);
+  builder.add_shot_distance(DistanceToGoal());
+  return builder.Finish();
+}
diff --git a/y2024/control_loops/superstructure/aiming.h b/y2024/control_loops/superstructure/aiming.h
new file mode 100644
index 0000000..21d427a
--- /dev/null
+++ b/y2024/control_loops/superstructure/aiming.h
@@ -0,0 +1,51 @@
+#ifndef Y2024_CONTROL_LOOPS_SUPERSTRUCTURE_AIMING_H_
+#define Y2024_CONTROL_LOOPS_SUPERSTRUCTURE_AIMING_H_
+
+#include "frc971/control_loops/aiming/aiming.h"
+#include "frc971/control_loops/drivetrain/drivetrain_status_generated.h"
+#include "frc971/control_loops/pose.h"
+#include "frc971/control_loops/static_zeroing_single_dof_profiled_subsystem.h"
+#include "frc971/shooter_interpolation/interpolation.h"
+#include "y2024/constants.h"
+#include "y2024/constants/constants_generated.h"
+#include "y2024/control_loops/drivetrain/drivetrain_base.h"
+#include "y2024/control_loops/superstructure/superstructure_status_generated.h"
+
+using y2024::control_loops::superstructure::AimerStatus;
+
+namespace y2024::control_loops::superstructure {
+
+class Aimer {
+ public:
+  Aimer(aos::EventLoop *event_loop, const Constants *robot_constants);
+
+  void Update(
+      const frc971::control_loops::drivetrain::Status *status,
+      frc971::control_loops::aiming::ShotMode shot_mode,
+      frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemGoalStatic
+          *turret_goal);
+
+  double DistanceToGoal() const { return current_goal_.virtual_shot_distance; }
+
+  flatbuffers::Offset<AimerStatus> PopulateStatus(
+      flatbuffers::FlatBufferBuilder *fbb) const;
+
+ private:
+  aos::EventLoop *event_loop_;
+
+  const Constants *robot_constants_;
+
+  frc971::control_loops::drivetrain::DrivetrainConfig<double>
+      drivetrain_config_;
+
+  frc971::shooter_interpolation::InterpolationTable<
+      y2024::constants::Values::ShotParams>
+      interpolation_table_;
+
+  aos::Fetcher<aos::JoystickState> joystick_state_fetcher_;
+
+  frc971::control_loops::aiming::TurretGoal current_goal_;
+};
+
+}  // namespace y2024::control_loops::superstructure
+#endif  // Y2024_CONTROL_LOOPS_SUPERSTRUCTURE_TURRET_AIMING_H_
diff --git a/y2024/control_loops/superstructure/altitude/BUILD b/y2024/control_loops/superstructure/altitude/BUILD
new file mode 100644
index 0000000..71e2e3b
--- /dev/null
+++ b/y2024/control_loops/superstructure/altitude/BUILD
@@ -0,0 +1,42 @@
+package(default_visibility = ["//y2024:__subpackages__"])
+
+genrule(
+    name = "genrule_altitude",
+    outs = [
+        "altitude_plant.h",
+        "altitude_plant.cc",
+        "altitude_plant.json",
+        "integral_altitude_plant.h",
+        "integral_altitude_plant.cc",
+        "integral_altitude_plant.json",
+    ],
+    cmd = "$(location //y2024/control_loops/python:altitude) $(OUTS)",
+    target_compatible_with = ["@platforms//os:linux"],
+    tools = [
+        "//y2024/control_loops/python:altitude",
+    ],
+)
+
+cc_library(
+    name = "altitude_plants",
+    srcs = [
+        "altitude_plant.cc",
+        "integral_altitude_plant.cc",
+    ],
+    hdrs = [
+        "altitude_plant.h",
+        "integral_altitude_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",
+    ],
+)
+
+filegroup(
+    name = "altitude_json",
+    srcs = ["integral_altitude_plant.json"],
+    visibility = ["//visibility:public"],
+)
diff --git a/y2024/control_loops/superstructure/catapult/BUILD b/y2024/control_loops/superstructure/catapult/BUILD
new file mode 100644
index 0000000..a6025d8
--- /dev/null
+++ b/y2024/control_loops/superstructure/catapult/BUILD
@@ -0,0 +1,42 @@
+package(default_visibility = ["//y2024:__subpackages__"])
+
+genrule(
+    name = "genrule_catapult",
+    outs = [
+        "catapult_plant.h",
+        "catapult_plant.cc",
+        "catapult_plant.json",
+        "integral_catapult_plant.h",
+        "integral_catapult_plant.cc",
+        "integral_catapult_plant.json",
+    ],
+    cmd = "$(location //y2024/control_loops/python:catapult) $(OUTS)",
+    target_compatible_with = ["@platforms//os:linux"],
+    tools = [
+        "//y2024/control_loops/python:catapult",
+    ],
+)
+
+cc_library(
+    name = "catapult_plants",
+    srcs = [
+        "catapult_plant.cc",
+        "integral_catapult_plant.cc",
+    ],
+    hdrs = [
+        "catapult_plant.h",
+        "integral_catapult_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",
+    ],
+)
+
+filegroup(
+    name = "catapult_json",
+    srcs = ["integral_catapult_plant.json"],
+    visibility = ["//visibility:public"],
+)
diff --git a/y2024/control_loops/superstructure/collision_avoidance.cc b/y2024/control_loops/superstructure/collision_avoidance.cc
new file mode 100644
index 0000000..bd82fe0
--- /dev/null
+++ b/y2024/control_loops/superstructure/collision_avoidance.cc
@@ -0,0 +1,156 @@
+#include "y2024/control_loops/superstructure/collision_avoidance.h"
+
+#include <cmath>
+
+#include "absl/functional/bind_front.h"
+#include "glog/logging.h"
+
+namespace y2024::control_loops::superstructure {
+
+CollisionAvoidance::CollisionAvoidance() {
+  clear_min_intake_pivot_goal();
+  clear_max_intake_pivot_goal();
+  clear_min_turret_goal();
+  clear_max_turret_goal();
+  clear_min_extend_goal();
+  clear_max_extend_goal();
+}
+
+bool CollisionAvoidance::IsCollided(const CollisionAvoidance::Status &status) {
+  // Checks if intake front is collided.
+  if (TurretCollided(status.intake_pivot_position, status.turret_position,
+                     status.extend_position)) {
+    return true;
+  }
+
+  return false;
+}
+
+bool AngleInRange(double theta, double theta_min, double theta_max) {
+  return (
+      (theta >= theta_min && theta <= theta_max) ||
+      (theta_min > theta_max && (theta >= theta_min || theta <= theta_max)));
+}
+
+bool CollisionAvoidance::TurretCollided(double intake_position,
+                                        double turret_position,
+                                        double extend_position) {
+  // Checks if turret is in the collision area.
+  if (AngleInRange(turret_position, kMinCollisionZoneTurret,
+                   kMaxCollisionZoneTurret)) {
+    // Returns true if the intake is raised.
+    if (intake_position > kCollisionZoneIntake) {
+      return true;
+    }
+  }
+  return ExtendCollided(intake_position, turret_position, extend_position);
+}
+
+bool CollisionAvoidance::ExtendCollided(double /*intake_position*/,
+                                        double turret_position,
+                                        double extend_position) {
+  // Checks if turret is in the collision area.
+  if (!AngleInRange(turret_position, kSafeTurretExtendedPosition - kEpsTurret,
+                    kSafeTurretExtendedPosition + kEpsTurret)) {
+    // Returns true if the extend is raised.
+    if (extend_position > kMinCollisionZoneExtend) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+void CollisionAvoidance::UpdateGoal(const CollisionAvoidance::Status &status,
+                                    const double turret_goal_position,
+                                    const double extend_goal_position) {
+  // Start with our constraints being wide open.
+  clear_min_extend_goal();
+  clear_max_extend_goal();
+  clear_max_turret_goal();
+  clear_min_turret_goal();
+  clear_max_intake_pivot_goal();
+  clear_min_intake_pivot_goal();
+
+  const double intake_pivot_position = status.intake_pivot_position;
+  const double turret_position = status.turret_position;
+  const double extend_position = status.extend_position;
+
+  const double turret_goal = turret_goal_position;
+  const double extend_goal = extend_goal_position;
+
+  // Calculating the avoidance with the intake
+
+  CalculateAvoidance(intake_pivot_position, turret_position, extend_position,
+                     turret_goal, extend_goal);
+}
+
+void CollisionAvoidance::CalculateAvoidance(double intake_position,
+                                            double turret_position,
+                                            double extend_position,
+                                            double turret_goal,
+                                            double extend_goal) {
+  // If the turret goal is in a collison zone or moving through one, limit
+  // intake.
+  const bool turret_intake_pos_unsafe = AngleInRange(
+      turret_position, kMinCollisionZoneTurret, kMaxCollisionZoneTurret);
+  const bool turret_extend_pos_unsafe =
+      turret_position > kEpsTurret + kSafeTurretExtendedPosition ||
+      turret_position < -kEpsTurret + kSafeTurretExtendedPosition;
+
+  const bool extend_goal_unsafe =
+      extend_goal > kMinCollisionZoneExtend - kEpsExtend;
+  const bool extend_position_unsafe =
+      extend_position > kMinCollisionZoneExtend - kEpsExtend;
+
+  // OK, we are trying to move the extend, and need the turret to be at 0.
+  // Pretend that's the goal.
+  if (extend_goal_unsafe || extend_position_unsafe) {
+    turret_goal = kSafeTurretExtendedPosition;
+  }
+
+  const bool turret_moving_forward = (turret_goal > turret_position);
+
+  // Check if the closest angles are going to be passed
+  const bool turret_moving_past_intake =
+      ((turret_moving_forward && (turret_position <= kMaxCollisionZoneTurret &&
+                                  turret_goal >= kMinCollisionZoneTurret)) ||
+       (!turret_moving_forward && (turret_position >= kMinCollisionZoneTurret &&
+                                   turret_goal <= kMaxCollisionZoneTurret)));
+
+  if (turret_intake_pos_unsafe || turret_moving_past_intake) {
+    // If the turret is unsafe, limit the intake
+    update_max_intake_pivot_goal(kCollisionZoneIntake - kEpsIntake);
+
+    // If the intake is in the way, limit the turret until moved. Otherwise,
+    // let'errip!
+    if (!turret_intake_pos_unsafe && (intake_position > kCollisionZoneIntake)) {
+      if (turret_position <
+          (kMinCollisionZoneTurret + kMaxCollisionZoneTurret) / 2.) {
+        update_max_turret_goal(kMinCollisionZoneTurret - kEpsTurret);
+      } else {
+        update_min_turret_goal(kMaxCollisionZoneTurret + kEpsTurret);
+      }
+    }
+  }
+
+  // OK, the logic is pretty simple.  The turret needs to be at
+  // kSafeTurretExtendedPosition any time extend is > kMinCollisionZoneExtend.
+  //
+  // Extend can't go up if the turret isn't near 0.
+  if (turret_extend_pos_unsafe) {
+    update_max_extend_goal(kMinCollisionZoneExtend - kEpsExtend);
+  }
+
+  // Turret is bound to the safe position if extend wants to be, or is unsafe.
+  if (extend_goal_unsafe || extend_position_unsafe) {
+    // If the turret isn't allowed to go to 0, don't drive it there.
+    if (min_turret_goal() < kSafeTurretExtendedPosition &&
+        max_turret_goal() > kSafeTurretExtendedPosition) {
+      update_min_turret_goal(kSafeTurretExtendedPosition);
+      update_max_turret_goal(kSafeTurretExtendedPosition);
+    }
+  }
+}
+
+}  // namespace y2024::control_loops::superstructure
diff --git a/y2024/control_loops/superstructure/collision_avoidance.h b/y2024/control_loops/superstructure/collision_avoidance.h
new file mode 100644
index 0000000..be58b35
--- /dev/null
+++ b/y2024/control_loops/superstructure/collision_avoidance.h
@@ -0,0 +1,130 @@
+#ifndef Y2024_CONTROL_LOOPS_SUPERSTRUCTURE_COLLISION_AVOIDENCE_H_
+#define Y2024_CONTROL_LOOPS_SUPERSTRUCTURE_COLLISION_AVOIDENCE_H_
+
+#include <cmath>
+
+#include "frc971/control_loops/control_loops_generated.h"
+#include "frc971/control_loops/profiled_subsystem_generated.h"
+
+namespace y2024::control_loops::superstructure {
+
+// Checks if theta is between theta_min and theta_max. Expects all angles to be
+// wrapped from 0 to 2pi
+bool AngleInRange(double theta, double theta_min, double theta_max);
+
+// 1. Prevent the turret from moving if the intake is up
+// and prevent the back of the turret from colliding with the intake when it's
+// up.
+// 2. If the intake is up, drop it so it is not in the way
+// 3. Move the turret to the desired position.
+// 4. When the turret moves away, if the intake is down, move it back up.
+class CollisionAvoidance {
+ public:
+  struct Status {
+    double intake_pivot_position;
+    double turret_position;
+    double extend_position;
+
+    bool operator==(const Status &s) const {
+      return (intake_pivot_position == s.intake_pivot_position &&
+              turret_position == s.turret_position &&
+              extend_position == s.extend_position);
+    }
+    bool operator!=(const Status &s) const { return !(*this == s); }
+  };
+
+  // For the turret, 0 rad is pointing straight forwards
+  static constexpr double kMinCollisionZoneTurret = 0.15;
+  static constexpr double kMaxCollisionZoneTurret = 1.15;
+
+  static constexpr double kSafeTurretExtendedPosition = 0.0;
+
+  // Maximum position of the intake to avoid collisions
+  static constexpr double kCollisionZoneIntake = 1.6;
+
+  static constexpr double kMinCollisionZoneExtend = 0.03;
+
+  // Tolerances for the subsystems
+  static constexpr double kEpsTurret = 0.05;
+  static constexpr double kEpsIntake = 0.05;
+  static constexpr double kEpsExtend = 0.01;
+
+  CollisionAvoidance();
+
+  // Reports if the superstructure is collided.
+  bool IsCollided(const Status &status);
+  // Checks if there is a collision with the intake.
+  bool TurretCollided(double intake_position, double turret_position,
+                      double extend_position);
+  // Checks if there is a collision with the extend.
+  bool ExtendCollided(double intake_position, double turret_position,
+                      double extend_position);
+  // Checks and alters goals to make sure they're safe.
+  void UpdateGoal(const CollisionAvoidance::Status &status,
+                  double turret_goal_position, double extend_goal_position);
+  // Limits if goal is in collision spots.
+  void CalculateAvoidance(double intake_position, double turret_position,
+                          double extend_position, double turret_goal,
+                          double extend_goal);
+
+  // Returns the goals to give to the respective control loops in
+  // superstructure.
+  double min_extend_goal() const { return min_extend_goal_; }
+  double max_extend_goal() const { return max_extend_goal_; }
+  double min_turret_goal() const { return min_turret_goal_; }
+  double max_turret_goal() const { return max_turret_goal_; }
+  double min_intake_pivot_goal() const { return min_intake_pivot_goal_; }
+  double max_intake_pivot_goal() const { return max_intake_pivot_goal_; }
+
+  void update_max_turret_goal(double max_turret_goal) {
+    max_turret_goal_ = ::std::min(max_turret_goal, max_turret_goal_);
+  }
+  void update_min_turret_goal(double min_turret_goal) {
+    min_turret_goal_ = ::std::max(min_turret_goal, min_turret_goal_);
+  }
+  void update_max_intake_pivot_goal(double max_intake_pivot_goal) {
+    max_intake_pivot_goal_ =
+        ::std::min(max_intake_pivot_goal, max_intake_pivot_goal_);
+  }
+  void update_min_intake_pivot_goal(double min_intake_pivot_goal) {
+    min_intake_pivot_goal_ =
+        ::std::max(min_intake_pivot_goal, min_intake_pivot_goal_);
+  }
+  void update_min_extend_goal(double min_extend_goal) {
+    min_extend_goal_ = ::std::max(min_extend_goal, min_extend_goal_);
+  }
+  void update_max_extend_goal(double max_extend_goal) {
+    max_extend_goal_ = ::std::min(max_extend_goal, max_extend_goal_);
+  }
+
+ private:
+  void clear_min_intake_pivot_goal() {
+    min_intake_pivot_goal_ = -::std::numeric_limits<double>::infinity();
+  }
+  void clear_max_intake_pivot_goal() {
+    max_intake_pivot_goal_ = ::std::numeric_limits<double>::infinity();
+  }
+  void clear_min_turret_goal() {
+    min_turret_goal_ = -::std::numeric_limits<double>::infinity();
+  }
+  void clear_max_turret_goal() {
+    max_turret_goal_ = ::std::numeric_limits<double>::infinity();
+  }
+  void clear_min_extend_goal() {
+    min_extend_goal_ = -::std::numeric_limits<double>::infinity();
+  }
+  void clear_max_extend_goal() {
+    max_extend_goal_ = ::std::numeric_limits<double>::infinity();
+  }
+
+  double min_intake_pivot_goal_;
+  double max_intake_pivot_goal_;
+  double min_turret_goal_;
+  double max_turret_goal_;
+  double min_extend_goal_;
+  double max_extend_goal_;
+};
+
+}  // namespace y2024::control_loops::superstructure
+
+#endif
diff --git a/y2024/control_loops/superstructure/collision_avoidance_test.cc b/y2024/control_loops/superstructure/collision_avoidance_test.cc
new file mode 100644
index 0000000..c3a8e3a
--- /dev/null
+++ b/y2024/control_loops/superstructure/collision_avoidance_test.cc
@@ -0,0 +1,241 @@
+#include "y2024/control_loops/superstructure/collision_avoidance.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+#include "aos/commonmath.h"
+#include "aos/flatbuffers.h"
+#include "y2024/control_loops/superstructure/superstructure_goal_generated.h"
+#include "y2024/control_loops/superstructure/superstructure_status_generated.h"
+
+namespace y2024::control_loops::superstructure::testing {
+
+using aos::FlatbufferDetachedBuffer;
+using frc971::control_loops::PotAndAbsoluteEncoderProfiledJointStatus;
+using frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemGoal;
+
+// Enums for the different classes of intake and turret states
+enum class IntakeState { kSafe, kUnsafe };
+enum class TurretState {
+  kSafe,
+  kUnsafe,
+};
+enum class ExtendState {
+  kSafe,
+  kUnsafe,
+};
+
+class CollisionAvoidanceTest : public ::testing::Test {
+ public:
+  CollisionAvoidanceTest()
+      : intake_goal_(0),
+        turret_goal_(0),
+        extend_goal_(0),
+        status_({0.0, 0.0, 0.0}),
+        prev_status_({0.0, 0.0, 0.0}) {}
+
+  void Simulate() {
+    double safe_intake_goal = 0;
+    double safe_turret_goal = 0;
+    double safe_extend_goal = 0;
+    bool was_collided = avoidance_.IsCollided(status_);
+
+    VLOG(1) << "Simulation of intake " << status_.intake_pivot_position
+            << ", intake_goal " << intake_goal_ << ", turret "
+            << status_.turret_position << ", turret_goal " << turret_goal_
+            << ", extend " << status_.extend_position << ", extend goal "
+            << extend_goal_;
+
+    bool moving = true;
+    while (moving) {
+      // Compute the safe goal
+      avoidance_.UpdateGoal(status_, turret_goal_, extend_goal_);
+
+      if (!was_collided) {
+        // The system should never be collided if it didn't start off collided
+        EXPECT_FALSE(avoidance_.IsCollided(status_))
+            << ": Now collided at intake " << status_.intake_pivot_position
+            << ", intake_goal " << intake_goal_ << ", turret "
+            << status_.turret_position << ", turret_goal " << turret_goal_
+            << ", extend " << status_.extend_position << ", extend goal "
+            << extend_goal_;
+      } else {
+        was_collided = avoidance_.IsCollided(status_);
+      }
+
+      safe_intake_goal =
+          ::aos::Clip(intake_goal_, avoidance_.min_intake_pivot_goal(),
+                      avoidance_.max_intake_pivot_goal());
+
+      safe_turret_goal = ::aos::Clip(turret_goal_, avoidance_.min_turret_goal(),
+                                     avoidance_.max_turret_goal());
+
+      safe_extend_goal = ::aos::Clip(extend_goal_, avoidance_.min_extend_goal(),
+                                     avoidance_.max_extend_goal());
+
+      // Move each subsystem towards their goals a bit
+      status_.intake_pivot_position =
+          LimitedMove(status_.intake_pivot_position, safe_intake_goal);
+      status_.turret_position =
+          LimitedMove(status_.turret_position, safe_turret_goal);
+      status_.extend_position =
+          LimitedMove(status_.extend_position, safe_extend_goal);
+
+      // If it stopped moving, we're done
+      if (!IsMoving()) {
+        moving = false;
+      } else {
+        prev_status_ = status_;
+      }
+    }
+
+    EXPECT_FALSE(avoidance_.IsCollided(status_));
+
+    CheckGoals();
+  }
+
+  bool IsMoving() { return (status_ != prev_status_); }
+
+  double ComputeIntakeAngle(IntakeState intake_state) {
+    double intake_angle = 0.0;
+
+    switch (intake_state) {
+      case IntakeState::kSafe:
+        intake_angle = CollisionAvoidance::kCollisionZoneIntake -
+                       CollisionAvoidance::kEpsIntake;
+        break;
+      case IntakeState::kUnsafe:
+        intake_angle = CollisionAvoidance::kCollisionZoneIntake +
+                       CollisionAvoidance::kEpsIntake;
+        break;
+    }
+
+    return intake_angle;
+  }
+
+  double ComputeTurretAngle(TurretState turret_state) {
+    double turret_angle = 0.0;
+
+    switch (turret_state) {
+      case TurretState::kSafe:
+        turret_angle = CollisionAvoidance::kMaxCollisionZoneTurret +
+                       CollisionAvoidance::kEpsTurret;
+        break;
+      case TurretState::kUnsafe:
+        turret_angle = CollisionAvoidance::kMaxCollisionZoneTurret -
+                       CollisionAvoidance::kEpsTurret;
+        break;
+    }
+
+    return turret_angle;
+  }
+
+  double ComputeExtendPosition(ExtendState extend_state) {
+    double extend_position = 0.0;
+
+    switch (extend_state) {
+      case ExtendState::kSafe:
+        extend_position = CollisionAvoidance::kMinCollisionZoneExtend -
+                          CollisionAvoidance::kEpsExtend;
+        break;
+      case ExtendState::kUnsafe:
+        extend_position = CollisionAvoidance::kMinCollisionZoneExtend +
+                          CollisionAvoidance::kEpsTurret;
+        break;
+    }
+
+    return extend_position;
+  }
+
+  void Test(IntakeState intake_front_pos_state, TurretState turret_pos_state,
+            ExtendState extend_pos_state, IntakeState intake_front_goal_state,
+            TurretState turret_goal_state, ExtendState extend_goal_state) {
+    status_ = {ComputeIntakeAngle(intake_front_pos_state),
+               ComputeTurretAngle(turret_pos_state),
+               ComputeExtendPosition(extend_pos_state)};
+
+    intake_goal_ = ComputeIntakeAngle(intake_front_goal_state);
+
+    turret_goal_ = ComputeTurretAngle(turret_goal_state);
+
+    extend_goal_ = ComputeExtendPosition(extend_goal_state);
+
+    Simulate();
+  }
+
+  // Provide goals and status messages
+  double intake_goal_;
+  double turret_goal_;
+  double extend_goal_;
+  CollisionAvoidance::Status status_;
+
+ private:
+  static constexpr double kIterationMove = 0.001;
+
+  void CheckGoals() {
+    // Check to see if we reached the goals
+    // Turret is highest priority and should always reach the unsafe goal
+    EXPECT_NEAR(extend_goal_, status_.extend_position, kIterationMove);
+
+    if (avoidance_.ExtendCollided(intake_goal_, turret_goal_, extend_goal_)) {
+      EXPECT_EQ(status_.turret_position,
+                CollisionAvoidance::kSafeTurretExtendedPosition);
+      // If the unsafe goal had an intake colliding with the turret the intake
+      // position should be at least the collision zone angle. Otherwise, the
+      // intake should be at the unsafe goal
+      if (avoidance_.TurretCollided(intake_goal_, status_.turret_position,
+                                    extend_goal_)) {
+        EXPECT_LE(status_.intake_pivot_position,
+                  CollisionAvoidance::kCollisionZoneIntake);
+      } else {
+        EXPECT_NEAR(intake_goal_, status_.intake_pivot_position,
+                    kIterationMove);
+      }
+    } else {
+      EXPECT_NEAR(turret_goal_, status_.turret_position, kIterationMove);
+    }
+  }
+
+  double LimitedMove(double position, double goal) {
+    if (position + kIterationMove < goal) {
+      return position + kIterationMove;
+    } else if (position - kIterationMove > goal) {
+      return position - kIterationMove;
+    } else {
+      return goal;
+    }
+  }
+
+  CollisionAvoidance avoidance_;
+  CollisionAvoidance::Status prev_status_;
+};  // namespace y2024::control_loops::superstructure::testing
+
+// Just to be safe, brute force ALL the possible position-goal combinations
+// and make sure we never collide and the correct goals are reached
+TEST_F(CollisionAvoidanceTest, BruteForce) {
+  // Intake front position
+  for (IntakeState intake_front_pos :
+       {IntakeState::kSafe, IntakeState::kUnsafe}) {
+    // Turret back position
+    for (TurretState turret_pos : {TurretState::kSafe, TurretState::kUnsafe}) {
+      for (ExtendState extend_pos :
+           {ExtendState::kSafe, ExtendState::kUnsafe}) {
+        // Intake front goal
+        for (IntakeState intake_front_goal :
+             {IntakeState::kSafe, IntakeState::kUnsafe}) {
+          // Turret goal
+          for (TurretState turret_goal :
+               {TurretState::kSafe, TurretState::kUnsafe}) {
+            for (ExtendState extend_goal :
+                 {ExtendState::kSafe, ExtendState::kUnsafe}) {
+              Test(intake_front_pos, turret_pos, extend_pos, intake_front_goal,
+                   turret_goal, extend_goal);
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+}  // namespace y2024::control_loops::superstructure::testing
diff --git a/y2024/control_loops/superstructure/extend/BUILD b/y2024/control_loops/superstructure/extend/BUILD
new file mode 100644
index 0000000..7a28bde
--- /dev/null
+++ b/y2024/control_loops/superstructure/extend/BUILD
@@ -0,0 +1,42 @@
+package(default_visibility = ["//y2024:__subpackages__"])
+
+genrule(
+    name = "genrule_extend",
+    outs = [
+        "extend_plant.h",
+        "extend_plant.cc",
+        "extend_plant.json",
+        "integral_extend_plant.h",
+        "integral_extend_plant.cc",
+        "integral_extend_plant.json",
+    ],
+    cmd = "$(location //y2024/control_loops/python:extend) $(OUTS)",
+    target_compatible_with = ["@platforms//os:linux"],
+    tools = [
+        "//y2024/control_loops/python:extend",
+    ],
+)
+
+cc_library(
+    name = "extend_plants",
+    srcs = [
+        "extend_plant.cc",
+        "integral_extend_plant.cc",
+    ],
+    hdrs = [
+        "extend_plant.h",
+        "integral_extend_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",
+    ],
+)
+
+filegroup(
+    name = "extend_json",
+    srcs = ["integral_extend_plant.json"],
+    visibility = ["//visibility:public"],
+)
diff --git a/y2024/control_loops/superstructure/shooter.cc b/y2024/control_loops/superstructure/shooter.cc
new file mode 100644
index 0000000..cc00454
--- /dev/null
+++ b/y2024/control_loops/superstructure/shooter.cc
@@ -0,0 +1,311 @@
+#include "y2024/control_loops/superstructure/shooter.h"
+
+#include "aos/flatbuffers.h"
+#include "aos/flatbuffers/base.h"
+#include "frc971/control_loops/aiming/aiming.h"
+#include "y2024/control_loops/superstructure/catapult/catapult_plant.h"
+#include "y2024/control_loops/superstructure/collision_avoidance.h"
+
+namespace y2024::control_loops::superstructure {
+
+using frc971::control_loops::PotAndAbsoluteEncoderProfiledJointStatus;
+
+constexpr double kCatapultActivationThreshold = 0.01;
+
+Shooter::Shooter(aos::EventLoop *event_loop, const Constants *robot_constants)
+    : drivetrain_status_fetcher_(
+          event_loop->MakeFetcher<frc971::control_loops::drivetrain::Status>(
+              "/drivetrain")),
+      robot_constants_(robot_constants),
+      catapult_(
+          robot_constants->common()->catapult(),
+          robot_constants->robot()->catapult_constants()->zeroing_constants()),
+      turret_(
+          robot_constants_->common()->turret(),
+          robot_constants_->robot()->turret_constants()->zeroing_constants()),
+      altitude_(
+          robot_constants_->common()->altitude(),
+          robot_constants_->robot()->altitude_constants()->zeroing_constants()),
+      aimer_(event_loop, robot_constants_),
+      interpolation_table_(
+          y2024::constants::Values::InterpolationTableFromFlatbuffer(
+              robot_constants_->common()->shooter_interpolation_table())),
+      debouncer_(std::chrono::milliseconds(100), std::chrono::milliseconds(8)) {
+}
+
+flatbuffers::Offset<y2024::control_loops::superstructure::ShooterStatus>
+Shooter::Iterate(
+    const y2024::control_loops::superstructure::Position *position,
+    const y2024::control_loops::superstructure::ShooterGoal *shooter_goal,
+    bool fire, double *catapult_output, double *altitude_output,
+    double *turret_output, double *retention_roller_output,
+    double *retention_roller_stator_current_limit, double /*battery_voltage*/,
+    CollisionAvoidance *collision_avoidance, const double extend_position,
+    const double extend_goal, double *max_extend_position,
+    double *min_extend_position, const double intake_pivot_position,
+    double *max_intake_pivot_position, double *min_intake_pivot_position,
+    flatbuffers::FlatBufferBuilder *fbb,
+    aos::monotonic_clock::time_point monotonic_now) {
+  drivetrain_status_fetcher_.Fetch();
+
+  // If our current is over the minimum current and our velocity is under our
+  // maximum velocity, then set loaded to true. If we are preloaded set it to
+  // true as well.
+  debouncer_.Update(position->catapult_beambreak() ||
+                        (shooter_goal != nullptr && shooter_goal->preloaded()),
+                    monotonic_now);
+  const bool piece_loaded = debouncer_.state();
+
+  aos::fbs::FixedStackAllocator<aos::fbs::Builder<
+      frc971::control_loops::
+          StaticZeroingSingleDOFProfiledSubsystemGoalStatic>::kBufferSize>
+      turret_allocator;
+
+  aos::fbs::Builder<
+      frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemGoalStatic>
+      turret_goal_builder(&turret_allocator);
+
+  aos::fbs::FixedStackAllocator<aos::fbs::Builder<
+      frc971::control_loops::
+          StaticZeroingSingleDOFProfiledSubsystemGoalStatic>::kBufferSize>
+      altitude_allocator;
+
+  aos::fbs::Builder<
+      frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemGoalStatic>
+      altitude_goal_builder(&altitude_allocator);
+
+  const double distance_to_goal = aimer_.DistanceToGoal();
+
+  // Always retain the game piece if we are enabled.
+  if (retention_roller_output != nullptr) {
+    *retention_roller_output =
+        robot_constants_->common()->retention_roller_voltages()->retaining();
+
+    if (piece_loaded) {
+      *retention_roller_stator_current_limit =
+          robot_constants_->common()
+              ->current_limits()
+              ->slower_retention_roller_stator_current_limit();
+    } else {
+      *retention_roller_stator_current_limit =
+          robot_constants_->common()
+              ->current_limits()
+              ->retention_roller_stator_current_limit();
+    }
+  }
+
+  bool aiming = false;
+
+  if (shooter_goal == nullptr || !shooter_goal->auto_aim() ||
+      (!piece_loaded && state_ == CatapultState::READY)) {
+    // We don't have the note so we should be ready to intake it.
+    PopulateStaticZeroingSingleDOFProfiledSubsystemGoal(
+        turret_goal_builder.get(),
+        robot_constants_->common()->turret_loading_position());
+
+    PopulateStaticZeroingSingleDOFProfiledSubsystemGoal(
+        altitude_goal_builder.get(),
+        robot_constants_->common()->altitude_loading_position());
+
+  } else {
+    // We have a game piece, lets start aiming.
+    if (drivetrain_status_fetcher_.get() != nullptr) {
+      aiming = true;
+      aimer_.Update(drivetrain_status_fetcher_.get(),
+                    frc971::control_loops::aiming::ShotMode::kShootOnTheFly,
+                    turret_goal_builder.get());
+    }
+  }
+
+  // We have a game piece and are being asked to aim.
+  constants::Values::ShotParams shot_params;
+  if (piece_loaded && shooter_goal != nullptr && shooter_goal->auto_aim() &&
+      interpolation_table_.GetInRange(distance_to_goal, &shot_params)) {
+    PopulateStaticZeroingSingleDOFProfiledSubsystemGoal(
+        altitude_goal_builder.get(), shot_params.shot_altitude_angle);
+  }
+
+  // The builder will contain either the auto-aim goal, or the loading goal. Use
+  // it if we have no goal, or no subsystem goal, or if we are auto-aiming.
+
+  const frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemGoal
+      *turret_goal = (shooter_goal != nullptr && !shooter_goal->auto_aim() &&
+                      shooter_goal->has_turret_position())
+                         ? shooter_goal->turret_position()
+                         : &turret_goal_builder->AsFlatbuffer();
+
+  const frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemGoal
+      *altitude_goal = (shooter_goal != nullptr && !shooter_goal->auto_aim() &&
+                        shooter_goal->has_altitude_position())
+                           ? shooter_goal->altitude_position()
+                           : &altitude_goal_builder->AsFlatbuffer();
+
+  bool subsystems_in_range =
+      (std::abs(turret_.estimated_position() - turret_goal->unsafe_goal()) <
+           kCatapultActivationThreshold &&
+       std::abs(altitude_.estimated_position() - altitude_goal->unsafe_goal()) <
+           kCatapultActivationThreshold &&
+       altitude_.estimated_position() >
+           robot_constants_->common()->min_altitude_shooting_angle());
+
+  const bool disabled = turret_.Correct(turret_goal, position->turret(),
+                                        turret_output == nullptr);
+
+  collision_avoidance->UpdateGoal(
+      {.intake_pivot_position = intake_pivot_position,
+       .turret_position = turret_.estimated_position(),
+       .extend_position = extend_position},
+      turret_goal->unsafe_goal(), extend_goal);
+
+  turret_.set_min_position(collision_avoidance->min_turret_goal());
+  turret_.set_max_position(collision_avoidance->max_turret_goal());
+
+  *max_intake_pivot_position = collision_avoidance->max_intake_pivot_goal();
+  *min_intake_pivot_position = collision_avoidance->min_intake_pivot_goal();
+
+  *max_extend_position = collision_avoidance->max_extend_goal();
+  *min_extend_position = collision_avoidance->min_extend_goal();
+
+  // Calculate the loops for a cycle.
+  const double voltage = turret_.UpdateController(disabled);
+
+  turret_.UpdateObserver(voltage);
+
+  // Write out all the voltages.
+  if (turret_output) {
+    *turret_output = voltage;
+  }
+
+  const flatbuffers::Offset<PotAndAbsoluteEncoderProfiledJointStatus>
+      turret_status_offset = turret_.MakeStatus(fbb);
+
+  const flatbuffers::Offset<PotAndAbsoluteEncoderProfiledJointStatus>
+      altitude_status_offset = altitude_.Iterate(
+          altitude_goal, position->altitude(), altitude_output, fbb);
+
+  flatbuffers::Offset<PotAndAbsoluteEncoderProfiledJointStatus>
+      catapult_status_offset;
+  {
+    // The catapult will never use a provided goal.  We'll always fabricate one
+    // for it.
+    //
+    // Correct handles resetting our state when disabled.
+    const bool disabled = catapult_.Correct(nullptr, position->catapult(),
+                                            catapult_output == nullptr);
+
+    catapult_.set_enable_profile(true);
+    // We want a trajectory which accelerates up over the first portion of the
+    // range of motion, holds top speed for a little bit, then decelerates
+    // before it swings too far.
+    //
+    // We can solve for these 3 parameters through the range of motion.  Top
+    // speed is goverened by the voltage headroom we want to have for the
+    // controller.
+    //
+    // Accel can be tuned given the distance to accelerate over, and decel can
+    // be solved similarly.
+    //
+    // accel = v^2 / (2 * x)
+    catapult_.mutable_profile()->set_maximum_velocity(
+        catapult::kFreeSpeed * catapult::kOutputRatio * 4.0 / 12.0);
+
+    if (disabled) {
+      state_ = CatapultState::RETRACTING;
+    }
+
+    constexpr double kLoadingAcceleration = 20.0;
+    constexpr double kLoadingDeceleration = 10.0;
+
+    switch (state_) {
+      case CatapultState::READY:
+        [[fallthrough]];
+      case CatapultState::LOADED: {
+        if (piece_loaded) {
+          state_ = CatapultState::LOADED;
+        } else {
+          state_ = CatapultState::READY;
+        }
+
+        const bool catapult_close = CatapultClose();
+
+        if (subsystems_in_range && shooter_goal != nullptr && fire &&
+            catapult_close && piece_loaded) {
+          state_ = CatapultState::FIRING;
+        } else {
+          catapult_.set_controller_index(0);
+          catapult_.mutable_profile()->set_maximum_acceleration(
+              kLoadingAcceleration);
+          catapult_.mutable_profile()->set_maximum_deceleration(
+              kLoadingDeceleration);
+          catapult_.set_unprofiled_goal(0.0, 0.0);
+
+          if (!catapult_close) {
+            state_ = CatapultState::RETRACTING;
+          }
+          break;
+        }
+        [[fallthrough]];
+      }
+      case CatapultState::FIRING:
+        *retention_roller_output =
+            robot_constants_->common()->retention_roller_voltages()->spitting();
+        *retention_roller_stator_current_limit =
+            robot_constants_->common()
+                ->current_limits()
+                ->shooting_retention_roller_stator_current_limit();
+        catapult_.set_controller_index(1);
+        catapult_.mutable_profile()->set_maximum_acceleration(400.0);
+        catapult_.mutable_profile()->set_maximum_deceleration(500.0);
+        catapult_.set_unprofiled_goal(2.0, 0.0);
+        if (CatapultClose()) {
+          state_ = CatapultState::RETRACTING;
+        } else {
+          break;
+        }
+        [[fallthrough]];
+      case CatapultState::RETRACTING:
+        catapult_.set_controller_index(0);
+        catapult_.mutable_profile()->set_maximum_acceleration(
+            kLoadingAcceleration);
+        catapult_.mutable_profile()->set_maximum_deceleration(
+            kLoadingDeceleration);
+        // TODO: catapult_return_position
+        catapult_.set_unprofiled_goal(0.0, 0.0);
+
+        if (CatapultClose()) {
+          if (piece_loaded) {
+            state_ = CatapultState::LOADED;
+          } else {
+            state_ = CatapultState::READY;
+          }
+        }
+        break;
+    }
+
+    const double voltage = catapult_.UpdateController(disabled);
+    catapult_.UpdateObserver(voltage);
+    if (catapult_output != nullptr) {
+      *catapult_output = voltage;
+    }
+    catapult_status_offset = catapult_.MakeStatus(fbb);
+  }
+
+  flatbuffers::Offset<AimerStatus> aimer_offset;
+  if (aiming) {
+    aimer_offset = aimer_.PopulateStatus(fbb);
+  }
+
+  y2024::control_loops::superstructure::ShooterStatus::Builder status_builder(
+      *fbb);
+  status_builder.add_turret(turret_status_offset);
+  status_builder.add_altitude(altitude_status_offset);
+  status_builder.add_catapult(catapult_status_offset);
+  status_builder.add_catapult_state(state_);
+  if (aiming) {
+    status_builder.add_aimer(aimer_offset);
+  }
+
+  return status_builder.Finish();
+}
+
+}  // namespace y2024::control_loops::superstructure
diff --git a/y2024/control_loops/superstructure/shooter.h b/y2024/control_loops/superstructure/shooter.h
new file mode 100644
index 0000000..2571cab
--- /dev/null
+++ b/y2024/control_loops/superstructure/shooter.h
@@ -0,0 +1,145 @@
+#ifndef Y2024_CONTROL_LOOPS_SUPERSTRUCTURE_SHOOTER_H_
+#define Y2024_CONTROL_LOOPS_SUPERSTRUCTURE_SHOOTER_H_
+
+#include "frc971/control_loops/catapult/catapult.h"
+#include "frc971/control_loops/catapult/catapult_goal_static.h"
+#include "frc971/control_loops/static_zeroing_single_dof_profiled_subsystem.h"
+#include "frc971/shooter_interpolation/interpolation.h"
+#include "frc971/zeroing/pot_and_absolute_encoder.h"
+#include "y2024/constants.h"
+#include "y2024/constants/constants_generated.h"
+#include "y2024/control_loops/superstructure/aiming.h"
+#include "y2024/control_loops/superstructure/collision_avoidance.h"
+#include "y2024/control_loops/superstructure/superstructure_can_position_generated.h"
+#include "y2024/control_loops/superstructure/superstructure_goal_generated.h"
+#include "y2024/control_loops/superstructure/superstructure_position_generated.h"
+#include "y2024/control_loops/superstructure/superstructure_status_generated.h"
+
+namespace y2024::control_loops::superstructure {
+
+class Debouncer {
+ public:
+  Debouncer(std::chrono::nanoseconds rising_delay,
+            std::chrono::nanoseconds falling_delay)
+      : rising_delay_(rising_delay), falling_delay_(falling_delay) {}
+
+  void Update(bool state, aos::monotonic_clock::time_point now) {
+    if (state_transition_ != state) {
+      transition_time_ = now;
+      state_transition_ = state;
+    }
+
+    if (state != output_state_) {
+      if (state) {
+        output_state_ = now > transition_time_ + rising_delay_;
+      } else {
+        output_state_ = !(now > transition_time_ + falling_delay_);
+      }
+    }
+  }
+
+  bool state() const { return output_state_; }
+
+ private:
+  const std::chrono::nanoseconds rising_delay_;
+  const std::chrono::nanoseconds falling_delay_;
+
+  bool state_transition_ = false;
+  bool output_state_ = false;
+  aos::monotonic_clock::time_point transition_time_ =
+      aos::monotonic_clock::min_time;
+};
+
+// The shooter class will control the various subsystems involved in the
+// shooter- the turret, altitude, and catapult.
+class Shooter {
+ public:
+  using PotAndAbsoluteEncoderSubsystem =
+      ::frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystem<
+          ::frc971::zeroing::PotAndAbsoluteEncoderZeroingEstimator,
+          ::frc971::control_loops::PotAndAbsoluteEncoderProfiledJointStatus>;
+
+  using CatapultSubsystem =
+      ::frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystem<
+          ::frc971::zeroing::PotAndAbsoluteEncoderZeroingEstimator,
+          ::frc971::control_loops::PotAndAbsoluteEncoderProfiledJointStatus,
+          ::frc971::zeroing::PotAndAbsoluteEncoderZeroingEstimator,
+          aos::util::AsymmetricTrapezoidProfile>;
+
+  Shooter(aos::EventLoop *event_loop, const Constants *robot_constants);
+
+  void Reset() {
+    catapult_.Reset();
+    turret_.Reset();
+    altitude_.Reset();
+  }
+
+  void Estop() {
+    catapult_.Estop();
+    turret_.Estop();
+    altitude_.Estop();
+  }
+
+  bool zeroed() {
+    return catapult_.zeroed() && turret_.zeroed() && altitude_.zeroed();
+  }
+
+  bool estopped() {
+    return catapult_.estopped() && turret_.estopped() && altitude_.estopped();
+  }
+
+  inline const PotAndAbsoluteEncoderSubsystem &turret() const {
+    return turret_;
+  }
+
+  inline const PotAndAbsoluteEncoderSubsystem &altitude() const {
+    return altitude_;
+  }
+
+  flatbuffers::Offset<ShooterStatus> Iterate(
+      const Position *position, const ShooterGoal *shooter_goal, bool fire,
+      double *catapult_output, double *altitude_output, double *turret_output,
+      double *retention_roller_output,
+      double *retention_roller_stator_current_limit, double battery_voltage,
+      /* Hacky way to use collision avoidance in this class */
+      CollisionAvoidance *collision_avoidance, const double extend_position,
+      const double extend_goal, double *max_extend_position,
+      double *min_extend_position, const double intake_pivot_position,
+      double *max_turret_intake_position, double *min_intake_pivot_position,
+      flatbuffers::FlatBufferBuilder *fbb,
+      aos::monotonic_clock::time_point monotonic_now);
+
+ private:
+  CatapultState state_ = CatapultState::RETRACTING;
+
+  bool CatapultClose() const {
+    return (std::abs(catapult_.estimated_position() -
+                     catapult_.unprofiled_goal(0, 0)) < 0.05 &&
+            std::abs(catapult_.estimated_velocity()) < 0.5);
+  }
+
+  aos::Fetcher<frc971::control_loops::drivetrain::Status>
+      drivetrain_status_fetcher_;
+
+  aos::Fetcher<y2024::control_loops::superstructure::CANPosition>
+      superstructure_can_position_fetcher_;
+
+  const Constants *robot_constants_;
+
+  CatapultSubsystem catapult_;
+
+  PotAndAbsoluteEncoderSubsystem turret_;
+  PotAndAbsoluteEncoderSubsystem altitude_;
+
+  Aimer aimer_;
+
+  frc971::shooter_interpolation::InterpolationTable<
+      y2024::constants::Values::ShotParams>
+      interpolation_table_;
+
+  Debouncer debouncer_;
+};
+
+}  // namespace y2024::control_loops::superstructure
+
+#endif
diff --git a/y2024/control_loops/superstructure/superstructure.cc b/y2024/control_loops/superstructure/superstructure.cc
index 18db6d6..dc9369e 100644
--- a/y2024/control_loops/superstructure/superstructure.cc
+++ b/y2024/control_loops/superstructure/superstructure.cc
@@ -1,13 +1,31 @@
 #include "y2024/control_loops/superstructure/superstructure.h"
 
+#include <chrono>
+
 #include "aos/events/event_loop.h"
 #include "aos/flatbuffer_merge.h"
 #include "aos/network/team_number.h"
+#include "aos/time/time.h"
 #include "frc971/shooter_interpolation/interpolation.h"
 #include "frc971/zeroing/wrap.h"
 
 DEFINE_bool(ignore_distance, false,
-            "If true, ignore distance when shooting and obay joystick_reader");
+            "If true, ignore distance when shooting and obey joystick_reader");
+
+// The threshold used when decided if the extend is close enough to a goal to
+// continue.
+constexpr double kExtendThreshold = 0.01;
+
+constexpr double kTurretLoadingThreshold = 0.01;
+constexpr double kAltitudeLoadingThreshold = 0.01;
+
+constexpr std::chrono::milliseconds kExtraIntakingTime =
+    std::chrono::milliseconds(500);
+
+// Exit catapult loading state after this much time if we never
+// trigger any beambreaks.
+constexpr std::chrono::milliseconds kMaxCatapultLoadingTime =
+    std::chrono::milliseconds(3000);
 
 namespace y2024::control_loops::superstructure {
 
@@ -18,11 +36,9 @@
 using frc971::control_loops::RelativeEncoderProfiledJointStatus;
 
 Superstructure::Superstructure(::aos::EventLoop *event_loop,
-                               std::shared_ptr<const constants::Values> values,
                                const ::std::string &name)
     : frc971::controls::ControlLoop<Goal, Position, Status, Output>(event_loop,
                                                                     name),
-      values_(values),
       constants_fetcher_(event_loop),
       robot_constants_(CHECK_NOTNULL(&constants_fetcher_.constants())),
       drivetrain_status_fetcher_(
@@ -30,16 +46,22 @@
               "/drivetrain")),
       joystick_state_fetcher_(
           event_loop->MakeFetcher<aos::JoystickState>("/aos")),
-      transfer_goal_(TransferRollerGoal::NONE),
-      intake_pivot_(
-          robot_constants_->common()->intake_pivot(),
-          robot_constants_->robot()->intake_constants()->zeroing_constants()),
+      intake_pivot_(robot_constants_->common()->intake_pivot(),
+                    robot_constants_->robot()->intake_constants()),
       climber_(
           robot_constants_->common()->climber(),
-          robot_constants_->robot()->climber_constants()->zeroing_constants()) {
+          robot_constants_->robot()->climber_constants()->zeroing_constants()),
+      shooter_(event_loop, robot_constants_),
+      extend_(
+          robot_constants_->common()->extend(),
+          robot_constants_->robot()->extend_constants()->zeroing_constants()) {
   event_loop->SetRuntimeRealtimePriority(30);
 }
 
+bool PositionNear(double position, double goal, double threshold) {
+  return std::abs(position - goal) < threshold;
+}
+
 void Superstructure::RunIteration(const Goal *unsafe_goal,
                                   const Position *position,
                                   aos::Sender<Output>::Builder *output,
@@ -47,72 +69,17 @@
   const monotonic_clock::time_point timestamp =
       event_loop()->context().monotonic_event_time;
 
-  (void)timestamp;
-  (void)position;
-
   if (WasReset()) {
     AOS_LOG(ERROR, "WPILib reset, restarting\n");
     intake_pivot_.Reset();
     climber_.Reset();
+    shooter_.Reset();
+    extend_.Reset();
   }
 
   OutputT output_struct;
 
-  double intake_pivot_position =
-      robot_constants_->common()->intake_pivot_set_points()->retracted();
-
-  if (unsafe_goal != nullptr &&
-      unsafe_goal->intake_pivot_goal() == IntakePivotGoal::EXTENDED) {
-    intake_pivot_position =
-        robot_constants_->common()->intake_pivot_set_points()->extended();
-  }
-
-  IntakeRollerState intake_roller_state = IntakeRollerState::NONE;
-
-  switch (unsafe_goal != nullptr ? unsafe_goal->intake_roller_goal()
-                                 : IntakeRollerGoal::NONE) {
-    case IntakeRollerGoal::NONE:
-      output_struct.intake_roller_voltage = 0.0;
-      break;
-    case IntakeRollerGoal::SPIT:
-      transfer_goal_ = TransferRollerGoal::TRANSFER_OUT;
-      intake_roller_state = IntakeRollerState::SPITTING;
-      output_struct.intake_roller_voltage =
-          robot_constants_->common()->intake_roller_voltages()->spitting();
-      break;
-    case IntakeRollerGoal::INTAKE:
-      transfer_goal_ = TransferRollerGoal::TRANSFER_IN;
-      intake_roller_state = IntakeRollerState::INTAKING;
-      output_struct.intake_roller_voltage =
-          robot_constants_->common()->intake_roller_voltages()->intaking();
-      break;
-  }
-
-  TransferRollerState transfer_roller_state = TransferRollerState::NONE;
-
-  switch (unsafe_goal != nullptr ? transfer_goal_ : TransferRollerGoal::NONE) {
-    case TransferRollerGoal::NONE:
-      output_struct.transfer_roller_voltage = 0.0;
-      break;
-    case TransferRollerGoal::TRANSFER_IN:
-      if (position->transfer_beambreak()) {
-        transfer_goal_ = TransferRollerGoal::NONE;
-        transfer_roller_state = TransferRollerState::NONE;
-        output_struct.transfer_roller_voltage = 0.0;
-        break;
-      }
-      transfer_roller_state = TransferRollerState::TRANSFERING_IN;
-      output_struct.transfer_roller_voltage =
-          robot_constants_->common()->transfer_roller_voltages()->transfer_in();
-      break;
-    case TransferRollerGoal::TRANSFER_OUT:
-      transfer_roller_state = TransferRollerState::TRANSFERING_OUT;
-      output_struct.transfer_roller_voltage = robot_constants_->common()
-                                                  ->transfer_roller_voltages()
-                                                  ->transfer_out();
-      break;
-  }
-
+  // Handle Climber Goal separately from main superstructure state machine
   double climber_position =
       robot_constants_->common()->climber_set_points()->retract();
 
@@ -122,19 +89,399 @@
         climber_position =
             robot_constants_->common()->climber_set_points()->full_extend();
         break;
-      case ClimberGoal::HALF_EXTEND:
-        climber_position =
-            robot_constants_->common()->climber_set_points()->half_extend();
-        break;
       case ClimberGoal::RETRACT:
         climber_position =
             robot_constants_->common()->climber_set_points()->retract();
         break;
-      default:
+      case ClimberGoal::STOWED:
+        climber_position =
+            robot_constants_->common()->climber_set_points()->stowed();
+    }
+  }
+
+  // If we started off preloaded, skip to the ready state.
+  if (unsafe_goal != nullptr && unsafe_goal->shooter_goal() &&
+      unsafe_goal->shooter_goal()->preloaded()) {
+    if (state_ != SuperstructureState::READY &&
+        state_ != SuperstructureState::FIRING) {
+      state_ = SuperstructureState::READY;
+      requested_note_goal_ = NoteGoal::CATAPULT;
+    }
+  }
+
+  // Handle the intake pivot goal separately from the main superstructure state
+  IntakeRollerStatus intake_roller_state = IntakeRollerStatus::NONE;
+  double intake_pivot_position =
+      robot_constants_->common()->intake_pivot_set_points()->retracted();
+
+  if (unsafe_goal != nullptr) {
+    switch (unsafe_goal->intake_goal()) {
+      case IntakeGoal::INTAKE:
+        intake_pivot_position =
+            robot_constants_->common()->intake_pivot_set_points()->extended();
+        intake_end_time_ = timestamp;
+        break;
+      case IntakeGoal::SPIT:
+        intake_pivot_position =
+            robot_constants_->common()->intake_pivot_set_points()->retracted();
+        break;
+      case IntakeGoal::NONE:
+        intake_pivot_position =
+            robot_constants_->common()->intake_pivot_set_points()->retracted();
         break;
     }
   }
 
+  ExtendRollerStatus extend_roller_status = ExtendRollerStatus::IDLE;
+  ExtendStatus extend_goal_location = ExtendStatus::RETRACTED;
+
+  // True if the extend is moving towards a goal
+  bool extend_moving = false;
+
+  TransferRollerStatus transfer_roller_status = TransferRollerStatus::NONE;
+
+  const ExtendSetPoints *extend_set_points =
+      robot_constants_->common()->extend_set_points();
+
+  // Checks if the extend is close enough to the retracted position to be
+  // considered ready to accept note from the transfer rollers.
+  const bool extend_at_retracted = PositionNear(
+      extend_.position(), extend_set_points->retracted(), kExtendThreshold);
+
+  // Check if the turret is at the position to accept the note from extend
+  const bool turret_ready_for_load =
+      PositionNear(shooter_.turret().estimated_position(),
+                   robot_constants_->common()->turret_loading_position(),
+                   kTurretLoadingThreshold);
+
+  // Check if the altitude is at the position to accept the note from
+  // extend
+  const bool altitude_ready_for_load =
+      PositionNear(shooter_.altitude().estimated_position(),
+                   robot_constants_->common()->altitude_loading_position(),
+                   kAltitudeLoadingThreshold);
+
+  // Check if the extend is at the position to load the catapult
+  const bool extend_ready_for_catapult_transfer = PositionNear(
+      extend_.position(), extend_set_points->catapult(), kExtendThreshold);
+
+  // Only update the reuested note goal to the first goal that is requested by
+  // the manipulator
+  if (unsafe_goal != nullptr && unsafe_goal->note_goal() != NoteGoal::NONE &&
+      requested_note_goal_ == NoteGoal::NONE) {
+    requested_note_goal_ = unsafe_goal->note_goal();
+  }
+
+  // Superstructure state machine:
+  // 1. IDLE. The intake is retracted and there is no note in the robot.
+  // Wait for a intake goal to switch state to INTAKING if the extend is ready
+  // 2. INTAKING. Intake the note and transfer it towards the extend.
+  // Give intake, transfer, and extend rollers positive voltage to intake and
+  // transfer. Switch to LOADED when the extend beambreak is triggered.
+  // 3. LOADED. The note is in the extend and the extend is retracted.
+  // Wait for a note goal to switch state to MOVING.
+  // For AMP/TRAP goals, check that the turret is in a position to avoid
+  // collision.
+  // 4. MOVING. The extend is moving towards a goal (AMP, TRAP, or CATAPULT).
+  // For CATAPULT goals, wait for the turret and altitude to be in a position to
+  // accept the note from the extend.
+  // Wait for the extend to reach the goal and switch state to READY if
+  // AMP or TRAP, or to LOADING_CATAPULT if CATAPULT.
+  // 5. LOADING_CATAPULT. The extend is at the position to load the catapult.
+  // Activate the extend roller to transfer the note to the catapult.
+  // Switch state to READY when the catapult beambreak is triggered.
+  // 6. READY. Ready for fire command. The note is either loaded in the catapult
+  // or in the extend and at the AMP or TRAP position. Wait for a fire command.
+  // 7. FIRING. The note is being fired, either from the extend or the catapult.
+  // Switch state back to IDLE when the note is fired.
+
+  switch (state_) {
+    case SuperstructureState::IDLE:
+      requested_note_goal_ = NoteGoal::NONE;
+
+      if (unsafe_goal != nullptr &&
+          unsafe_goal->intake_goal() == IntakeGoal::INTAKE &&
+          extend_at_retracted) {
+        state_ = SuperstructureState::INTAKING;
+      }
+
+      extend_goal_location = ExtendStatus::RETRACTED;
+      catapult_requested_ = false;
+      break;
+    case SuperstructureState::INTAKING:
+      // Switch to LOADED state when the extend beambreak is triggered
+      // meaning the note is loaded in the extend
+      if (position->extend_beambreak()) {
+        state_ = SuperstructureState::LOADED;
+      }
+      intake_roller_state = IntakeRollerStatus::INTAKING;
+      transfer_roller_status = TransferRollerStatus::TRANSFERING_IN;
+      extend_roller_status = ExtendRollerStatus::TRANSFERING_TO_EXTEND;
+      extend_goal_location = ExtendStatus::RETRACTED;
+
+      if (!catapult_requested_ && unsafe_goal != nullptr &&
+          unsafe_goal->note_goal() == NoteGoal::CATAPULT) {
+        catapult_requested_ = true;
+      }
+
+      // If we are no longer requesting INTAKE or we are no longer requesting
+      // an INTAKE goal, wait 0.5 seconds then go back to IDLE.
+      if (!(unsafe_goal != nullptr &&
+            unsafe_goal->intake_goal() == IntakeGoal::INTAKE) &&
+          timestamp > intake_end_time_ + kExtraIntakingTime) {
+        state_ = SuperstructureState::IDLE;
+      }
+
+      break;
+    case SuperstructureState::LOADED:
+      if (!position->extend_beambreak() && !position->catapult_beambreak()) {
+        state_ = SuperstructureState::IDLE;
+      }
+
+      switch (requested_note_goal_) {
+        case NoteGoal::NONE:
+          break;
+        case NoteGoal::CATAPULT:
+          state_ = SuperstructureState::MOVING;
+          transfer_roller_status = TransferRollerStatus::TRANSFERING_IN;
+          break;
+        case NoteGoal::TRAP:
+          [[fallthrough]];
+        case NoteGoal::AMP:
+          transfer_roller_status = TransferRollerStatus::EXTEND_MOVING;
+          state_ = SuperstructureState::MOVING;
+          break;
+      }
+      extend_goal_location = ExtendStatus::RETRACTED;
+      break;
+    case SuperstructureState::MOVING:
+      transfer_roller_status = TransferRollerStatus::EXTEND_MOVING;
+      switch (requested_note_goal_) {
+        case NoteGoal::NONE:
+          extend_goal_location = ExtendStatus::RETRACTED;
+          if (extend_at_retracted) {
+            state_ = SuperstructureState::LOADED;
+          }
+          break;
+        case NoteGoal::CATAPULT:
+          extend_goal_location = ExtendStatus::CATAPULT;
+          if (extend_ready_for_catapult_transfer && turret_ready_for_load &&
+              altitude_ready_for_load) {
+            loading_catapult_start_time_ = timestamp;
+            state_ = SuperstructureState::LOADING_CATAPULT;
+          }
+          break;
+        case NoteGoal::TRAP:
+          extend_goal_location = ExtendStatus::TRAP;
+          // Check if the extend is at the TRAP position and if it is
+          // switch to READY state
+          if (PositionNear(extend_.position(), extend_set_points->trap(),
+                           kExtendThreshold)) {
+            state_ = SuperstructureState::READY;
+          }
+          break;
+        case NoteGoal::AMP:
+          extend_goal_location = ExtendStatus::AMP;
+          // Check if the extend is at the AMP position and if it is
+          // switch to READY state
+          if (PositionNear(extend_.position(), extend_set_points->amp(),
+                           kExtendThreshold)) {
+            state_ = SuperstructureState::READY;
+          }
+          break;
+      }
+
+      extend_moving = true;
+      break;
+    case SuperstructureState::LOADING_CATAPULT:
+      extend_moving = false;
+      extend_goal_location = ExtendStatus::CATAPULT;
+      extend_roller_status = ExtendRollerStatus::TRANSFERING_TO_CATAPULT;
+
+      // If we lost the game piece, reset state to idle.
+      if (((timestamp - loading_catapult_start_time_) >
+           kMaxCatapultLoadingTime) &&
+          !position->catapult_beambreak() && !position->extend_beambreak()) {
+        state_ = SuperstructureState::IDLE;
+      }
+
+      // Switch to READY state when the catapult beambreak is triggered
+      if (position->catapult_beambreak()) {
+        state_ = SuperstructureState::READY;
+      }
+      break;
+    case SuperstructureState::READY:
+      extend_moving = false;
+
+      // Switch to FIRING state when the fire button is pressed
+      if (unsafe_goal != nullptr && unsafe_goal->fire()) {
+        state_ = SuperstructureState::FIRING;
+      }
+
+      switch (requested_note_goal_) {
+        case NoteGoal::NONE:
+          extend_goal_location = ExtendStatus::RETRACTED;
+          extend_moving = true;
+          state_ = SuperstructureState::MOVING;
+          break;
+        case NoteGoal::CATAPULT:
+          extend_goal_location = ExtendStatus::CATAPULT;
+          break;
+        case NoteGoal::TRAP:
+          extend_goal_location = ExtendStatus::TRAP;
+          break;
+        case NoteGoal::AMP:
+          extend_goal_location = ExtendStatus::AMP;
+          break;
+      }
+      break;
+    case SuperstructureState::FIRING:
+      switch (requested_note_goal_) {
+        case NoteGoal::NONE:
+
+          break;
+        case NoteGoal::CATAPULT:
+          extend_goal_location = ExtendStatus::CATAPULT;
+          // Reset the state to IDLE when the game piece is fired from the
+          // catapult. We consider the game piece to be fired from the catapult
+          // when the catapultbeambreak is no longer triggered.
+          if (!position->catapult_beambreak()) {
+            state_ = SuperstructureState::IDLE;
+          }
+          break;
+        case NoteGoal::TRAP:
+          extend_roller_status = ExtendRollerStatus::SCORING_IN_TRAP;
+          extend_goal_location = ExtendStatus::TRAP;
+          if (!position->extend_beambreak() && unsafe_goal != nullptr &&
+              !unsafe_goal->fire()) {
+            state_ = SuperstructureState::IDLE;
+          }
+          break;
+        case NoteGoal::AMP:
+          extend_roller_status = ExtendRollerStatus::SCORING_IN_AMP;
+          extend_goal_location = ExtendStatus::AMP;
+          if (!position->extend_beambreak() && unsafe_goal != nullptr &&
+              !unsafe_goal->fire()) {
+            state_ = SuperstructureState::IDLE;
+          }
+          break;
+      }
+      break;
+  }
+
+  if (unsafe_goal != nullptr &&
+      unsafe_goal->intake_goal() == IntakeGoal::SPIT) {
+    intake_roller_state = IntakeRollerStatus::SPITTING;
+    transfer_roller_status = TransferRollerStatus::TRANSFERING_OUT;
+  }
+
+  // Update Intake Roller voltage based on status from state machine.
+  switch (intake_roller_state) {
+    case IntakeRollerStatus::NONE:
+      output_struct.intake_roller_voltage = 0.0;
+      break;
+    case IntakeRollerStatus::SPITTING:
+      output_struct.intake_roller_voltage =
+          robot_constants_->common()->intake_roller_voltages()->spitting();
+      break;
+    case IntakeRollerStatus::INTAKING:
+      output_struct.intake_roller_voltage =
+          robot_constants_->common()->intake_roller_voltages()->intaking();
+      break;
+  }
+
+  // Update Transfer Roller voltage based on status from state machine.
+  switch (transfer_roller_status) {
+    case TransferRollerStatus::NONE:
+      output_struct.transfer_roller_voltage = 0.0;
+      break;
+    case TransferRollerStatus::TRANSFERING_IN:
+      output_struct.transfer_roller_voltage =
+          robot_constants_->common()->transfer_roller_voltages()->transfer_in();
+      break;
+    case TransferRollerStatus::TRANSFERING_OUT:
+      output_struct.transfer_roller_voltage = robot_constants_->common()
+                                                  ->transfer_roller_voltages()
+                                                  ->transfer_out();
+      break;
+    case TransferRollerStatus::EXTEND_MOVING:
+      output_struct.transfer_roller_voltage = robot_constants_->common()
+                                                  ->transfer_roller_voltages()
+                                                  ->extend_moving();
+      break;
+  }
+
+  // Update Extend Roller voltage based on status from state machine.
+  const ExtendRollerVoltages *extend_roller_voltages =
+      robot_constants_->common()->extend_roller_voltages();
+  switch (extend_roller_status) {
+    case ExtendRollerStatus::IDLE:
+      // No voltage applied when idle
+      output_struct.extend_roller_voltage = 0.0;
+      break;
+    case ExtendRollerStatus::TRANSFERING_TO_EXTEND:
+      output_struct.extend_roller_voltage = extend_roller_voltages->scoring();
+      break;
+    case ExtendRollerStatus::SCORING_IN_AMP:
+      [[fallthrough]];
+    case ExtendRollerStatus::SCORING_IN_TRAP:
+      // Apply scoring voltage during scoring in amp or trap
+      output_struct.extend_roller_voltage = extend_roller_voltages->scoring();
+      break;
+    case ExtendRollerStatus::TRANSFERING_TO_CATAPULT:
+      // Apply scoring voltage during transferring to catapult
+      output_struct.extend_roller_voltage = extend_roller_voltages->scoring();
+      break;
+  }
+
+  double extend_goal_position = 0.0;
+
+  if (unsafe_goal != nullptr && unsafe_goal->note_goal() == NoteGoal::TRAP) {
+    extend_goal_location = ExtendStatus::TRAP;
+  }
+
+  // Set the extend position based on the state machine output
+  switch (extend_goal_location) {
+    case ExtendStatus::RETRACTED:
+      extend_goal_position = extend_set_points->retracted();
+      break;
+    case ExtendStatus::AMP:
+      extend_goal_position = extend_set_points->amp();
+      break;
+    case ExtendStatus::TRAP:
+      extend_goal_position = extend_set_points->trap();
+      break;
+    case ExtendStatus::CATAPULT:
+      extend_goal_position = extend_set_points->catapult();
+      break;
+    case ExtendStatus::MOVING:
+      // Should never happen
+      break;
+  }
+
+  NoteStatus uncompleted_note_goal_status = NoteStatus::NONE;
+
+  switch (requested_note_goal_) {
+    case NoteGoal::NONE:
+      uncompleted_note_goal_status = NoteStatus::NONE;
+      break;
+    case NoteGoal::CATAPULT:
+      uncompleted_note_goal_status = NoteStatus::CATAPULT;
+      break;
+    case NoteGoal::AMP:
+      uncompleted_note_goal_status = NoteStatus::AMP;
+      break;
+    case NoteGoal::TRAP:
+      uncompleted_note_goal_status = NoteStatus::TRAP;
+      break;
+  }
+
+  // Set the extend status based on the state machine output
+  // If the extend is moving, the status is MOVING, otherwise it is the same
+  // as extend_status
+  ExtendStatus extend_status =
+      (extend_moving ? ExtendStatus::MOVING : extend_goal_location);
+
   if (joystick_state_fetcher_.Fetch() &&
       joystick_state_fetcher_->has_alliance()) {
     alliance_ = joystick_state_fetcher_->alliance();
@@ -142,22 +489,11 @@
 
   drivetrain_status_fetcher_.Fetch();
 
-  aos::FlatbufferFixedAllocatorArray<
-      frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemGoal, 512>
-      intake_pivot_goal_buffer;
-
-  intake_pivot_goal_buffer.Finish(
-      frc971::control_loops::CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
-          *intake_pivot_goal_buffer.fbb(), intake_pivot_position));
-
-  const frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemGoal
-      *intake_pivot_goal = &intake_pivot_goal_buffer.message();
-
-  const flatbuffers::Offset<PotAndAbsoluteEncoderProfiledJointStatus>
-      intake_pivot_status_offset = intake_pivot_.Iterate(
-          intake_pivot_goal, position->intake_pivot(),
-          output != nullptr ? &output_struct.intake_pivot_voltage : nullptr,
-          status->fbb());
+  const bool collided = collision_avoidance_.IsCollided({
+      .intake_pivot_position = intake_pivot_.estimated_position(),
+      .turret_position = shooter_.turret().estimated_position(),
+      .extend_position = extend_.estimated_position(),
+  });
 
   aos::FlatbufferFixedAllocatorArray<
       frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemGoal, 512>
@@ -176,21 +512,116 @@
           output != nullptr ? &output_struct.climber_voltage : nullptr,
           status->fbb());
 
+  double max_intake_pivot_position = 0;
+  double min_intake_pivot_position = 0;
+  double max_extend_position = 0;
+  double min_extend_position = 0;
+
+  aos::FlatbufferFixedAllocatorArray<
+      frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemGoal, 512>
+      intake_pivot_goal_buffer;
+
+  intake_pivot_goal_buffer.Finish(
+      frc971::control_loops::CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+          *intake_pivot_goal_buffer.fbb(), intake_pivot_position));
+
+  const frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemGoal
+      *intake_pivot_goal = &intake_pivot_goal_buffer.message();
+
+  double *intake_output =
+      (output != nullptr ? &output_struct.intake_pivot_voltage : nullptr);
+
+  const bool disabled = intake_pivot_.Correct(
+      intake_pivot_goal, position->intake_pivot(), intake_output == nullptr);
+
+  aos::FlatbufferFixedAllocatorArray<
+      frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemGoal, 512>
+      extend_goal_buffer;
+
+  extend_goal_buffer.Finish(
+      frc971::control_loops::CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+          *extend_goal_buffer.fbb(), extend_goal_position));
+
+  const frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemGoal
+      *extend_goal = &extend_goal_buffer.message();
+
+  // TODO(max): Change how we handle the collision with the turret and
+  // intake to be clearer
+  const flatbuffers::Offset<ShooterStatus> shooter_status_offset =
+      shooter_.Iterate(
+          position,
+          unsafe_goal != nullptr ? unsafe_goal->shooter_goal() : nullptr,
+          unsafe_goal != nullptr ? unsafe_goal->fire() : false,
+          output != nullptr ? &output_struct.catapult_voltage : nullptr,
+          output != nullptr ? &output_struct.altitude_voltage : nullptr,
+          output != nullptr ? &output_struct.turret_voltage : nullptr,
+          output != nullptr ? &output_struct.retention_roller_voltage : nullptr,
+          output != nullptr
+              ? &output_struct.retention_roller_stator_current_limit
+              : nullptr,
+          robot_state().voltage_battery(), &collision_avoidance_,
+          extend_goal_position, extend_.estimated_position(),
+          &max_extend_position, &min_extend_position,
+          intake_pivot_.estimated_position(), &max_intake_pivot_position,
+          &min_intake_pivot_position, status->fbb(), timestamp);
+
+  intake_pivot_.set_min_position(min_intake_pivot_position);
+  intake_pivot_.set_max_position(max_intake_pivot_position);
+
+  extend_.set_min_position(min_extend_position);
+  extend_.set_max_position(max_extend_position);
+
+  // Calculate the loops for a cycle.
+  const double voltage = intake_pivot_.UpdateController(disabled);
+
+  intake_pivot_.UpdateObserver(voltage);
+
+  // Write out all the voltages.
+  if (intake_output) {
+    *intake_output = voltage;
+  }
+
+  const flatbuffers::Offset<AbsoluteEncoderProfiledJointStatus>
+      intake_pivot_status_offset = intake_pivot_.MakeStatus(status->fbb());
+
+  const flatbuffers::Offset<PotAndAbsoluteEncoderProfiledJointStatus>
+      extend_status_offset = extend_.Iterate(
+          extend_goal, position->extend(),
+          output != nullptr ? &output_struct.extend_voltage : nullptr,
+          status->fbb());
+
   if (output) {
     output->CheckOk(output->Send(Output::Pack(*output->fbb(), &output_struct)));
   }
 
   Status::Builder status_builder = status->MakeBuilder<Status>();
 
-  const bool zeroed = intake_pivot_.zeroed() && climber_.zeroed();
-  const bool estopped = intake_pivot_.estopped() || climber_.estopped();
+  const bool zeroed = intake_pivot_.zeroed() && climber_.zeroed() &&
+                      shooter_.zeroed() && extend_.zeroed();
+  const bool estopped = intake_pivot_.estopped() || climber_.estopped() ||
+                        shooter_.estopped() || extend_.estopped();
 
   status_builder.add_zeroed(zeroed);
   status_builder.add_estopped(estopped);
-  status_builder.add_intake_roller_state(intake_roller_state);
-  status_builder.add_intake_pivot_state(intake_pivot_status_offset);
-  status_builder.add_transfer_roller_state(transfer_roller_state);
-  status_builder.add_climber_state(climber_status_offset);
+  status_builder.add_intake_roller(intake_roller_state);
+  status_builder.add_intake_pivot(intake_pivot_status_offset);
+  status_builder.add_transfer_roller(transfer_roller_status);
+  status_builder.add_climber(climber_status_offset);
+  status_builder.add_shooter(shooter_status_offset);
+  status_builder.add_collided(collided);
+  status_builder.add_extend_roller(extend_roller_status);
+  status_builder.add_extend_status(extend_status);
+  status_builder.add_extend(extend_status_offset);
+  status_builder.add_state(state_);
+  status_builder.add_uncompleted_note_goal(uncompleted_note_goal_status);
+  status_builder.add_extend_ready_for_transfer(extend_at_retracted);
+  status_builder.add_extend_at_retracted(extend_at_retracted);
+  status_builder.add_turret_ready_for_load(turret_ready_for_load);
+  status_builder.add_altitude_ready_for_load(altitude_ready_for_load);
+  status_builder.add_extend_ready_for_catapult_transfer(
+      extend_ready_for_catapult_transfer);
+  status_builder.add_extend_beambreak(position->extend_beambreak());
+  status_builder.add_catapult_beambreak(position->catapult_beambreak());
 
   (void)status->Send(status_builder.Finish());
 }
diff --git a/y2024/control_loops/superstructure/superstructure.h b/y2024/control_loops/superstructure/superstructure.h
index 88db2e2..1c2d119 100644
--- a/y2024/control_loops/superstructure/superstructure.h
+++ b/y2024/control_loops/superstructure/superstructure.h
@@ -3,6 +3,7 @@
 
 #include "aos/events/event_loop.h"
 #include "aos/json_to_flatbuffer.h"
+#include "aos/time/time.h"
 #include "frc971/constants/constants_sender_lib.h"
 #include "frc971/control_loops/control_loop.h"
 #include "frc971/control_loops/drivetrain/drivetrain_can_position_generated.h"
@@ -11,6 +12,7 @@
 #include "frc971/zeroing/pot_and_absolute_encoder.h"
 #include "y2024/constants.h"
 #include "y2024/constants/constants_generated.h"
+#include "y2024/control_loops/superstructure/shooter.h"
 #include "y2024/control_loops/superstructure/superstructure_goal_generated.h"
 #include "y2024/control_loops/superstructure/superstructure_output_generated.h"
 #include "y2024/control_loops/superstructure/superstructure_position_generated.h"
@@ -21,16 +23,20 @@
 class Superstructure
     : public ::frc971::controls::ControlLoop<Goal, Position, Status, Output> {
  public:
+  using AbsoluteEncoderSubsystem =
+      ::frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystem<
+          ::frc971::zeroing::AbsoluteEncoderZeroingEstimator,
+          ::frc971::control_loops::AbsoluteEncoderProfiledJointStatus>;
+
   using PotAndAbsoluteEncoderSubsystem =
       ::frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystem<
           ::frc971::zeroing::PotAndAbsoluteEncoderZeroingEstimator,
           ::frc971::control_loops::PotAndAbsoluteEncoderProfiledJointStatus>;
 
   explicit Superstructure(::aos::EventLoop *event_loop,
-                          std::shared_ptr<const constants::Values> values,
                           const ::std::string &name = "/superstructure");
 
-  inline const PotAndAbsoluteEncoderSubsystem &intake_pivot() const {
+  inline const AbsoluteEncoderSubsystem &intake_pivot() const {
     return intake_pivot_;
   }
 
@@ -38,6 +44,11 @@
     return climber_;
   }
 
+  inline const Shooter &shooter() const { return shooter_; }
+  inline const PotAndAbsoluteEncoderSubsystem &extend() const {
+    return extend_;
+  }
+
   double robot_velocity() const;
 
  protected:
@@ -46,18 +57,34 @@
                             aos::Sender<Status>::Builder *status) override;
 
  private:
-  std::shared_ptr<const constants::Values> values_;
   frc971::constants::ConstantsFetcher<Constants> constants_fetcher_;
   const Constants *robot_constants_;
   aos::Fetcher<frc971::control_loops::drivetrain::Status>
       drivetrain_status_fetcher_;
   aos::Fetcher<aos::JoystickState> joystick_state_fetcher_;
 
+  CollisionAvoidance collision_avoidance_;
+
   aos::Alliance alliance_ = aos::Alliance::kInvalid;
 
-  TransferRollerGoal transfer_goal_;
-  PotAndAbsoluteEncoderSubsystem intake_pivot_;
+  bool catapult_requested_ = false;
+
+  SuperstructureState state_ = SuperstructureState::IDLE;
+
+  NoteGoal requested_note_goal_ = NoteGoal::NONE;
+
+  aos::monotonic_clock::time_point intake_end_time_ =
+      aos::monotonic_clock::time_point::min();
+
+  aos::monotonic_clock::time_point loading_catapult_start_time_ =
+      aos::monotonic_clock::time_point::min();
+
+  AbsoluteEncoderSubsystem intake_pivot_;
   PotAndAbsoluteEncoderSubsystem climber_;
+
+  Shooter shooter_;
+
+  PotAndAbsoluteEncoderSubsystem extend_;
   DISALLOW_COPY_AND_ASSIGN(Superstructure);
 };
 
diff --git a/y2024/control_loops/superstructure/superstructure_can_position.fbs b/y2024/control_loops/superstructure/superstructure_can_position.fbs
index 6cd3f1e..e809adf 100644
--- a/y2024/control_loops/superstructure/superstructure_can_position.fbs
+++ b/y2024/control_loops/superstructure/superstructure_can_position.fbs
@@ -23,6 +23,23 @@
 
     // CAN Position of the climber falcon
     climber:frc971.control_loops.CANTalonFX (id: 5);
+
+    // CAN Position of the retention roller falcon
+    retention_roller:frc971.control_loops.CANTalonFX (id: 6);
+
+    // CAN Position of the shooter turret falcon
+    turret:frc971.control_loops.CANTalonFX (id: 7);
+
+    // CAN Position of the shooter altitude falcon
+    altitude:frc971.control_loops.CANTalonFX (id: 8);
+
+    // CAN Position of the extend falcon
+    extend:frc971.control_loops.CANTalonFX (id: 9);
+
+    // CAN Position of the extend roller fancon
+    extend_roller:frc971.control_loops.CANTalonFX (id: 10);
+    catapult_one:frc971.control_loops.CANTalonFX (id: 11);
+    catapult_two:frc971.control_loops.CANTalonFX (id: 12);
 }
 
-root_type CANPosition;
\ No newline at end of file
+root_type CANPosition;
diff --git a/y2024/control_loops/superstructure/superstructure_goal.fbs b/y2024/control_loops/superstructure/superstructure_goal.fbs
index bb7b706..42caf9b 100644
--- a/y2024/control_loops/superstructure/superstructure_goal.fbs
+++ b/y2024/control_loops/superstructure/superstructure_goal.fbs
@@ -3,43 +3,60 @@
 
 namespace y2024.control_loops.superstructure;
 
-// Represents goal for intake rollers
-enum IntakeRollerGoal : ubyte {
+// Represents goal for the intake pivot and rollers
+// INTAKE will extend the pivot and turn on the rollers to intake the note.
+// SPIT will extend the pivot and turn on the rollers (in reverse) to spit out the note.
+enum IntakeGoal : ubyte {
     NONE = 0,
-    SPIT = 1,
-    INTAKE = 2,
-}
-
-// Represents goal for pivot on intake
-enum IntakePivotGoal : ubyte {
-    EXTENDED = 0,
-    RETRACTED = 1, 
-}
-
-// Represents goal of transfer rollers
-// TRANSFER_IN is for transfering game piece in from the intake to the shooter
-// TRANSFER_OUT is for transfering game piece out to the intake for spitting
-enum TransferRollerGoal : ubyte {
-    NONE = 0,
-    TRANSFER_IN = 1,
-    TRANSFER_OUT = 2,
+    INTAKE = 1,
+    SPIT = 2,
 }
 
 // Represents goal for climber
 // FULL_EXTEND is for fully extending the climber
-// HALF_EXTEND is for partially extending the climber
 // RETRACT is for retracting the climber
 enum ClimberGoal : ubyte {
     FULL_EXTEND = 0,
-    HALF_EXTEND = 1,
-    RETRACT = 2,
+    RETRACT = 1,
+    STOWED = 2,
 }
 
+table ShooterGoal {
+    catapult_goal:frc971.control_loops.catapult.CatapultGoal (id: 0);
+
+    // If true we ignore the other provided positions
+    auto_aim: bool (id: 1);
+
+    // Position for the turret when we aren't auto aiming
+    turret_position: frc971.control_loops.StaticZeroingSingleDOFProfiledSubsystemGoal (id: 2);
+
+    // Position for the altitude when we aren't auto aiming
+    altitude_position: frc971.control_loops.StaticZeroingSingleDOFProfiledSubsystemGoal (id: 3);
+
+    // If true, we started with the ball loaded and should proceed to that state.
+    preloaded:bool = false (id: 4);
+}
+
+// Represents goal for the note movement through the robot
+// to various scoring positions
+// NONE represents no goal for the note
+// AMP represents the goal to move the note and the extend to the AMP scoring position
+// TRAP represents the goal to move the note and the extend to the TRAP scoring position
+// CATAPULT represents the goal to load the note in the catapult.
+// It will complete the catapult goal before accepting new goals.
+enum NoteGoal : ubyte {
+    NONE = 0,
+    AMP = 1,
+    TRAP = 2,
+    CATAPULT = 3,
+}
+
+
 table Goal {
-    intake_roller_goal:IntakeRollerGoal (id: 0);
-    intake_pivot_goal:IntakePivotGoal (id: 1);
-    catapult_goal:frc971.control_loops.catapult.CatapultGoal (id: 2);
-    transfer_roller_goal:TransferRollerGoal (id: 3);
-    climber_goal:ClimberGoal (id: 4);
+    intake_goal:IntakeGoal = NONE (id: 0);
+    climber_goal:ClimberGoal (id: 1);
+    shooter_goal:ShooterGoal (id: 2);
+    note_goal:NoteGoal (id: 3);
+    fire: bool (id: 4);
 }
 root_type Goal;
diff --git a/y2024/control_loops/superstructure/superstructure_lib_test.cc b/y2024/control_loops/superstructure/superstructure_lib_test.cc
index 8020a4d..56452c8 100644
--- a/y2024/control_loops/superstructure/superstructure_lib_test.cc
+++ b/y2024/control_loops/superstructure/superstructure_lib_test.cc
@@ -5,6 +5,7 @@
 
 #include "aos/events/logging/log_writer.h"
 #include "frc971/control_loops/capped_test_plant.h"
+#include "frc971/control_loops/catapult/catapult_goal_static.h"
 #include "frc971/control_loops/control_loop_test.h"
 #include "frc971/control_loops/position_sensor_sim.h"
 #include "frc971/control_loops/subsystem_simulator.h"
@@ -12,9 +13,13 @@
 #include "frc971/zeroing/absolute_encoder.h"
 #include "y2024/constants/simulated_constants_sender.h"
 #include "y2024/control_loops/drivetrain/drivetrain_dog_motor_plant.h"
+#include "y2024/control_loops/superstructure/altitude/altitude_plant.h"
+#include "y2024/control_loops/superstructure/catapult/catapult_plant.h"
 #include "y2024/control_loops/superstructure/climber/climber_plant.h"
+#include "y2024/control_loops/superstructure/extend/extend_plant.h"
 #include "y2024/control_loops/superstructure/intake_pivot/intake_pivot_plant.h"
 #include "y2024/control_loops/superstructure/superstructure.h"
+#include "y2024/control_loops/superstructure/turret/turret_plant.h"
 
 DEFINE_string(output_folder, "",
               "If set, logs all channels to the provided logfile.");
@@ -33,11 +38,16 @@
 using DrivetrainStatus = ::frc971::control_loops::drivetrain::Status;
 typedef Superstructure::PotAndAbsoluteEncoderSubsystem
     PotAndAbsoluteEncoderSubsystem;
+typedef Superstructure::AbsoluteEncoderSubsystem AbsoluteEncoderSubsystem;
 using PotAndAbsoluteEncoderSimulator =
     frc971::control_loops::SubsystemSimulator<
         frc971::control_loops::PotAndAbsoluteEncoderProfiledJointStatus,
         PotAndAbsoluteEncoderSubsystem::State,
         constants::Values::PotAndAbsEncoderConstants>;
+using AbsoluteEncoderSimulator = frc971::control_loops::SubsystemSimulator<
+    frc971::control_loops::AbsoluteEncoderProfiledJointStatus,
+    AbsoluteEncoderSubsystem::State,
+    constants::Values::AbsoluteEncoderConstants>;
 
 class SuperstructureSimulation {
  public:
@@ -48,30 +58,26 @@
         dt_(dt),
         superstructure_position_sender_(
             event_loop_->MakeSender<Position>("/superstructure")),
+        superstructure_can_position_sender_(
+            event_loop_->MakeSender<CANPosition>("/superstructure/rio")),
         superstructure_status_fetcher_(
             event_loop_->MakeFetcher<Status>("/superstructure")),
         superstructure_output_fetcher_(
             event_loop_->MakeFetcher<Output>("/superstructure")),
-        transfer_beambreak_(false),
+        extend_beambreak_(false),
+        catapult_beambreak_(false),
         intake_pivot_(
             new CappedTestPlant(intake_pivot::MakeIntakePivotPlant()),
             PositionSensorSimulator(simulated_robot_constants->robot()
                                         ->intake_constants()
-                                        ->zeroing_constants()
                                         ->one_revolution_distance()),
             {.subsystem_params =
                  {simulated_robot_constants->common()->intake_pivot(),
-                  simulated_robot_constants->robot()
-                      ->intake_constants()
-                      ->zeroing_constants()},
-             .potentiometer_offset = simulated_robot_constants->robot()
-                                         ->intake_constants()
-                                         ->potentiometer_offset()},
+                  simulated_robot_constants->robot()->intake_constants()}},
             frc971::constants::Range::FromFlatbuffer(
                 simulated_robot_constants->common()->intake_pivot()->range()),
             simulated_robot_constants->robot()
                 ->intake_constants()
-                ->zeroing_constants()
                 ->measured_absolute_position(),
             dt_),
         climber_(new CappedTestPlant(climber::MakeClimberPlant()),
@@ -93,7 +99,87 @@
                      ->climber_constants()
                      ->zeroing_constants()
                      ->measured_absolute_position(),
-                 dt_) {
+                 dt_),
+        catapult_(new CappedTestPlant(catapult::MakeCatapultPlant()),
+                  PositionSensorSimulator(simulated_robot_constants->robot()
+                                              ->catapult_constants()
+                                              ->zeroing_constants()
+                                              ->one_revolution_distance()),
+                  {.subsystem_params =
+                       {simulated_robot_constants->common()->catapult(),
+                        simulated_robot_constants->robot()
+                            ->catapult_constants()
+                            ->zeroing_constants()},
+                   .potentiometer_offset = simulated_robot_constants->robot()
+                                               ->catapult_constants()
+                                               ->potentiometer_offset()},
+                  frc971::constants::Range::FromFlatbuffer(
+                      simulated_robot_constants->common()->catapult()->range()),
+                  simulated_robot_constants->robot()
+                      ->catapult_constants()
+                      ->zeroing_constants()
+                      ->measured_absolute_position(),
+                  dt_),
+        altitude_(new CappedTestPlant(altitude::MakeAltitudePlant()),
+                  PositionSensorSimulator(simulated_robot_constants->robot()
+                                              ->altitude_constants()
+                                              ->zeroing_constants()
+                                              ->one_revolution_distance()),
+                  {.subsystem_params =
+                       {simulated_robot_constants->common()->altitude(),
+                        simulated_robot_constants->robot()
+                            ->altitude_constants()
+                            ->zeroing_constants()},
+                   .potentiometer_offset = simulated_robot_constants->robot()
+                                               ->altitude_constants()
+                                               ->potentiometer_offset()},
+                  frc971::constants::Range::FromFlatbuffer(
+                      simulated_robot_constants->common()->altitude()->range()),
+                  simulated_robot_constants->robot()
+                      ->altitude_constants()
+                      ->zeroing_constants()
+                      ->measured_absolute_position(),
+                  dt_),
+        turret_(
+            new CappedTestPlant(turret::MakeTurretPlant()),
+            PositionSensorSimulator(simulated_robot_constants->robot()
+                                        ->turret_constants()
+                                        ->zeroing_constants()
+                                        ->one_revolution_distance()),
+            {.subsystem_params = {simulated_robot_constants->common()->turret(),
+                                  simulated_robot_constants->robot()
+                                      ->turret_constants()
+                                      ->zeroing_constants()},
+             .potentiometer_offset = simulated_robot_constants->robot()
+                                         ->turret_constants()
+                                         ->potentiometer_offset()},
+            frc971::constants::Range::FromFlatbuffer(
+                simulated_robot_constants->common()->turret()->range()),
+            simulated_robot_constants->robot()
+                ->turret_constants()
+                ->zeroing_constants()
+                ->measured_absolute_position(),
+            dt_),
+        extend_(
+            new CappedTestPlant(extend::MakeExtendPlant()),
+            PositionSensorSimulator(simulated_robot_constants->robot()
+                                        ->extend_constants()
+                                        ->zeroing_constants()
+                                        ->one_revolution_distance()),
+            {.subsystem_params = {simulated_robot_constants->common()->extend(),
+                                  simulated_robot_constants->robot()
+                                      ->extend_constants()
+                                      ->zeroing_constants()},
+             .potentiometer_offset = simulated_robot_constants->robot()
+                                         ->extend_constants()
+                                         ->potentiometer_offset()},
+            frc971::constants::Range::FromFlatbuffer(
+                simulated_robot_constants->common()->extend()->range()),
+            simulated_robot_constants->robot()
+                ->extend_constants()
+                ->zeroing_constants()
+                ->measured_absolute_position(),
+            dt_) {
     intake_pivot_.InitializePosition(
         frc971::constants::Range::FromFlatbuffer(
             simulated_robot_constants->common()->intake_pivot()->range())
@@ -102,6 +188,23 @@
         frc971::constants::Range::FromFlatbuffer(
             simulated_robot_constants->common()->climber()->range())
             .middle());
+    catapult_.InitializePosition(
+        frc971::constants::Range::FromFlatbuffer(
+            simulated_robot_constants->common()->catapult()->range())
+            .middle());
+    altitude_.InitializePosition(
+        frc971::constants::Range::FromFlatbuffer(
+            simulated_robot_constants->common()->altitude()->range())
+            .middle());
+    turret_.InitializePosition(
+        frc971::constants::Range::FromFlatbuffer(
+            simulated_robot_constants->common()->turret()->range())
+            .middle());
+    extend_.InitializePosition(
+        frc971::constants::Range::FromFlatbuffer(
+            simulated_robot_constants->common()->extend()->range())
+            .middle());
+
     phased_loop_handle_ = event_loop_->AddPhasedLoop(
         [this](int) {
           // Skip this the first time.
@@ -111,10 +214,24 @@
 
             intake_pivot_.Simulate(
                 superstructure_output_fetcher_->intake_pivot_voltage(),
-                superstructure_status_fetcher_->intake_pivot_state());
+                superstructure_status_fetcher_->intake_pivot());
 
             climber_.Simulate(superstructure_output_fetcher_->climber_voltage(),
-                              superstructure_status_fetcher_->climber_state());
+                              superstructure_status_fetcher_->climber());
+            catapult_.Simulate(
+                superstructure_output_fetcher_->catapult_voltage(),
+                superstructure_status_fetcher_->shooter()->catapult());
+
+            altitude_.Simulate(
+                superstructure_output_fetcher_->altitude_voltage(),
+                superstructure_status_fetcher_->shooter()->altitude());
+
+            turret_.Simulate(
+                superstructure_output_fetcher_->turret_voltage(),
+                superstructure_status_fetcher_->shooter()->turret());
+
+            extend_.Simulate(superstructure_output_fetcher_->extend_voltage(),
+                             superstructure_status_fetcher_->extend());
           }
           first_ = false;
           SendPositionMessage();
@@ -127,47 +244,85 @@
     ::aos::Sender<Position>::Builder builder =
         superstructure_position_sender_.MakeBuilder();
 
-    frc971::PotAndAbsolutePosition::Builder intake_pivot_builder =
-        builder.MakeBuilder<frc971::PotAndAbsolutePosition>();
-    flatbuffers::Offset<frc971::PotAndAbsolutePosition> intake_pivot_offset =
+    frc971::AbsolutePosition::Builder intake_pivot_builder =
+        builder.MakeBuilder<frc971::AbsolutePosition>();
+    flatbuffers::Offset<frc971::AbsolutePosition> intake_pivot_offset =
         intake_pivot_.encoder()->GetSensorValues(&intake_pivot_builder);
 
     frc971::PotAndAbsolutePosition::Builder climber_builder =
         builder.MakeBuilder<frc971::PotAndAbsolutePosition>();
+
     flatbuffers::Offset<frc971::PotAndAbsolutePosition> climber_offset =
         climber_.encoder()->GetSensorValues(&climber_builder);
+    frc971::PotAndAbsolutePosition::Builder catapult_builder =
+        builder.MakeBuilder<frc971::PotAndAbsolutePosition>();
+
+    flatbuffers::Offset<frc971::PotAndAbsolutePosition> catapult_offset =
+        catapult_.encoder()->GetSensorValues(&catapult_builder);
+
+    frc971::PotAndAbsolutePosition::Builder altitude_builder =
+        builder.MakeBuilder<frc971::PotAndAbsolutePosition>();
+    flatbuffers::Offset<frc971::PotAndAbsolutePosition> altitude_offset =
+        altitude_.encoder()->GetSensorValues(&altitude_builder);
+
+    frc971::PotAndAbsolutePosition::Builder turret_builder =
+        builder.MakeBuilder<frc971::PotAndAbsolutePosition>();
+    flatbuffers::Offset<frc971::PotAndAbsolutePosition> turret_offset =
+        turret_.encoder()->GetSensorValues(&turret_builder);
+
+    frc971::PotAndAbsolutePosition::Builder extend_builder =
+        builder.MakeBuilder<frc971::PotAndAbsolutePosition>();
+    flatbuffers::Offset<frc971::PotAndAbsolutePosition> extend_offset =
+        extend_.encoder()->GetSensorValues(&extend_builder);
 
     Position::Builder position_builder = builder.MakeBuilder<Position>();
 
-    position_builder.add_transfer_beambreak(transfer_beambreak_);
+    position_builder.add_extend_beambreak(extend_beambreak_);
+    position_builder.add_catapult_beambreak(catapult_beambreak_);
     position_builder.add_intake_pivot(intake_pivot_offset);
-
+    position_builder.add_catapult(catapult_offset);
+    position_builder.add_altitude(altitude_offset);
+    position_builder.add_turret(turret_offset);
     position_builder.add_climber(climber_offset);
+    position_builder.add_extend(extend_offset);
+
     CHECK_EQ(builder.Send(position_builder.Finish()),
              aos::RawSender::Error::kOk);
   }
 
-  void set_transfer_beambreak(bool triggered) {
-    transfer_beambreak_ = triggered;
+  void set_extend_beambreak(bool triggered) { extend_beambreak_ = triggered; }
+
+  void set_catapult_beambreak(bool triggered) {
+    catapult_beambreak_ = triggered;
   }
 
-  PotAndAbsoluteEncoderSimulator *intake_pivot() { return &intake_pivot_; }
-
+  AbsoluteEncoderSimulator *intake_pivot() { return &intake_pivot_; }
+  PotAndAbsoluteEncoderSimulator *catapult() { return &catapult_; }
+  PotAndAbsoluteEncoderSimulator *altitude() { return &altitude_; }
+  PotAndAbsoluteEncoderSimulator *turret() { return &turret_; }
   PotAndAbsoluteEncoderSimulator *climber() { return &climber_; }
 
+  PotAndAbsoluteEncoderSimulator *extend() { return &extend_; }
+
  private:
   ::aos::EventLoop *event_loop_;
   const chrono::nanoseconds dt_;
   ::aos::PhasedLoopHandler *phased_loop_handle_ = nullptr;
 
   ::aos::Sender<Position> superstructure_position_sender_;
+  ::aos::Sender<CANPosition> superstructure_can_position_sender_;
   ::aos::Fetcher<Status> superstructure_status_fetcher_;
   ::aos::Fetcher<Output> superstructure_output_fetcher_;
 
-  bool transfer_beambreak_;
+  bool extend_beambreak_;
+  bool catapult_beambreak_;
 
-  PotAndAbsoluteEncoderSimulator intake_pivot_;
+  AbsoluteEncoderSimulator intake_pivot_;
   PotAndAbsoluteEncoderSimulator climber_;
+  PotAndAbsoluteEncoderSimulator catapult_;
+  PotAndAbsoluteEncoderSimulator altitude_;
+  PotAndAbsoluteEncoderSimulator turret_;
+  PotAndAbsoluteEncoderSimulator extend_;
 
   bool first_ = true;
 };
@@ -178,13 +333,12 @@
       : ::frc971::testing::ControlLoopTest(
             aos::configuration::ReadConfig("y2024/aos_config.json"),
             std::chrono::microseconds(5050)),
-        values_(std::make_shared<constants::Values>(constants::MakeValues())),
         simulated_constants_dummy_(SendSimulationConstants(
             event_loop_factory(), 7971, "y2024/constants/test_constants.json")),
         roborio_(aos::configuration::GetNode(configuration(), "roborio")),
         logger_pi_(aos::configuration::GetNode(configuration(), "logger")),
         superstructure_event_loop(MakeEventLoop("Superstructure", roborio_)),
-        superstructure_(superstructure_event_loop.get(), (values_)),
+        superstructure_(superstructure_event_loop.get()),
         test_event_loop_(MakeEventLoop("test", roborio_)),
         constants_fetcher_(test_event_loop_.get()),
         simulated_robot_constants_(
@@ -201,6 +355,8 @@
             test_event_loop_->MakeFetcher<Position>("/superstructure")),
         superstructure_position_sender_(
             test_event_loop_->MakeSender<Position>("/superstructure")),
+        superstructure_can_position_sender_(
+            test_event_loop_->MakeSender<CANPosition>("/superstructure/rio")),
         drivetrain_status_sender_(
             test_event_loop_->MakeSender<DrivetrainStatus>("/drivetrain")),
         superstructure_plant_event_loop_(MakeEventLoop("plant", roborio_)),
@@ -223,30 +379,64 @@
     superstructure_status_fetcher_.Fetch();
     superstructure_output_fetcher_.Fetch();
 
+    ASSERT_FALSE(superstructure_status_fetcher_->estopped());
+
     ASSERT_TRUE(superstructure_goal_fetcher_.get() != nullptr) << ": No goal";
     ASSERT_TRUE(superstructure_status_fetcher_.get() != nullptr)
         << ": No status";
     ASSERT_TRUE(superstructure_output_fetcher_.get() != nullptr)
         << ": No output";
 
-    double set_point = simulated_robot_constants_->common()
-                           ->intake_pivot_set_points()
-                           ->retracted();
+    EXPECT_FALSE(superstructure_status_fetcher_->collided());
 
-    if (superstructure_goal_fetcher_->intake_pivot_goal() ==
-        IntakePivotGoal::EXTENDED) {
+    double set_point =
+        superstructure_status_fetcher_->intake_pivot()->goal_position();
+
+    if (superstructure_goal_fetcher_->intake_goal() == IntakeGoal::INTAKE) {
       set_point = simulated_robot_constants_->common()
                       ->intake_pivot_set_points()
                       ->extended();
     }
 
-    EXPECT_NEAR(
-        set_point,
-        superstructure_status_fetcher_->intake_pivot_state()->position(),
-        0.001);
+    EXPECT_NEAR(set_point,
+                superstructure_status_fetcher_->intake_pivot()->position(),
+                0.03);
 
-    if (superstructure_status_fetcher_->intake_roller_state() ==
-        IntakeRollerState::NONE) {
+    if (superstructure_goal_fetcher_->has_shooter_goal() &&
+        superstructure_status_fetcher_->uncompleted_note_goal() !=
+            NoteStatus::AMP &&
+        superstructure_status_fetcher_->uncompleted_note_goal() !=
+            NoteStatus::TRAP) {
+      if (superstructure_goal_fetcher_->shooter_goal()->has_turret_position() &&
+          !superstructure_goal_fetcher_->shooter_goal()->auto_aim()) {
+        EXPECT_NEAR(
+            superstructure_goal_fetcher_->shooter_goal()
+                ->turret_position()
+                ->unsafe_goal(),
+            superstructure_status_fetcher_->shooter()->turret()->position(),
+            0.001);
+      }
+    }
+
+    if (superstructure_goal_fetcher_->has_shooter_goal()) {
+      if (superstructure_goal_fetcher_->shooter_goal()
+              ->has_altitude_position() &&
+          !superstructure_goal_fetcher_->shooter_goal()->auto_aim()) {
+        EXPECT_NEAR(
+            superstructure_goal_fetcher_->shooter_goal()
+                ->altitude_position()
+                ->unsafe_goal(),
+            superstructure_status_fetcher_->shooter()->altitude()->position(),
+            0.001);
+        EXPECT_NEAR(superstructure_goal_fetcher_->shooter_goal()
+                        ->altitude_position()
+                        ->unsafe_goal(),
+                    superstructure_plant_.altitude()->position(), 0.001);
+      }
+    }
+
+    if (superstructure_status_fetcher_->intake_roller() ==
+        IntakeRollerStatus::NONE) {
       EXPECT_EQ(superstructure_output_fetcher_->intake_roller_voltage(), 0.0);
     }
 
@@ -259,16 +449,38 @@
         set_point = simulated_robot_constants_->common()
                         ->climber_set_points()
                         ->full_extend();
-      } else if (superstructure_goal_fetcher_->climber_goal() ==
-                 ClimberGoal::HALF_EXTEND) {
+      }
+
+      if (superstructure_goal_fetcher_->climber_goal() == ClimberGoal::STOWED) {
         set_point = simulated_robot_constants_->common()
                         ->climber_set_points()
-                        ->half_extend();
+                        ->stowed();
+      }
+      EXPECT_NEAR(set_point,
+                  superstructure_status_fetcher_->climber()->position(), 0.001);
+    }
+
+    if (superstructure_status_fetcher_->has_uncompleted_note_goal()) {
+      double set_point = simulated_robot_constants_->common()
+                             ->extend_set_points()
+                             ->retracted();
+      if (superstructure_status_fetcher_->uncompleted_note_goal() ==
+          NoteStatus::TRAP) {
+        set_point =
+            simulated_robot_constants_->common()->extend_set_points()->trap();
+      } else if (superstructure_status_fetcher_->uncompleted_note_goal() ==
+                 NoteStatus::AMP) {
+        set_point =
+            simulated_robot_constants_->common()->extend_set_points()->amp();
+      } else if (superstructure_status_fetcher_->uncompleted_note_goal() ==
+                 NoteStatus::CATAPULT) {
+        set_point = simulated_robot_constants_->common()
+                        ->extend_set_points()
+                        ->catapult();
       }
 
       EXPECT_NEAR(set_point,
-                  superstructure_status_fetcher_->climber_state()->position(),
-                  0.001);
+                  superstructure_status_fetcher_->extend()->position(), 0.001);
     }
   }
 
@@ -293,6 +505,25 @@
              !superstructure_status_fetcher_.get()->zeroed());
   }
 
+  void WaitUntilNear(double turret_goal, double altitude_goal) {
+    int i = 0;
+    do {
+      i++;
+      RunFor(dt());
+      superstructure_status_fetcher_.Fetch();
+      // 10 Seconds
+
+      ASSERT_LE(i, 10.0 / ::aos::time::DurationInSeconds(dt()));
+
+      // Since there is a delay when sending running, make sure we have a
+      // status before checking it.
+    } while (superstructure_status_fetcher_.get() == nullptr ||
+             std::abs(superstructure_plant_.altitude()->position() -
+                      altitude_goal) > 1e-3 ||
+             std::abs(superstructure_plant_.turret()->position() -
+                      turret_goal) > 1e-3);
+  }
+
   void SendRobotVelocity(double robot_velocity) {
     SendDrivetrainStatus(robot_velocity, {0.0, 0.0}, 0.0);
   }
@@ -311,7 +542,6 @@
     builder.CheckOk(builder.Send(drivetrain_status_builder.Finish()));
   }
 
-  std::shared_ptr<const constants::Values> values_;
   const bool simulated_constants_dummy_;
 
   const aos::Node *const roborio_;
@@ -331,6 +561,7 @@
   ::aos::Fetcher<Output> superstructure_output_fetcher_;
   ::aos::Fetcher<Position> superstructure_position_fetcher_;
   ::aos::Sender<Position> superstructure_position_sender_;
+  ::aos::Sender<CANPosition> superstructure_can_position_sender_;
   ::aos::Sender<DrivetrainStatus> drivetrain_status_sender_;
 
   ::std::unique_ptr<::aos::EventLoop> superstructure_plant_event_loop_;
@@ -349,20 +580,57 @@
   SetEnabled(true);
   WaitUntilZeroed();
 
+  superstructure_plant_.turret()->InitializePosition(
+      frc971::constants::Range::FromFlatbuffer(
+          simulated_robot_constants_->common()->turret()->range())
+          .middle());
+  superstructure_plant_.altitude()->InitializePosition(
+      frc971::constants::Range::FromFlatbuffer(
+          simulated_robot_constants_->common()->altitude()->range())
+          .middle());
+
   {
     auto builder = superstructure_goal_sender_.MakeBuilder();
 
+    flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
+        turret_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+            *builder.fbb(),
+            frc971::constants::Range::FromFlatbuffer(
+                simulated_robot_constants_->common()->turret()->range())
+                .middle());
+
+    flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
+        altitude_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+            *builder.fbb(),
+            frc971::constants::Range::FromFlatbuffer(
+                simulated_robot_constants_->common()->altitude()->range())
+                .middle());
+
+    ShooterGoal::Builder shooter_goal_builder =
+        builder.MakeBuilder<ShooterGoal>();
+
+    shooter_goal_builder.add_turret_position(turret_offset);
+    shooter_goal_builder.add_altitude_position(altitude_offset);
+    shooter_goal_builder.add_auto_aim(false);
+
+    flatbuffers::Offset<ShooterGoal> shooter_goal_offset =
+        shooter_goal_builder.Finish();
+
     Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
-    goal_builder.add_intake_pivot_goal(IntakePivotGoal::RETRACTED);
     goal_builder.add_climber_goal(ClimberGoal::RETRACT);
+    goal_builder.add_shooter_goal(shooter_goal_offset);
+    goal_builder.add_intake_goal(IntakeGoal::NONE);
+    goal_builder.add_note_goal(NoteGoal::NONE);
 
     ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
   }
   RunFor(chrono::seconds(10));
 
-  EXPECT_TRUE(superstructure_output_fetcher_.Fetch());
-
   VerifyNearGoal();
+
+  EXPECT_EQ(superstructure_status_fetcher_->state(), SuperstructureState::IDLE);
+  EXPECT_EQ(superstructure_status_fetcher_->shooter()->catapult_state(),
+            CatapultState::READY);
 }
 
 // Tests that loops can reach a goal.
@@ -371,26 +639,74 @@
   superstructure_plant_.intake_pivot()->InitializePosition(
       frc971::constants::Range::FromFlatbuffer(
           simulated_robot_constants_->common()->intake_pivot()->range())
+          .lower);
+
+  superstructure_plant_.turret()->InitializePosition(
+      frc971::constants::Range::FromFlatbuffer(
+          simulated_robot_constants_->common()->turret()->range())
+          .lower);
+
+  superstructure_plant_.altitude()->InitializePosition(
+      frc971::constants::Range::FromFlatbuffer(
+          simulated_robot_constants_->common()->altitude()->range())
           .middle());
+
   superstructure_plant_.climber()->InitializePosition(
       frc971::constants::Range::FromFlatbuffer(
           simulated_robot_constants_->common()->climber()->range())
           .lower);
+
+  superstructure_plant_.extend()->InitializePosition(
+      frc971::constants::Range::FromFlatbuffer(
+          simulated_robot_constants_->common()->extend()->range())
+          .lower);
   WaitUntilZeroed();
+
   {
     auto builder = superstructure_goal_sender_.MakeBuilder();
 
+    flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
+        turret_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+            *builder.fbb(),
+            frc971::constants::Range::FromFlatbuffer(
+                simulated_robot_constants_->common()->turret()->range())
+                .upper);
+
+    flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
+        altitude_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+            *builder.fbb(),
+            frc971::constants::Range::FromFlatbuffer(
+                simulated_robot_constants_->common()->altitude()->range())
+                .upper);
+
+    ShooterGoal::Builder shooter_goal_builder =
+        builder.MakeBuilder<ShooterGoal>();
+
+    shooter_goal_builder.add_turret_position(turret_offset);
+    shooter_goal_builder.add_altitude_position(altitude_offset);
+    shooter_goal_builder.add_auto_aim(false);
+
+    flatbuffers::Offset<ShooterGoal> shooter_goal_offset =
+        shooter_goal_builder.Finish();
+
     Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
-    goal_builder.add_intake_pivot_goal(IntakePivotGoal::EXTENDED);
+    goal_builder.add_intake_goal(IntakeGoal::INTAKE);
     goal_builder.add_climber_goal(ClimberGoal::FULL_EXTEND);
+    goal_builder.add_shooter_goal(shooter_goal_offset);
+    goal_builder.add_note_goal(NoteGoal::NONE);
 
     ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
   }
 
+  superstructure_plant_.set_extend_beambreak(true);
+
   // Give it a lot of time to get there.
   RunFor(chrono::seconds(15));
 
   VerifyNearGoal();
+
+  EXPECT_EQ(superstructure_status_fetcher_->state(),
+            SuperstructureState::LOADED);
 }
 
 // Makes sure that the voltage on a motor is properly pulled back after
@@ -404,29 +720,93 @@
   {
     auto builder = superstructure_goal_sender_.MakeBuilder();
 
+    flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
+        turret_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+            *builder.fbb(),
+            frc971::constants::Range::FromFlatbuffer(
+                simulated_robot_constants_->common()->turret()->range())
+                .upper);
+
+    flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
+        altitude_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+            *builder.fbb(),
+            frc971::constants::Range::FromFlatbuffer(
+                simulated_robot_constants_->common()->altitude()->range())
+                .upper);
+
+    ShooterGoal::Builder shooter_goal_builder =
+        builder.MakeBuilder<ShooterGoal>();
+
+    shooter_goal_builder.add_turret_position(turret_offset);
+    shooter_goal_builder.add_altitude_position(altitude_offset);
+    shooter_goal_builder.add_auto_aim(false);
+
+    flatbuffers::Offset<ShooterGoal> shooter_goal_offset =
+        shooter_goal_builder.Finish();
+
     Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
-    goal_builder.add_intake_pivot_goal(IntakePivotGoal::EXTENDED);
+    goal_builder.add_intake_goal(IntakeGoal::INTAKE);
     goal_builder.add_climber_goal(ClimberGoal::FULL_EXTEND);
+    goal_builder.add_shooter_goal(shooter_goal_offset);
+    goal_builder.add_note_goal(NoteGoal::AMP);
 
     ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
   }
+  superstructure_plant_.set_extend_beambreak(true);
+
   RunFor(chrono::seconds(20));
   VerifyNearGoal();
 
+  EXPECT_EQ(superstructure_status_fetcher_->state(),
+            SuperstructureState::READY);
+
   // Try a low acceleration move with a high max velocity and verify the
   // acceleration is capped like expected.
   {
     auto builder = superstructure_goal_sender_.MakeBuilder();
 
+    flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
+        turret_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+            *builder.fbb(),
+            frc971::constants::Range::FromFlatbuffer(
+                simulated_robot_constants_->common()->turret()->range())
+                .lower,
+            CreateProfileParameters(*builder.fbb(), 20.0, 10));
+
+    flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
+        altitude_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+            *builder.fbb(),
+            frc971::constants::Range::FromFlatbuffer(
+                simulated_robot_constants_->common()->altitude()->range())
+                .lower,
+            CreateProfileParameters(*builder.fbb(), 20.0, 10));
+
+    ShooterGoal::Builder shooter_goal_builder =
+        builder.MakeBuilder<ShooterGoal>();
+
+    shooter_goal_builder.add_turret_position(turret_offset);
+    shooter_goal_builder.add_altitude_position(altitude_offset);
+    shooter_goal_builder.add_auto_aim(false);
+
+    flatbuffers::Offset<ShooterGoal> shooter_goal_offset =
+        shooter_goal_builder.Finish();
+
     Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
-    goal_builder.add_intake_pivot_goal(IntakePivotGoal::RETRACTED);
+    goal_builder.add_intake_goal(IntakeGoal::NONE);
     goal_builder.add_climber_goal(ClimberGoal::RETRACT);
+    goal_builder.add_shooter_goal(shooter_goal_offset);
+    goal_builder.add_note_goal(NoteGoal::NONE);
 
     ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
   }
 
+  superstructure_plant_.set_extend_beambreak(true);
+
   RunFor(chrono::seconds(10));
   VerifyNearGoal();
+
+  EXPECT_EQ(superstructure_status_fetcher_->state(),
+            SuperstructureState::READY);
 }
 
 // Tests that the loop zeroes when run for a while without a goal.
@@ -435,11 +815,19 @@
   WaitUntilZeroed();
   RunFor(chrono::seconds(2));
 
-  EXPECT_EQ(PotAndAbsoluteEncoderSubsystem::State::RUNNING,
+  EXPECT_EQ(AbsoluteEncoderSubsystem::State::RUNNING,
             superstructure_.intake_pivot().state());
 
   EXPECT_EQ(PotAndAbsoluteEncoderSubsystem::State::RUNNING,
             superstructure_.climber().state());
+
+  EXPECT_EQ(PotAndAbsoluteEncoderSubsystem::State::RUNNING,
+            superstructure_.shooter().turret().state());
+
+  EXPECT_EQ(PotAndAbsoluteEncoderSubsystem::State::RUNNING,
+            superstructure_.shooter().altitude().state());
+  EXPECT_EQ(PotAndAbsoluteEncoderSubsystem::State::RUNNING,
+            superstructure_.extend().state());
 }
 
 // Tests that running disabled works
@@ -448,65 +836,6 @@
   CheckIfZeroed();
 }
 
-// Tests Climber in multiple scenarios
-TEST_F(SuperstructureTest, ClimberTest) {
-  SetEnabled(true);
-  WaitUntilZeroed();
-
-  superstructure_plant_.climber()->InitializePosition(
-      frc971::constants::Range::FromFlatbuffer(
-          simulated_robot_constants_->common()->climber()->range())
-          .middle());
-
-  WaitUntilZeroed();
-
-  {
-    auto builder = superstructure_goal_sender_.MakeBuilder();
-
-    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
-
-    goal_builder.add_climber_goal(ClimberGoal::FULL_EXTEND);
-
-    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
-  }
-
-  RunFor(chrono::seconds(5));
-
-  VerifyNearGoal();
-
-  WaitUntilZeroed();
-
-  {
-    auto builder = superstructure_goal_sender_.MakeBuilder();
-
-    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
-
-    goal_builder.add_climber_goal(ClimberGoal::HALF_EXTEND);
-
-    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
-  }
-
-  RunFor(chrono::seconds(5));
-
-  VerifyNearGoal();
-
-  WaitUntilZeroed();
-
-  {
-    auto builder = superstructure_goal_sender_.MakeBuilder();
-
-    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
-
-    goal_builder.add_climber_goal(ClimberGoal::RETRACT);
-
-    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
-  }
-
-  RunFor(chrono::seconds(5));
-
-  VerifyNearGoal();
-}
-
 // Tests intake and transfer in multiple scenarios
 TEST_F(SuperstructureTest, IntakeGoal) {
   SetEnabled(true);
@@ -524,18 +853,18 @@
 
     Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
 
-    goal_builder.add_intake_pivot_goal(IntakePivotGoal::RETRACTED);
-    goal_builder.add_intake_roller_goal(IntakeRollerGoal::NONE);
+    goal_builder.add_intake_goal(IntakeGoal::NONE);
+    goal_builder.add_note_goal(NoteGoal::NONE);
 
     ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
   }
 
-  superstructure_plant_.set_transfer_beambreak(false);
-
   RunFor(chrono::seconds(5));
 
   VerifyNearGoal();
 
+  EXPECT_EQ(superstructure_status_fetcher_->state(), SuperstructureState::IDLE);
+
   WaitUntilZeroed();
 
   {
@@ -543,14 +872,12 @@
 
     Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
 
-    goal_builder.add_intake_pivot_goal(IntakePivotGoal::EXTENDED);
-    goal_builder.add_intake_roller_goal(IntakeRollerGoal::SPIT);
+    goal_builder.add_intake_goal(IntakeGoal::SPIT);
+    goal_builder.add_note_goal(NoteGoal::NONE);
 
     ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
   }
 
-  superstructure_plant_.set_transfer_beambreak(false);
-
   RunFor(chrono::seconds(5));
 
   VerifyNearGoal();
@@ -565,29 +892,587 @@
 
     Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
 
-    goal_builder.add_intake_pivot_goal(IntakePivotGoal::EXTENDED);
-    goal_builder.add_intake_roller_goal(IntakeRollerGoal::INTAKE);
+    goal_builder.add_intake_goal(IntakeGoal::INTAKE);
+    goal_builder.add_note_goal(NoteGoal::NONE);
 
     ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
   }
 
-  superstructure_plant_.set_transfer_beambreak(false);
-
   RunFor(chrono::seconds(5));
 
   VerifyNearGoal();
 
+  EXPECT_EQ(superstructure_status_fetcher_->state(),
+            SuperstructureState::INTAKING);
+
+  EXPECT_EQ(superstructure_output_fetcher_->transfer_roller_voltage(),
+            simulated_robot_constants_->common()
+                ->transfer_roller_voltages()
+                ->transfer_in());
+
   {
     auto builder = superstructure_goal_sender_.MakeBuilder();
 
     Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
 
-    goal_builder.add_intake_roller_goal(IntakeRollerGoal::INTAKE);
+    goal_builder.add_intake_goal(IntakeGoal::INTAKE);
+    goal_builder.add_note_goal(NoteGoal::NONE);
 
     ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
   }
 
-  superstructure_plant_.set_transfer_beambreak(false);
+  RunFor(chrono::milliseconds(500));
+
+  // Make sure we're still intaking for 500 ms after we stop giving it an
+  // intaking goal.
+  {
+    auto builder = superstructure_goal_sender_.MakeBuilder();
+
+    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+    goal_builder.add_note_goal(NoteGoal::NONE);
+
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
+  }
+
+  RunFor(chrono::milliseconds(200));
+
+  ASSERT_TRUE(superstructure_status_fetcher_.Fetch());
+
+  EXPECT_EQ(superstructure_status_fetcher_->state(),
+            SuperstructureState::INTAKING);
+  EXPECT_EQ(superstructure_status_fetcher_->intake_roller(),
+            IntakeRollerStatus::INTAKING);
+
+  // Make sure we stop when loaded
+  {
+    auto builder = superstructure_goal_sender_.MakeBuilder();
+
+    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+    goal_builder.add_intake_goal(IntakeGoal::INTAKE);
+    goal_builder.add_note_goal(NoteGoal::NONE);
+
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
+  }
+
+  superstructure_plant_.set_extend_beambreak(true);
+
+  RunFor(chrono::seconds(2));
+
+  VerifyNearGoal();
+
+  EXPECT_EQ(superstructure_status_fetcher_->state(),
+            SuperstructureState::LOADED);
+
+  EXPECT_EQ(superstructure_output_fetcher_->transfer_roller_voltage(), 0.0);
+}
+
+// Tests the full range of activities we need to be doing from loading ->
+// shooting
+TEST_F(SuperstructureTest, LoadingToShooting) {
+  SetEnabled(true);
+
+  WaitUntilZeroed();
+
+  constexpr double kTurretGoal = 2.0;
+  constexpr double kAltitudeGoal = 0.55;
+
+  set_alliance(aos::Alliance::kRed);
+  SendDrivetrainStatus(0.0, {0.0, 5.0}, 0.0);
+
+  // Auto aim, but don't fire.
+  {
+    auto builder = superstructure_goal_sender_.MakeBuilder();
+
+    ShooterGoal::Builder shooter_goal_builder =
+        builder.MakeBuilder<ShooterGoal>();
+
+    shooter_goal_builder.add_auto_aim(true);
+
+    flatbuffers::Offset<ShooterGoal> shooter_goal_offset =
+        shooter_goal_builder.Finish();
+
+    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+    goal_builder.add_intake_goal(IntakeGoal::NONE);
+    goal_builder.add_shooter_goal(shooter_goal_offset);
+    goal_builder.add_fire(false);
+
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
+  }
+
+  superstructure_plant_.set_catapult_beambreak(false);
+
+  RunFor(chrono::seconds(5));
+
+  VerifyNearGoal();
+
+  EXPECT_EQ(superstructure_status_fetcher_->state(), SuperstructureState::IDLE);
+
+  EXPECT_NEAR(superstructure_status_fetcher_->shooter()->turret()->position(),
+              simulated_robot_constants_->common()->turret_loading_position(),
+              0.01);
+
+  EXPECT_NEAR(superstructure_status_fetcher_->shooter()->altitude()->position(),
+              simulated_robot_constants_->common()->altitude_loading_position(),
+              0.01);
+
+  EXPECT_EQ(superstructure_status_fetcher_->shooter()->catapult_state(),
+            CatapultState::READY);
+
+  // Now, extend the intake.
+  {
+    auto builder = superstructure_goal_sender_.MakeBuilder();
+
+    ShooterGoal::Builder shooter_goal_builder =
+        builder.MakeBuilder<ShooterGoal>();
+
+    shooter_goal_builder.add_auto_aim(true);
+
+    flatbuffers::Offset<ShooterGoal> shooter_goal_offset =
+        shooter_goal_builder.Finish();
+
+    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+    goal_builder.add_intake_goal(IntakeGoal::INTAKE);
+    goal_builder.add_shooter_goal(shooter_goal_offset);
+    goal_builder.add_fire(false);
+
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
+  }
+
+  RunFor(chrono::seconds(5));
+
+  VerifyNearGoal();
+
+  EXPECT_EQ(superstructure_status_fetcher_->state(),
+            SuperstructureState::INTAKING);
+
+  EXPECT_NEAR(superstructure_status_fetcher_->shooter()->turret()->position(),
+              simulated_robot_constants_->common()->turret_loading_position(),
+              0.01);
+
+  EXPECT_NEAR(superstructure_status_fetcher_->shooter()->altitude()->position(),
+              simulated_robot_constants_->common()->altitude_loading_position(),
+              0.01);
+
+  EXPECT_EQ(superstructure_status_fetcher_->shooter()->catapult_state(),
+            CatapultState::READY);
+
+  {
+    auto builder = superstructure_goal_sender_.MakeBuilder();
+
+    ShooterGoal::Builder shooter_goal_builder =
+        builder.MakeBuilder<ShooterGoal>();
+
+    shooter_goal_builder.add_auto_aim(true);
+
+    flatbuffers::Offset<ShooterGoal> shooter_goal_offset =
+        shooter_goal_builder.Finish();
+
+    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+    goal_builder.add_intake_goal(IntakeGoal::INTAKE);
+    goal_builder.add_shooter_goal(shooter_goal_offset);
+    goal_builder.add_note_goal(NoteGoal::NONE);
+    goal_builder.add_fire(false);
+
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
+  }
+
+  superstructure_plant_.set_extend_beambreak(false);
+
+  RunFor(10 * dt());
+
+  VerifyNearGoal();
+
+  EXPECT_EQ(superstructure_status_fetcher_->state(),
+            SuperstructureState::INTAKING);
+
+  EXPECT_EQ(superstructure_status_fetcher_->shooter()->catapult_state(),
+            CatapultState::READY);
+
+  EXPECT_EQ(superstructure_status_fetcher_->extend_status(),
+            ExtendStatus::RETRACTED);
+
+  EXPECT_EQ(superstructure_status_fetcher_->extend_roller(),
+            ExtendRollerStatus::TRANSFERING_TO_EXTEND);
+
+  EXPECT_LT(4.0, superstructure_output_fetcher_->transfer_roller_voltage());
+  EXPECT_LT(4.0, superstructure_output_fetcher_->extend_roller_voltage());
+
+  superstructure_plant_.set_extend_beambreak(true);
+
+  RunFor(chrono::milliseconds(750));
+
+  VerifyNearGoal();
+
+  EXPECT_EQ(superstructure_status_fetcher_->state(),
+            SuperstructureState::LOADED);
+
+  EXPECT_EQ(superstructure_status_fetcher_->shooter()->catapult_state(),
+            CatapultState::READY);
+
+  EXPECT_EQ(superstructure_status_fetcher_->extend_status(),
+            ExtendStatus::RETRACTED);
+  EXPECT_EQ(superstructure_status_fetcher_->extend_roller(),
+            ExtendRollerStatus::IDLE);
+
+  EXPECT_EQ(superstructure_output_fetcher_->transfer_roller_voltage(), 0.0);
+  EXPECT_EQ(superstructure_output_fetcher_->extend_roller_voltage(), 0.0);
+
+  // Verify we are in the loading position.
+  EXPECT_NEAR(superstructure_status_fetcher_->shooter()->turret()->position(),
+              simulated_robot_constants_->common()->turret_loading_position(),
+              0.01);
+
+  {
+    auto builder = superstructure_goal_sender_.MakeBuilder();
+
+    ShooterGoal::Builder shooter_goal_builder =
+        builder.MakeBuilder<ShooterGoal>();
+
+    shooter_goal_builder.add_auto_aim(true);
+
+    flatbuffers::Offset<ShooterGoal> shooter_goal_offset =
+        shooter_goal_builder.Finish();
+
+    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+    goal_builder.add_shooter_goal(shooter_goal_offset);
+    goal_builder.add_note_goal(NoteGoal::CATAPULT);
+    goal_builder.add_fire(false);
+
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
+  }
+
+  superstructure_plant_.set_extend_beambreak(true);
+
+  RunFor(chrono::milliseconds(500));
+
+  VerifyNearGoal();
+
+  EXPECT_EQ(superstructure_status_fetcher_->state(),
+            SuperstructureState::LOADING_CATAPULT);
+
+  EXPECT_EQ(superstructure_status_fetcher_->shooter()->catapult_state(),
+            CatapultState::READY);
+
+  superstructure_plant_.set_catapult_beambreak(true);
+  // Set retention roller to show that we are loaded.
+  EXPECT_EQ(superstructure_status_fetcher_->extend_status(),
+            ExtendStatus::CATAPULT);
+  EXPECT_EQ(superstructure_status_fetcher_->extend_roller(),
+            ExtendRollerStatus::TRANSFERING_TO_CATAPULT);
+
+  superstructure_plant_.set_extend_beambreak(false);
+
+  RunFor(chrono::seconds(10));
+
+  ASSERT_TRUE(superstructure_status_fetcher_.Fetch());
+
+  EXPECT_EQ(superstructure_status_fetcher_->state(),
+            SuperstructureState::READY);
+
+  EXPECT_EQ(superstructure_status_fetcher_->shooter()->catapult_state(),
+            CatapultState::LOADED);
+
+  EXPECT_EQ(superstructure_status_fetcher_->extend_status(),
+            ExtendStatus::CATAPULT);
+  EXPECT_EQ(superstructure_status_fetcher_->extend_roller(),
+            ExtendRollerStatus::IDLE);
+
+  // Should now be loaded.
+  EXPECT_EQ(superstructure_output_fetcher_->transfer_roller_voltage(), 0.0);
+
+  EXPECT_NEAR(superstructure_status_fetcher_->shooter()->altitude()->position(),
+              simulated_robot_constants_->common()->altitude_loading_position(),
+              0.01);
+
+  // Fire.  Start by triggering a motion and then firing all in 1 go.
+  {
+    auto builder = superstructure_goal_sender_.MakeBuilder();
+
+    flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
+        turret_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+            *builder.fbb(), kTurretGoal);
+
+    flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
+        altitude_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+            *builder.fbb(), kAltitudeGoal);
+
+    auto catapult_builder =
+        builder.MakeBuilder<frc971::control_loops::catapult::CatapultGoal>();
+    catapult_builder.add_shot_velocity(15.0);
+
+    const auto catapult_offset = catapult_builder.Finish();
+
+    ShooterGoal::Builder shooter_goal_builder =
+        builder.MakeBuilder<ShooterGoal>();
+
+    shooter_goal_builder.add_auto_aim(false);
+    shooter_goal_builder.add_catapult_goal(catapult_offset);
+    shooter_goal_builder.add_altitude_position(altitude_offset);
+    shooter_goal_builder.add_turret_position(turret_offset);
+
+    flatbuffers::Offset<ShooterGoal> shooter_goal_offset =
+        shooter_goal_builder.Finish();
+
+    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+    goal_builder.add_intake_goal(IntakeGoal::NONE);
+    goal_builder.add_shooter_goal(shooter_goal_offset);
+    goal_builder.add_fire(true);
+
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
+  }
+
+  // Wait until the bot finishes auto-aiming.
+  WaitUntilNear(kTurretGoal, kAltitudeGoal);
+
+  RunFor(chrono::milliseconds(1000));
+
+  ASSERT_TRUE(superstructure_status_fetcher_.Fetch());
+
+  EXPECT_EQ(superstructure_status_fetcher_->state(),
+            SuperstructureState::FIRING);
+
+  // Make sure it stays at firing for a bit.
+  EXPECT_EQ(superstructure_status_fetcher_->shooter()->catapult_state(),
+            CatapultState::FIRING);
+
+  EXPECT_NEAR(superstructure_status_fetcher_->shooter()->turret()->position(),
+              kTurretGoal, 0.001);
+
+  EXPECT_NEAR(superstructure_status_fetcher_->shooter()->altitude()->position(),
+              kAltitudeGoal, 0.001);
+
+  {
+    auto builder = superstructure_goal_sender_.MakeBuilder();
+
+    ShooterGoal::Builder shooter_goal_builder =
+        builder.MakeBuilder<ShooterGoal>();
+
+    shooter_goal_builder.add_auto_aim(false);
+
+    flatbuffers::Offset<ShooterGoal> shooter_goal_offset =
+        shooter_goal_builder.Finish();
+
+    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+    goal_builder.add_shooter_goal(shooter_goal_offset);
+    goal_builder.add_fire(false);
+
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
+  }
+
+  superstructure_plant_.set_catapult_beambreak(false);
+
+  RunFor(chrono::seconds(5));
+
+  VerifyNearGoal();
+
+  EXPECT_EQ(superstructure_status_fetcher_->state(), SuperstructureState::IDLE);
+
+  EXPECT_EQ(superstructure_status_fetcher_->shooter()->catapult_state(),
+            CatapultState::READY);
+
+  EXPECT_NEAR(superstructure_status_fetcher_->shooter()->turret()->position(),
+              simulated_robot_constants_->common()->turret_loading_position(),
+              0.01);
+
+  EXPECT_NEAR(superstructure_status_fetcher_->shooter()->altitude()->position(),
+              simulated_robot_constants_->common()->altitude_loading_position(),
+              0.01);
+}
+
+// Test that we are able to signal that the note was preloaded
+TEST_F(SuperstructureTest, Preloaded) {
+  // Put the bucket at the starting position.
+  superstructure_plant_.catapult()->InitializePosition(
+      simulated_robot_constants_->common()->catapult_return_position());
+
+  SetEnabled(true);
+  WaitUntilZeroed();
+
+  {
+    auto builder = superstructure_goal_sender_.MakeBuilder();
+
+    ShooterGoal::Builder shooter_goal_builder =
+        builder.MakeBuilder<ShooterGoal>();
+
+    shooter_goal_builder.add_preloaded(true);
+
+    flatbuffers::Offset<ShooterGoal> shooter_goal_offset =
+        shooter_goal_builder.Finish();
+
+    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+    goal_builder.add_shooter_goal(shooter_goal_offset);
+
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
+  }
+
+  RunFor(chrono::milliseconds(200));
+
+  superstructure_status_fetcher_.Fetch();
+  ASSERT_TRUE(superstructure_status_fetcher_.get() != nullptr);
+
+  LOG(INFO) << EnumNameNoteStatus(
+      superstructure_status_fetcher_->uncompleted_note_goal());
+
+  EXPECT_EQ(superstructure_status_fetcher_->state(),
+            SuperstructureState::READY);
+
+  EXPECT_EQ(superstructure_status_fetcher_->shooter()->catapult_state(),
+            CatapultState::LOADED);
+}
+
+// Tests that auto aim works properly for the shooter
+TEST_F(SuperstructureTest, AutoAim) {
+  superstructure_plant_.catapult()->InitializePosition(
+      simulated_robot_constants_->common()->catapult_return_position());
+  SetEnabled(true);
+  WaitUntilZeroed();
+
+  constexpr double kDistanceFromSpeaker = 5.0;
+
+  const double kRedSpeakerX = simulated_robot_constants_->common()
+                                  ->shooter_targets()
+                                  ->red_alliance()
+                                  ->pos()
+                                  ->data()
+                                  ->Get(0);
+  const double kRedSpeakerY = simulated_robot_constants_->common()
+                                  ->shooter_targets()
+                                  ->red_alliance()
+                                  ->pos()
+                                  ->data()
+                                  ->Get(1);
+
+  const double kBlueSpeakerX = simulated_robot_constants_->common()
+                                   ->shooter_targets()
+                                   ->blue_alliance()
+                                   ->pos()
+                                   ->data()
+                                   ->Get(0);
+  const double kBlueSpeakerY = simulated_robot_constants_->common()
+                                   ->shooter_targets()
+                                   ->blue_alliance()
+                                   ->pos()
+                                   ->data()
+                                   ->Get(1);
+
+  set_alliance(aos::Alliance::kRed);
+  // Set the robot facing 90 degrees away from the speaker
+  SendDrivetrainStatus(
+      0.0, {(kRedSpeakerX - kDistanceFromSpeaker), kRedSpeakerY}, M_PI_2);
+
+  {
+    auto builder = superstructure_goal_sender_.MakeBuilder();
+
+    ShooterGoal::Builder shooter_goal_builder =
+        builder.MakeBuilder<ShooterGoal>();
+
+    shooter_goal_builder.add_auto_aim(true);
+    shooter_goal_builder.add_preloaded(true);
+
+    flatbuffers::Offset<ShooterGoal> shooter_goal_offset =
+        shooter_goal_builder.Finish();
+
+    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+    goal_builder.add_shooter_goal(shooter_goal_offset);
+
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
+  }
+
+  superstructure_plant_.set_catapult_beambreak(true);
+
+  RunFor(chrono::seconds(5));
+
+  VerifyNearGoal();
+
+  EXPECT_EQ(superstructure_status_fetcher_->state(),
+            SuperstructureState::READY);
+
+  EXPECT_NEAR(
+      -M_PI_2,
+      superstructure_status_fetcher_->shooter()->aimer()->turret_position() -
+          M_PI,
+      5e-4);
+  EXPECT_NEAR(
+      -M_PI_2,
+      superstructure_status_fetcher_->shooter()->turret()->position() - M_PI,
+      5e-4);
+
+  EXPECT_EQ(
+      kDistanceFromSpeaker,
+      superstructure_status_fetcher_->shooter()->aimer()->target_distance());
+
+  set_alliance(aos::Alliance::kBlue);
+  // Set the robot facing 90 degrees away from the speaker
+  SendDrivetrainStatus(
+      0.0, {(kBlueSpeakerX + kDistanceFromSpeaker), kBlueSpeakerY}, M_PI_2);
+
+  {
+    auto builder = superstructure_goal_sender_.MakeBuilder();
+
+    ShooterGoal::Builder shooter_goal_builder =
+        builder.MakeBuilder<ShooterGoal>();
+
+    shooter_goal_builder.add_auto_aim(true);
+
+    flatbuffers::Offset<ShooterGoal> shooter_goal_offset =
+        shooter_goal_builder.Finish();
+
+    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+    goal_builder.add_shooter_goal(shooter_goal_offset);
+
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
+  }
+
+  superstructure_plant_.set_catapult_beambreak(true);
+
+  RunFor(chrono::seconds(5));
+
+  VerifyNearGoal();
+
+  EXPECT_EQ(superstructure_status_fetcher_->state(),
+            SuperstructureState::READY);
+
+  EXPECT_NEAR(
+      M_PI_2,
+      superstructure_status_fetcher_->shooter()->aimer()->turret_position() +
+          M_PI,
+      5e-4);
+  EXPECT_NEAR(
+      M_PI_2,
+      superstructure_status_fetcher_->shooter()->turret()->position() + M_PI,
+      5e-4);
+  EXPECT_EQ(
+      kDistanceFromSpeaker,
+      superstructure_status_fetcher_->shooter()->aimer()->target_distance());
+}
+
+// Test entire sequence of loading, transfering, and scoring at amp position.
+TEST_F(SuperstructureTest, ScoreInAmp) {
+  SetEnabled(true);
+
+  WaitUntilZeroed();
+
+  {
+    auto builder = superstructure_goal_sender_.MakeBuilder();
+
+    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+    goal_builder.add_intake_goal(IntakeGoal::INTAKE);
+    goal_builder.add_note_goal(NoteGoal::NONE);
+
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
+  }
 
   RunFor(chrono::seconds(5));
 
@@ -598,12 +1483,167 @@
                 ->transfer_roller_voltages()
                 ->transfer_in());
 
-  superstructure_plant_.set_transfer_beambreak(true);
+  EXPECT_EQ(superstructure_status_fetcher_->extend_roller(),
+            ExtendRollerStatus::TRANSFERING_TO_EXTEND);
 
-  RunFor(chrono::seconds(2));
+  EXPECT_EQ(superstructure_status_fetcher_->state(),
+            SuperstructureState::INTAKING);
+
+  {
+    auto builder = superstructure_goal_sender_.MakeBuilder();
+
+    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+    goal_builder.add_intake_goal(IntakeGoal::INTAKE);
+    goal_builder.add_note_goal(NoteGoal::NONE);
+
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
+  }
+
+  superstructure_plant_.set_extend_beambreak(true);
+
+  RunFor(chrono::milliseconds(10));
 
   VerifyNearGoal();
 
   EXPECT_EQ(superstructure_output_fetcher_->transfer_roller_voltage(), 0.0);
+
+  EXPECT_EQ(superstructure_status_fetcher_->extend_roller(),
+            ExtendRollerStatus::IDLE);
+
+  EXPECT_EQ(superstructure_status_fetcher_->state(),
+            SuperstructureState::LOADED);
+
+  {
+    auto builder = superstructure_goal_sender_.MakeBuilder();
+
+    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+    goal_builder.add_intake_goal(IntakeGoal::NONE);
+    goal_builder.add_note_goal(NoteGoal::AMP);
+
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
+  }
+
+  RunFor(10 * dt());
+
+  ASSERT_TRUE(superstructure_status_fetcher_.Fetch());
+
+  EXPECT_EQ(superstructure_status_fetcher_->extend_status(),
+            ExtendStatus::MOVING);
+
+  RunFor(chrono::seconds(5));
+
+  ASSERT_TRUE(superstructure_status_fetcher_.Fetch());
+
+  EXPECT_NEAR(superstructure_status_fetcher_->extend()->position(),
+              simulated_robot_constants_->common()->extend_set_points()->amp(),
+              0.01);
+  EXPECT_EQ(superstructure_status_fetcher_->extend_status(), ExtendStatus::AMP);
+
+  EXPECT_EQ(superstructure_status_fetcher_->state(),
+            SuperstructureState::READY);
+
+  {
+    auto builder = superstructure_goal_sender_.MakeBuilder();
+
+    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+    goal_builder.add_intake_goal(IntakeGoal::NONE);
+    goal_builder.add_note_goal(NoteGoal::AMP);
+    goal_builder.add_fire(true);
+
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
+  }
+
+  superstructure_plant_.set_extend_beambreak(true);
+
+  RunFor(chrono::milliseconds(10));
+
+  ASSERT_TRUE(superstructure_status_fetcher_.Fetch());
+  ASSERT_TRUE(superstructure_output_fetcher_.Fetch());
+
+  EXPECT_EQ(superstructure_status_fetcher_->state(),
+            SuperstructureState::FIRING);
+
+  EXPECT_EQ(superstructure_status_fetcher_->extend_roller(),
+            ExtendRollerStatus::SCORING_IN_AMP);
+  EXPECT_LT(4.0, superstructure_output_fetcher_->extend_roller_voltage());
+
+  {
+    auto builder = superstructure_goal_sender_.MakeBuilder();
+
+    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+    goal_builder.add_intake_goal(IntakeGoal::NONE);
+    goal_builder.add_note_goal(NoteGoal::AMP);
+    goal_builder.add_fire(false);
+
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
+  }
+
+  superstructure_plant_.set_extend_beambreak(false);
+
+  RunFor(chrono::milliseconds(100));
+
+  ASSERT_TRUE(superstructure_status_fetcher_.Fetch());
+  ASSERT_TRUE(superstructure_output_fetcher_.Fetch());
+
+  EXPECT_EQ(superstructure_status_fetcher_->state(), SuperstructureState::IDLE);
+
+  EXPECT_EQ(superstructure_status_fetcher_->extend_roller(),
+            ExtendRollerStatus::IDLE);
+  EXPECT_EQ(superstructure_output_fetcher_->extend_roller_voltage(), 0.0);
 }
-}  // namespace y2024::control_loops::superstructure::testing
\ No newline at end of file
+
+TEST_F(SuperstructureTest, Climbing) {
+  SetEnabled(true);
+
+  WaitUntilZeroed();
+
+  {
+    auto builder = superstructure_goal_sender_.MakeBuilder();
+
+    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+    goal_builder.add_climber_goal(ClimberGoal::STOWED);
+
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
+  }
+
+  RunFor(chrono::seconds(10));
+
+  VerifyNearGoal();
+
+  {
+    auto builder = superstructure_goal_sender_.MakeBuilder();
+
+    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+    goal_builder.add_climber_goal(ClimberGoal::FULL_EXTEND);
+
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
+  }
+
+  RunFor(chrono::seconds(10));
+
+  VerifyNearGoal();
+
+  {
+    auto builder = superstructure_goal_sender_.MakeBuilder();
+
+    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+    goal_builder.add_climber_goal(ClimberGoal::RETRACT);
+
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
+  }
+
+  RunFor(chrono::seconds(10));
+
+  VerifyNearGoal();
+
+  // TODO(max): Fill this with the logic to move the altitude and turret on the
+  // chain.
+}
+}  // namespace y2024::control_loops::superstructure::testing
diff --git a/y2024/control_loops/superstructure/superstructure_main.cc b/y2024/control_loops/superstructure/superstructure_main.cc
index 58932c7..a5ab8ed 100644
--- a/y2024/control_loops/superstructure/superstructure_main.cc
+++ b/y2024/control_loops/superstructure/superstructure_main.cc
@@ -17,10 +17,7 @@
 
   frc971::constants::WaitForConstants<y2024::Constants>(&config.message());
 
-  std::shared_ptr<const y2024::constants::Values> values =
-      std::make_shared<const y2024::constants::Values>(
-          y2024::constants::MakeValues());
-  Superstructure superstructure(&event_loop, values);
+  Superstructure superstructure(&event_loop);
 
   event_loop.Run();
 
diff --git a/y2024/control_loops/superstructure/superstructure_output.fbs b/y2024/control_loops/superstructure/superstructure_output.fbs
index a429976..24970d0 100644
--- a/y2024/control_loops/superstructure/superstructure_output.fbs
+++ b/y2024/control_loops/superstructure/superstructure_output.fbs
@@ -2,18 +2,24 @@
 
 table Output {
     // Voltage of rollers on intake
+    // Positive means intaking a game piece.
     intake_roller_voltage:double (id: 0);
 
     // Voltage of intake pivot
+    // Positive voltage causes pivot to retract.
     intake_pivot_voltage:double (id: 1);
 
     // Voltage of the turret
+    // Positive voltage is for rotating the turret clockwise.
     turret_voltage: double (id: 2);
 
     // Voltage of the altitude
+    // Positive voltage is for raising it upward.
     altitude_voltage: double (id: 3);
 
     // Voltage of the catapult
+    // Positive voltage is for shooting the game piece,
+    // rotating the catapult counter-clockwise.
     catapult_voltage: double (id: 4);
 
     // Voltage of transfer rollers
@@ -25,6 +31,23 @@
     // Positive voltage is for climber up
     // Negative voltage is for climber down
     climber_voltage:double (id: 6);
+
+    // Voltage of the retention rollers
+    // Positive voltage will hold the game piece in the catapult.
+    retention_roller_voltage: double (id: 7);
+
+    // Voltage of extend
+    // Positive voltage extends the extend
+    // Negative voltage retracts the extend
+    extend_voltage:double (id: 8);
+
+    // Voltage of the extend rollers
+    // Positive voltage rolls the game piece up towards the scoring mechanisms,
+    // either the catapult or amp/trap mechanism
+    extend_roller_voltage:double (id: 9);
+
+    // Dynamically sets the stator current limit for the retention_rollers
+    retention_roller_stator_current_limit: double (id: 10);
 }
 
 root_type Output;
diff --git a/y2024/control_loops/superstructure/superstructure_plotter.ts b/y2024/control_loops/superstructure/superstructure_plotter.ts
index 3498f77..3a2dcaa 100644
--- a/y2024/control_loops/superstructure/superstructure_plotter.ts
+++ b/y2024/control_loops/superstructure/superstructure_plotter.ts
@@ -1,5 +1,5 @@
 // Provides a plot for debugging robot state-related issues.
-import {AosPlotter} from '../../../aos/network/www/aos_plotter';
+import {AosPlotter, MessageHandler} from '../../../aos/network/www/aos_plotter';
 import {BLUE, BROWN, CYAN, GREEN, PINK, RED, WHITE} from '../../../aos/network/www/colors';
 import * as proxy from '../../../aos/network/www/proxy';
 
@@ -7,24 +7,214 @@
 
 const TIME = AosPlotter.TIME;
 const DEFAULT_WIDTH = AosPlotter.DEFAULT_WIDTH * 2;
-const DEFAULT_HEIGHT = AosPlotter.DEFAULT_HEIGHT * 3;
+const DEFAULT_HEIGHT = AosPlotter.DEFAULT_HEIGHT * 1;
+
+function plotSzsdofSubsystem(
+    name: string, plotter: AosPlotter, element: Element, position: MessageHandler, positionName: string,
+    status: MessageHandler, statusName: string, output: MessageHandler, outputName: string, hasPot:boolean = true): void {
+  {
+    const positionPlot =
+        plotter.addPlot(element, [DEFAULT_WIDTH, DEFAULT_HEIGHT]);
+    positionPlot.plot.getAxisLabels().setTitle(name + ' Position');
+    positionPlot.plot.getAxisLabels().setXLabel(TIME);
+    positionPlot.plot.getAxisLabels().setYLabel('Position [rad,m]');
+    positionPlot.addMessageLine(position, [positionName, 'encoder'])
+        .setColor(RED);
+    positionPlot.addMessageLine(position, [positionName, 'absolute_encoder'])
+        .setColor(GREEN);
+    if (hasPot) {
+      positionPlot.addMessageLine(position, [positionName, 'pot'])
+          .setColor(BLUE);
+    }
+    positionPlot
+        .addMessageLine(status, [statusName, 'estimator_state', 'position'])
+        .setColor(BROWN);
+    positionPlot.addMessageLine(status, [statusName, 'position'])
+        .setColor(WHITE);
+  }
+  {
+    const statesPlot =
+        plotter.addPlot(element, [DEFAULT_WIDTH, DEFAULT_HEIGHT / 2]);
+    statesPlot.plot.getAxisLabels().setTitle(name + ' State');
+    statesPlot.plot.getAxisLabels().setXLabel(TIME);
+    statesPlot.plot.getAxisLabels().setYLabel('[bool,ZeroingError]');
+    statesPlot.addMessageLine(status, [statusName, 'estopped']).setColor(RED);
+    statesPlot.addMessageLine(status, [statusName, 'zeroed']).setColor(GREEN);
+    statesPlot
+        .addMessageLine(status, [statusName, 'estimator_state', 'errors[]'])
+        .setColor(BLUE)
+        .setDrawLine(false);
+  }
+  {
+    const positionConvergencePlot =
+        plotter.addPlot(element, [DEFAULT_WIDTH, DEFAULT_HEIGHT]);
+    positionConvergencePlot.plot.getAxisLabels().setTitle(name + ' Position Goals');
+    positionConvergencePlot.plot.getAxisLabels().setXLabel(TIME);
+    positionConvergencePlot.plot.getAxisLabels().setYLabel('[rad,m]');
+    positionConvergencePlot.addMessageLine(status, [statusName, 'position'])
+        .setColor(RED);
+    positionConvergencePlot.addMessageLine(status, [statusName, 'goal_position'])
+        .setColor(GREEN);
+    positionConvergencePlot
+        .addMessageLine(status, [statusName, 'unprofiled_goal_position'])
+        .setColor(BROWN);
+  }
+  {
+    const velocityConvergencePlot =
+        plotter.addPlot(element, [DEFAULT_WIDTH, DEFAULT_HEIGHT]);
+    velocityConvergencePlot.plot.getAxisLabels().setTitle(name + ' Velocity Goals');
+    velocityConvergencePlot.plot.getAxisLabels().setXLabel(TIME);
+    velocityConvergencePlot.plot.getAxisLabels().setYLabel('[rad,m]');
+    velocityConvergencePlot.addMessageLine(status, [statusName, 'velocity'])
+        .setColor(RED);
+    velocityConvergencePlot.addMessageLine(status, [statusName, 'calculated_velocity'])
+        .setColor(RED).setDrawLine(false);
+    velocityConvergencePlot.addMessageLine(status, [statusName, 'goal_velocity'])
+        .setColor(GREEN);
+    velocityConvergencePlot
+        .addMessageLine(status, [statusName, 'unprofiled_goal_velocity'])
+        .setColor(BROWN);
+  }
+  {
+    const outputPlot =
+        plotter.addPlot(element, [DEFAULT_WIDTH, DEFAULT_HEIGHT]);
+    outputPlot.plot.getAxisLabels().setTitle(name + ' Outputs');
+    outputPlot.plot.getAxisLabels().setXLabel(TIME);
+    outputPlot.plot.getAxisLabels().setYLabel('[volts]');
+    outputPlot.addMessageLine(output, [outputName])
+        .setColor(RED);
+    outputPlot.addMessageLine(status, [statusName, 'voltage_error'])
+        .setColor(GREEN);
+    outputPlot.addMessageLine(status, [statusName, 'position_power'])
+        .setColor(BLUE);
+    outputPlot.addMessageLine(status, [statusName, 'velocity_power'])
+        .setColor(BROWN);
+    outputPlot.addMessageLine(status, [statusName, 'feedforwards_power'])
+        .setColor(WHITE);
+  }
+}
 
 export function plotSuperstructure(conn: Connection, element: Element): void {
   const aosPlotter = new AosPlotter(conn);
-  //const goal = aosPlotter.addMessageSource(
-  //    '/superstructure', 'y2024.control_loops.superstructure.Goal');
-  //const output = aosPlotter.addMessageSource(
-  //    '/superstructure', 'y2024.control_loops.superstructure.Output');
-  //const status = aosPlotter.addMessageSource(
-  //    '/superstructure', 'y2024.control_loops.superstructure.Status');
+  const status = aosPlotter.addMessageSource(
+      '/superstructure', 'y2024.control_loops.superstructure.Status');
+  const robotState = aosPlotter.addMessageSource('/aos', 'aos.RobotState');
+
+  {
+    const robotStatePlot =
+        aosPlotter.addPlot(element, [DEFAULT_WIDTH, DEFAULT_HEIGHT]);
+    robotStatePlot.plot.getAxisLabels().setTitle('Robot State Plot');
+    robotStatePlot.plot.getAxisLabels().setXLabel(TIME);
+    robotStatePlot.plot.getAxisLabels().setYLabel('[bool]');
+    robotStatePlot.addMessageLine(robotState, ['outputs_enabled'])
+        .setColor(RED);
+    robotStatePlot.addMessageLine(status, ['zeroed'])
+        .setColor(GREEN);
+    robotStatePlot.addMessageLine(status, ['estopped'])
+        .setColor(BLUE);
+  }
+}
+
+export function plotClimber(conn: Connection, element: Element): void {
+  const aosPlotter = new AosPlotter(conn);
+  const goal = aosPlotter.addMessageSource(
+      '/superstructure', 'y2024.control_loops.superstructure.Goal');
+  const output = aosPlotter.addMessageSource(
+      '/superstructure', 'y2024.control_loops.superstructure.Output');
+  const status = aosPlotter.addMessageSource(
+      '/superstructure', 'y2024.control_loops.superstructure.Status');
   const position = aosPlotter.addMessageSource(
       '/superstructure', 'y2024.control_loops.superstructure.Position');
-  //const robotState = aosPlotter.addMessageSource('/aos', 'aos.RobotState');
+  {
+    const goalPlot =
+        aosPlotter.addPlot(element, [DEFAULT_WIDTH, DEFAULT_HEIGHT]);
+    goalPlot.plot.getAxisLabels().setTitle('Climber Goal');
+    goalPlot.plot.getAxisLabels().setXLabel(TIME);
+    goalPlot.plot.getAxisLabels().setYLabel('[enum]');
+    goalPlot.addMessageLine(goal, ['climber_goal']).setColor(RED);
+  }
 
-  const positionPlot =
-      aosPlotter.addPlot(element, [DEFAULT_WIDTH, DEFAULT_HEIGHT / 2]);
-  positionPlot.plot.getAxisLabels().setTitle('States');
-  positionPlot.plot.getAxisLabels().setXLabel(TIME);
-  positionPlot.plot.getAxisLabels().setYLabel('wonky state units');
-  positionPlot.plot.setDefaultYRange([-1.0, 2.0]);
+  plotSzsdofSubsystem(
+      'Climber', aosPlotter, element, position, 'climber', status, 'climber',
+      output, 'climber_voltage');
+}
+
+export function plotIntake(conn: Connection, element: Element): void {
+  const aosPlotter = new AosPlotter(conn);
+  const goal = aosPlotter.addMessageSource(
+      '/superstructure', 'y2024.control_loops.superstructure.Goal');
+  const output = aosPlotter.addMessageSource(
+      '/superstructure', 'y2024.control_loops.superstructure.Output');
+  const status = aosPlotter.addMessageSource(
+      '/superstructure', 'y2024.control_loops.superstructure.Status');
+  const position = aosPlotter.addMessageSource(
+      '/superstructure', 'y2024.control_loops.superstructure.Position');
+
+  {
+    const goalPlot =
+        aosPlotter.addPlot(element, [DEFAULT_WIDTH, DEFAULT_HEIGHT]);
+    goalPlot.plot.getAxisLabels().setTitle('Intake Goal');
+    goalPlot.plot.getAxisLabels().setXLabel(TIME);
+    goalPlot.plot.getAxisLabels().setYLabel('[enum]');
+    goalPlot.addMessageLine(goal, ['intake_goal']).setColor(RED);
+  }
+  {
+    const rollerPlot =
+        aosPlotter.addPlot(element, [DEFAULT_WIDTH, DEFAULT_HEIGHT]);
+    rollerPlot.plot.getAxisLabels().setTitle('Intake Rollers');
+    rollerPlot.plot.getAxisLabels().setXLabel(TIME);
+    rollerPlot.plot.getAxisLabels().setYLabel('[enum,voltage]');
+    rollerPlot.addMessageLine(status, ['intake_roller']).setColor(RED);
+    rollerPlot.addMessageLine(status, ['transfer_roller']).setColor(BLUE);
+    rollerPlot.addMessageLine(output, ['intake_roller_voltage'])
+        .setColor(RED)
+        .setPointSize(0);
+    rollerPlot.addMessageLine(output, ['transfer_roller_voltage'])
+        .setColor(BLUE)
+        .setPointSize(0);
+  }
+
+  plotSzsdofSubsystem(
+      'Intake', aosPlotter, element, position, 'intake_pivot', status, 'intake_pivot',
+      output, 'intake_pivot_voltage', false);
+}
+
+export function plotExtend(conn: Connection, element: Element): void {
+  const aosPlotter = new AosPlotter(conn);
+  const goal = aosPlotter.addMessageSource(
+      '/superstructure', 'y2024.control_loops.superstructure.Goal');
+  const output = aosPlotter.addMessageSource(
+      '/superstructure', 'y2024.control_loops.superstructure.Output');
+  const status = aosPlotter.addMessageSource(
+      '/superstructure', 'y2024.control_loops.superstructure.Status');
+  const position = aosPlotter.addMessageSource(
+      '/superstructure', 'y2024.control_loops.superstructure.Position');
+
+  {
+    const goalPlot =
+        aosPlotter.addPlot(element, [DEFAULT_WIDTH, DEFAULT_HEIGHT]);
+    goalPlot.plot.getAxisLabels().setTitle('Extend Goal');
+    goalPlot.plot.getAxisLabels().setXLabel(TIME);
+    goalPlot.plot.getAxisLabels().setYLabel('[enum]');
+    goalPlot.addMessageLine(goal, ['intake_goal']).setColor(RED);
+  }
+  {
+    const rollerPlot =
+        aosPlotter.addPlot(element, [DEFAULT_WIDTH, DEFAULT_HEIGHT]);
+    rollerPlot.plot.getAxisLabels().setTitle('Extend Rollers');
+    rollerPlot.plot.getAxisLabels().setXLabel(TIME);
+    rollerPlot.plot.getAxisLabels().setYLabel('[enum,voltage]');
+    rollerPlot.addMessageLine(status, ['intake_roller']).setColor(RED);
+    rollerPlot.addMessageLine(status, ['transfer_roller']).setColor(BLUE);
+    rollerPlot.addMessageLine(output, ['intake_roller_voltage'])
+        .setColor(RED)
+        .setPointSize(0);
+    rollerPlot.addMessageLine(output, ['transfer_roller_voltage'])
+        .setColor(BLUE)
+        .setPointSize(0);
+  }
+
+  plotSzsdofSubsystem(
+      'Extend', aosPlotter, element, position, 'extend', status, 'extend',
+      output, 'extend_voltage');
 }
diff --git a/y2024/control_loops/superstructure/superstructure_position.fbs b/y2024/control_loops/superstructure/superstructure_position.fbs
index cd12da0..b1facba 100644
--- a/y2024/control_loops/superstructure/superstructure_position.fbs
+++ b/y2024/control_loops/superstructure/superstructure_position.fbs
@@ -5,23 +5,44 @@
 
 table Position {
     // Values of the encoder and potentiometer at the intake pivot
-    intake_pivot:frc971.PotAndAbsolutePosition (id: 0);
+    // Zero is when the lowest extent of the lexan is level with the
+    // bellypan, positive is retracted inward.
+    intake_pivot:frc971.AbsolutePosition (id: 0);
 
     // Values of the encoder and potentiometer at the turret
+    // Zero is facing backwards, positive is rotated counter-clockwise.
+    // I.e., zero is at approximately the loading position for getting
+    // the game piece from the extend into the catapult.
     turret:frc971.PotAndAbsolutePosition (id: 1);
 
     // Values of the encoder and potentiometer at the altitude
+    // Zero is level with the ground, positive is raised upward.
     altitude:frc971.PotAndAbsolutePosition (id: 2);
 
     // Values of the encoder and potentiometer at the catapult
+    // Zero is when the note is fully seated in the catapult and the catapult
+    // arm is just touching the note. Positive is rotated counter-clockwise, to
+    // launch game piece.
     catapult:frc971.PotAndAbsolutePosition (id: 3);
 
     // True means there is a game piece in the transfer.
     transfer_beambreak:bool (id: 4);
 
     // Values of the encoder and potentiometer at the climber.
-    // Zero is fully retracted, positive is extended upward.
+    // Zero is fully extended, with top of the highest slider aligned with the
+    // caps on the tubes for the climber.
+    // Positive is more extended.
     climber:frc971.PotAndAbsolutePosition (id: 5);
+
+    // True if there is a game piece in the catapult
+    catapult_beambreak:bool (id: 6);
+
+    // Values of the encoder and potentiometer at the extend motor
+    // Zero is fully retracted, positive is extended upward.
+    extend:frc971.PotAndAbsolutePosition (id: 7);
+
+    // True means there is a game piece in the extend.
+    extend_beambreak:bool (id: 8);
 }
 
 root_type Position;
diff --git a/y2024/control_loops/superstructure/superstructure_replay.cc b/y2024/control_loops/superstructure/superstructure_replay.cc
index bd7f9fd..ab88a46 100644
--- a/y2024/control_loops/superstructure/superstructure_replay.cc
+++ b/y2024/control_loops/superstructure/superstructure_replay.cc
@@ -48,8 +48,7 @@
 
   roborio->OnStartup([roborio]() {
     roborio->AlwaysStart<y2024::control_loops::superstructure::Superstructure>(
-        "superstructure", std::make_shared<y2024::constants::Values>(
-                              y2024::constants::MakeValues()));
+        "superstructure");
   });
 
   std::unique_ptr<aos::EventLoop> print_loop = roborio->MakeEventLoop("print");
diff --git a/y2024/control_loops/superstructure/superstructure_status.fbs b/y2024/control_loops/superstructure/superstructure_status.fbs
index cd727ac..8d6b14f 100644
--- a/y2024/control_loops/superstructure/superstructure_status.fbs
+++ b/y2024/control_loops/superstructure/superstructure_status.fbs
@@ -3,29 +3,115 @@
 
 namespace y2024.control_loops.superstructure;
 
+enum SuperstructureState : ubyte {
+  // Before a note has been intaked, the extend should be retracted.
+  IDLE = 0,
+  // Intaking a note and transferring it to the extned through the
+  // intake, transfer, and extend rollers.
+  INTAKING = 1,
+  // The note is in the extend and the extend is not moving.
+  LOADED = 2,
+  // The note is in the extend and the extend is moving towards a goal,
+  // either the catapult, amp, or trap.
+  MOVING = 3,
+  // For Catapult Path, the note is being transferred between the extend and the catapult.
+  LOADING_CATAPULT = 4,
+  // The note is either:
+  // 1. Loaded in the catapult and ready to fire
+  // 2. In the extend and the extend is at the amp or trap scoring position.
+  READY = 5,
+  // Fire goal recieved and the note is being fired from the catapult or being scored in the amp or trap.
+  FIRING = 6,
+}
+
 // Contains if intake is intaking
-enum IntakeRollerState : ubyte {
+enum IntakeRollerStatus : ubyte {
   NONE = 0,
   SPITTING = 1,
   INTAKING = 2,
 }
 
+enum CatapultState: ubyte {
+    // Means we are waiting for a game piece
+    READY = 0,
+    // Means we have a game piece
+    LOADED = 1,
+    // Means we are firing a game piece
+    FIRING = 2,
+    // We are retracting the bucket to do it again.
+    RETRACTING = 3,
+}
+
+table AimerStatus {
+  // The current goal angle for the turret auto-tracking, in radians.
+  turret_position:double (id: 0);
+  // The current goal velocity for the turret, in radians / sec.
+  turret_velocity:double (id: 1);
+  // The current distance to the target, in meters.
+  target_distance:double (id: 2);
+  // The current "shot distance." When shooting on the fly, this may be
+  // different from the static distance to the target.
+  shot_distance:double (id: 3);
+}
+
+// Enum representing where the superstructure
+// is currently trying to send the note.
+enum NoteStatus : ubyte {
+  NONE = 0,
+  CATAPULT = 1,
+  AMP = 2,
+  TRAP = 3,
+}
+
 table ShooterStatus {
   // Estimated angle and angular velocitiy of the turret.
-  turret_state:frc971.control_loops.PotAndAbsoluteEncoderProfiledJointStatus (id: 0);
+  turret:frc971.control_loops.PotAndAbsoluteEncoderProfiledJointStatus (id: 0);
 
   // Estimated angle and angular velocitiy of the catapult.
-  catapult_state:frc971.control_loops.PotAndAbsoluteEncoderProfiledJointStatus (id: 1);
+  catapult:frc971.control_loops.PotAndAbsoluteEncoderProfiledJointStatus (id: 1);
 
   // Estimated angle and angular velocitiy of the altitude.
-  altitude_state:frc971.control_loops.PotAndAbsoluteEncoderProfiledJointStatus (id: 2);
+  altitude:frc971.control_loops.PotAndAbsoluteEncoderProfiledJointStatus (id: 2);
+
+  catapult_state: CatapultState (id: 3);
+
+  // Status of the aimer
+  aimer:AimerStatus (id: 4);
 }
 
 // Contains status of transfer rollers
-enum TransferRollerState : ubyte {
+enum TransferRollerStatus : ubyte {
     NONE = 0,
     TRANSFERING_IN = 1,
     TRANSFERING_OUT = 2,
+    EXTEND_MOVING = 3,
+}
+
+// Contains status of extend rollers
+enum ExtendRollerStatus: ubyte {
+  // Means the rollers are not moving.
+  IDLE = 0,
+  // Means we're transfer from the transfer rollers into the extend rollers.
+  TRANSFERING_TO_EXTEND = 1,
+  // Means we are transfering from the extend to the catapult.
+  TRANSFERING_TO_CATAPULT = 2,
+  // Means we're trying to score in the amp/trap
+  SCORING_IN_AMP = 3,
+  SCORING_IN_TRAP = 4,
+}
+
+// Contains the status of the extend subsystem
+enum ExtendStatus : ubyte  {
+  // Means we are near 0 and ready to transfer a game piece.
+  RETRACTED = 0,
+  // Means we are moving to some goal.
+  MOVING = 1,
+  // Means we are currently at the catapult.
+  CATAPULT = 2,
+  // Means we are at the amp position.
+  AMP = 3,
+  // Means we are at the trap positon.
+  TRAP = 4,
 }
 
 table Status {
@@ -35,17 +121,59 @@
   // If true, we have aborted. This is the or of all subsystem estops.
   estopped:bool (id: 1);
 
-  // State of the rollers
-  intake_roller_state:IntakeRollerState (id: 2);
+  state : SuperstructureState (id: 2);
+
+  // Status of the rollers
+  intake_roller:IntakeRollerStatus (id: 3);
 
   // Estimated angle and angular velocitiy of the intake.
-  intake_pivot_state:frc971.control_loops.PotAndAbsoluteEncoderProfiledJointStatus (id: 3);
+  intake_pivot:frc971.control_loops.AbsoluteEncoderProfiledJointStatus (id: 4);
 
-  // State of transfer rollers
-  transfer_roller_state:TransferRollerState (id: 4);
+  // Status of transfer rollers
+  transfer_roller:TransferRollerStatus (id: 5);
 
   // Estimated angle and angular velocitiy of the climber.
-  climber_state:frc971.control_loops.PotAndAbsoluteEncoderProfiledJointStatus (id: 5);
+  climber:frc971.control_loops.PotAndAbsoluteEncoderProfiledJointStatus (id: 6);
+
+  // Status of the subsytems involved in the shooter
+  shooter:ShooterStatus (id: 7);
+
+  // Estimated angle and angular velocitiy of the extend.
+  extend:frc971.control_loops.PotAndAbsoluteEncoderProfiledJointStatus (id: 8);
+
+  // State of the extender rollers
+  extend_roller:ExtendRollerStatus (id: 9);
+
+  // The status of if the turret and intake is colliding
+  collided: bool (id: 10);
+
+  extend_status:ExtendStatus (id: 11);
+
+  // Indicates that the extend is in position to allow a game piece to
+  // be transfered into it.
+  extend_ready_for_transfer:bool (id: 12);
+
+  // Indicates that the turret is in position to avoid the extend.
+  turret_ready_for_extend_move:bool (id: 13, deprecated);
+
+  uncompleted_note_goal:NoteStatus = NONE (id: 14);
+
+  // Indicates if the extend is close enough to the retracted position to be
+  // considered ready to accept note from the transfer rollers.
+  extend_at_retracted:bool (id: 15);
+
+  // Indicates if the turret is at the position to accept the note from extend
+  turret_ready_for_load:bool (id: 16);
+
+  // Indicates if the altitude is at the position to accept the note from
+  // extend
+  altitude_ready_for_load:bool (id: 17);
+
+  // Indicates if the extend is at the position to load the catapult
+  extend_ready_for_catapult_transfer:bool (id: 18);
+
+  extend_beambreak:bool (id: 19);
+  catapult_beambreak:bool (id: 20);
 }
 
 root_type Status;
diff --git a/y2024/control_loops/superstructure/turret/BUILD b/y2024/control_loops/superstructure/turret/BUILD
new file mode 100644
index 0000000..c67eb89
--- /dev/null
+++ b/y2024/control_loops/superstructure/turret/BUILD
@@ -0,0 +1,42 @@
+package(default_visibility = ["//y2024:__subpackages__"])
+
+genrule(
+    name = "genrule_turret",
+    outs = [
+        "turret_plant.h",
+        "turret_plant.cc",
+        "turret_plant.json",
+        "integral_turret_plant.h",
+        "integral_turret_plant.cc",
+        "integral_turret_plant.json",
+    ],
+    cmd = "$(location //y2024/control_loops/python:turret) $(OUTS)",
+    target_compatible_with = ["@platforms//os:linux"],
+    tools = [
+        "//y2024/control_loops/python:turret",
+    ],
+)
+
+cc_library(
+    name = "turret_plants",
+    srcs = [
+        "integral_turret_plant.cc",
+        "turret_plant.cc",
+    ],
+    hdrs = [
+        "integral_turret_plant.h",
+        "turret_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",
+    ],
+)
+
+filegroup(
+    name = "turret_json",
+    srcs = ["integral_turret_plant.json"],
+    visibility = ["//visibility:public"],
+)
diff --git a/y2024/joystick_reader.cc b/y2024/joystick_reader.cc
index baac66f..caa6805 100644
--- a/y2024/joystick_reader.cc
+++ b/y2024/joystick_reader.cc
@@ -13,6 +13,7 @@
 #include "frc971/constants/constants_sender_lib.h"
 #include "frc971/control_loops/drivetrain/localizer_generated.h"
 #include "frc971/control_loops/profiled_subsystem_generated.h"
+#include "frc971/control_loops/static_zeroing_single_dof_profiled_subsystem.h"
 #include "frc971/input/action_joystick_input.h"
 #include "frc971/input/driver_station_data.h"
 #include "frc971/input/drivetrain_input.h"
@@ -21,11 +22,10 @@
 #include "frc971/zeroing/wrap.h"
 #include "y2024/constants/constants_generated.h"
 #include "y2024/control_loops/drivetrain/drivetrain_base.h"
-#include "y2024/control_loops/superstructure/superstructure_goal_generated.h"
-#include "y2024/control_loops/superstructure/superstructure_status_generated.h"
+#include "y2024/control_loops/superstructure/superstructure_goal_static.h"
+#include "y2024/control_loops/superstructure/superstructure_status_static.h"
+
 using frc971::CreateProfileParameters;
-using frc971::control_loops::CreateStaticZeroingSingleDOFProfiledSubsystemGoal;
-using frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemGoal;
 using frc971::input::driver_station::ButtonLocation;
 using frc971::input::driver_station::ControlBit;
 using frc971::input::driver_station::JoystickAxis;
@@ -34,38 +34,120 @@
 
 namespace y2024::input::joysticks {
 
+namespace superstructure = y2024::control_loops::superstructure;
+
+// TODO(Xander): add button location from physical wiring
+// Note: Due to use_redundant_joysticks, the AOS_LOG statements
+// for the internal joystick code will give offset joystick numbering.
+const ButtonLocation kIntake(2, 8);
+const ButtonLocation kSpit(0, 0);
+const ButtonLocation kCatapultLoad(1, 7);
+const ButtonLocation kAmp(2, 7);
+const ButtonLocation kFire(2, 6);
+const ButtonLocation kTrap(2, 5);
+const ButtonLocation kAutoAim(0, 0);
+const ButtonLocation kAimSpeaker(1, 6);
+const ButtonLocation kAimPodium(0, 0);
+const ButtonLocation kShoot(0, 0);
+const ButtonLocation kRaiseClimber(3, 2);
+const ButtonLocation kRetractClimber(2, 4);
+const ButtonLocation kExtraButtonOne(0, 0);
+const ButtonLocation kExtraButtonTwo(0, 0);
+const ButtonLocation kExtraButtonThree(0, 0);
+const ButtonLocation kExtraButtonFour(0, 0);
+
 class Reader : public ::frc971::input::ActionJoystickInput {
  public:
-  Reader(::aos::EventLoop *event_loop)
+  Reader(::aos::EventLoop *event_loop, const y2024::Constants *robot_constants)
       : ::frc971::input::ActionJoystickInput(
             event_loop,
             ::y2024::control_loops::drivetrain::GetDrivetrainConfig(event_loop),
             ::frc971::input::DrivetrainInputReader::InputType::kPistol,
             {.use_redundant_joysticks = true}),
         superstructure_goal_sender_(
-            event_loop->MakeSender<control_loops::superstructure::Goal>(
+            event_loop->MakeSender<control_loops::superstructure::GoalStatic>(
                 "/superstructure")),
         superstructure_status_fetcher_(
             event_loop->MakeFetcher<control_loops::superstructure::Status>(
-                "/superstructure")) {}
+                "/superstructure")),
+        robot_constants_(CHECK_NOTNULL(robot_constants)) {}
 
   void AutoEnded() override { AOS_LOG(INFO, "Auto ended.\n"); }
 
   void HandleTeleop(
       const ::frc971::input::driver_station::Data &data) override {
-    (void)data;
     superstructure_status_fetcher_.Fetch();
     if (!superstructure_status_fetcher_.get()) {
       AOS_LOG(ERROR, "Got no superstructure status message.\n");
       return;
     }
+
+    aos::Sender<superstructure::GoalStatic>::StaticBuilder
+        superstructure_goal_builder =
+            superstructure_goal_sender_.MakeStaticBuilder();
+
+    if (data.IsPressed(kIntake)) {
+      // Intake is pressed
+      superstructure_goal_builder->set_intake_goal(
+          superstructure::IntakeGoal::INTAKE);
+    } else {
+      superstructure_goal_builder->set_intake_goal(
+          superstructure::IntakeGoal::NONE);
+    }
+    if (data.IsPressed(kAmp)) {
+      superstructure_goal_builder->set_note_goal(superstructure::NoteGoal::AMP);
+    } else if (data.IsPressed(kTrap)) {
+      superstructure_goal_builder->set_note_goal(
+          superstructure::NoteGoal::TRAP);
+    } else if (data.IsPressed(kCatapultLoad)) {
+      superstructure_goal_builder->set_note_goal(
+          superstructure::NoteGoal::CATAPULT);
+    } else {
+      superstructure_goal_builder->set_note_goal(
+          superstructure::NoteGoal::NONE);
+    }
+    auto shooter_goal = superstructure_goal_builder->add_shooter_goal();
+    shooter_goal->set_auto_aim(false);
+
+    // Updating aiming for shooter goal, only one type of aim should be possible
+    // at a time, auto-aiming is preferred over the setpoints.
+    if (data.IsPressed(kAimSpeaker)) {
+      auto catapult_goal = shooter_goal->add_catapult_goal();
+      catapult_goal->set_shot_velocity(robot_constants_->common()
+                                           ->shooter_speaker_set_point()
+                                           ->shot_velocity());
+      PopulateStaticZeroingSingleDOFProfiledSubsystemGoal(
+          shooter_goal->add_altitude_position(),
+          robot_constants_->common()
+              ->shooter_speaker_set_point()
+              ->altitude_position());
+      PopulateStaticZeroingSingleDOFProfiledSubsystemGoal(
+          shooter_goal->add_turret_position(), robot_constants_->common()
+                                                   ->shooter_speaker_set_point()
+                                                   ->turret_position());
+    }
+    superstructure_goal_builder->set_fire(data.IsPressed(kFire));
+
+    if (data.IsPressed(kRaiseClimber)) {
+      superstructure_goal_builder->set_climber_goal(
+          superstructure::ClimberGoal::FULL_EXTEND);
+    } else if (data.IsPressed(kRetractClimber)) {
+      superstructure_goal_builder->set_climber_goal(
+          superstructure::ClimberGoal::RETRACT);
+    } else {
+      superstructure_goal_builder->set_climber_goal(
+          superstructure::ClimberGoal::STOWED);
+    }
+
+    superstructure_goal_builder.CheckOk(superstructure_goal_builder.Send());
   }
 
  private:
-  ::aos::Sender<control_loops::superstructure::Goal>
+  ::aos::Sender<control_loops::superstructure::GoalStatic>
       superstructure_goal_sender_;
   ::aos::Fetcher<control_loops::superstructure::Status>
       superstructure_status_fetcher_;
+  const y2024::Constants *robot_constants_;
 };
 
 }  // namespace y2024::input::joysticks
@@ -77,8 +159,13 @@
       aos::configuration::ReadConfig("aos_config.json");
   frc971::constants::WaitForConstants<y2024::Constants>(&config.message());
 
+  ::aos::ShmEventLoop constant_fetcher_event_loop(&config.message());
+  frc971::constants::ConstantsFetcher<y2024::Constants> constants_fetcher(
+      &constant_fetcher_event_loop);
+  const y2024::Constants *robot_constants = &constants_fetcher.constants();
+
   ::aos::ShmEventLoop event_loop(&config.message());
-  ::y2024::input::joysticks::Reader reader(&event_loop);
+  ::y2024::input::joysticks::Reader reader(&event_loop, robot_constants);
 
   event_loop.Run();
 
diff --git a/y2024/localizer/BUILD b/y2024/localizer/BUILD
new file mode 100644
index 0000000..caec284
--- /dev/null
+++ b/y2024/localizer/BUILD
@@ -0,0 +1,168 @@
+load("//aos/flatbuffers:generate.bzl", "static_flatbuffer")
+load("//tools/build_rules:js.bzl", "ts_project")
+load("@com_github_google_flatbuffers//:typescript.bzl", "flatbuffer_ts_library")
+
+ts_project(
+    name = "localizer_plotter",
+    srcs = ["localizer_plotter.ts"],
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//aos/network/www:aos_plotter",
+        "//aos/network/www:colors",
+        "//aos/network/www:proxy",
+        "//frc971/wpilib:imu_plot_utils",
+    ],
+)
+
+static_flatbuffer(
+    name = "status_fbs",
+    srcs = [
+        "status.fbs",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//frc971/control_loops/drivetrain:drivetrain_status_fbs",
+        "//frc971/imu_reader:imu_failures_fbs",
+    ],
+)
+
+flatbuffer_ts_library(
+    name = "status_ts_fbs",
+    srcs = ["status.fbs"],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//frc971/control_loops/drivetrain:drivetrain_status_ts_fbs",
+        "//frc971/imu_reader:imu_failures_ts_fbs",
+    ],
+)
+
+static_flatbuffer(
+    name = "visualization_fbs",
+    srcs = [
+        "visualization.fbs",
+    ],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":status_fbs",
+    ],
+)
+
+flatbuffer_ts_library(
+    name = "visualization_ts_fbs",
+    srcs = ["visualization.fbs"],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":status_ts_fbs",
+    ],
+)
+
+cc_library(
+    name = "localizer",
+    srcs = ["localizer.cc"],
+    hdrs = ["localizer.h"],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":status_fbs",
+        ":visualization_fbs",
+        "//aos/containers:sized_array",
+        "//aos/events:event_loop",
+        "//aos/network:message_bridge_client_fbs",
+        "//frc971/constants:constants_sender_lib",
+        "//frc971/control_loops:pose",
+        "//frc971/control_loops/drivetrain:hybrid_ekf",
+        "//frc971/control_loops/drivetrain:improved_down_estimator",
+        "//frc971/control_loops/drivetrain:localizer_fbs",
+        "//frc971/control_loops/drivetrain/localization:localizer_output_fbs",
+        "//frc971/control_loops/drivetrain/localization:utils",
+        "//frc971/imu_reader:imu_watcher",
+        "//frc971/vision:target_map_fbs",
+        "//frc971/vision:target_map_utils",
+        "//y2024:constants",
+        "//y2024/constants:constants_fbs",
+    ],
+)
+
+cc_test(
+    name = "localizer_test",
+    srcs = ["localizer_test.cc"],
+    data = ["//y2024:aos_config"],
+    deps = [
+        ":localizer",
+        ":status_fbs",
+        "//aos/events:simulated_event_loop",
+        "//aos/events/logging:log_writer",
+        "//aos/testing:googletest",
+        "//frc971/control_loops/drivetrain:drivetrain_test_lib",
+        "//frc971/control_loops/drivetrain:localizer_fbs",
+        "//frc971/control_loops/drivetrain/localization:utils",
+        "//y2024/constants:simulated_constants_sender",
+        "//y2024/control_loops/drivetrain:drivetrain_base",
+    ],
+)
+
+cc_binary(
+    name = "localizer_main",
+    srcs = ["localizer_main.cc"],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":localizer",
+        "//aos:init",
+        "//aos/events:shm_event_loop",
+        "//frc971/constants:constants_sender_lib",
+        "//y2024/control_loops/drivetrain:drivetrain_base",
+    ],
+)
+
+ts_project(
+    name = "corrections_plotter",
+    srcs = ["corrections_plotter.ts"],
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":visualization_ts_fbs",
+        "//aos/network/www:aos_plotter",
+        "//aos/network/www:colors",
+        "//aos/network/www:proxy",
+    ],
+)
+
+cc_binary(
+    name = "localizer_replay",
+    srcs = ["localizer_replay.cc"],
+    data = [
+        "//y2024:aos_config",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = [
+        ":localizer",
+        "//aos:configuration",
+        "//aos:init",
+        "//aos:json_to_flatbuffer",
+        "//aos/events:simulated_event_loop",
+        "//aos/events/logging:log_reader",
+        "//aos/events/logging:log_writer",
+        "//aos/util:simulation_logger",
+        "//y2024/control_loops/drivetrain:drivetrain_base",
+    ],
+)
+
+cc_binary(
+    name = "localizer_logger",
+    srcs = [
+        "localizer_logger.cc",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//aos:configuration",
+        "//aos:init",
+        "//aos/events:shm_event_loop",
+        "//aos/events/logging:log_writer",
+        "//aos/events/logging:snappy_encoder",
+        "//aos/logging:log_namer",
+        "@com_github_gflags_gflags//:gflags",
+        "@com_github_google_glog//:glog",
+    ],
+)
diff --git a/y2024/localizer/corrections_plotter.ts b/y2024/localizer/corrections_plotter.ts
new file mode 100644
index 0000000..c883759
--- /dev/null
+++ b/y2024/localizer/corrections_plotter.ts
@@ -0,0 +1,177 @@
+import {ByteBuffer} from 'flatbuffers';
+import {AosPlotter} from '../../aos/network/www/aos_plotter';
+import {MessageHandler, TimestampedMessage} from '../../aos/network/www/aos_plotter';
+import {BLUE, BROWN, CYAN, GREEN, PINK, RED, WHITE} from '../../aos/network/www/colors';
+import {Connection} from '../../aos/network/www/proxy';
+import {Table} from '../../aos/network/www/reflection';
+import {Schema} from 'flatbuffers_reflection/reflection_generated';
+import {Visualization, TargetEstimateDebug} from './visualization_generated';
+
+
+const TIME = AosPlotter.TIME;
+// magenta, yellow, cyan, black
+const PI_COLORS = [[255, 0, 255], [255, 255, 0], [0, 255, 255], [0, 0, 0]];
+
+class VisionMessageHandler extends MessageHandler {
+  constructor(private readonly schema: Schema) {
+    super(schema);
+  }
+
+  private readScalar(table: Table, fieldName: string): number|BigInt|null {
+    return this.parser.readScalar(table, fieldName);
+  }
+
+  addMessage(data: Uint8Array, time: number): void {
+    const message = Visualization.getRootAsVisualization(new ByteBuffer(data));
+    for (let ii = 0; ii < message.targetsLength(); ++ii) {
+      const target = message.targets(ii);
+      const time = Number(target.imageMonotonicTimestampNs()) * 1e-9;
+      if (time == 0) {
+        console.log('Dropping message without populated time?');
+        continue;
+      }
+      const table = Table.getNamedTable(
+          target.bb, this.schema, 'y2024.localizer.TargetEstimateDebug', target.bb_pos);
+      this.messages.push(new TimestampedMessage(table, time));
+    }
+  }
+}
+
+export function plotVision(conn: Connection, element: Element): void {
+  const aosPlotter = new AosPlotter(conn);
+
+  const targets = [];
+  const targetLabels = [];
+  for (const orin of ['orin1', 'imu']) {
+    for (const camera of ['camera0', 'camera1']) {
+      targetLabels.push(orin + ' ' + camera);
+      targets.push(aosPlotter.addRawMessageSource(
+          '/' + orin + '/' + camera, 'y2024.localizer.Visualization',
+          new VisionMessageHandler(
+              conn.getSchema('y2024.localizer.Visualization'))));
+    }
+  }
+  const localizerStatus = aosPlotter.addMessageSource(
+      '/localizer', 'y2024.localizer.Status');
+  const localizerOutput = aosPlotter.addMessageSource(
+      '/localizer', 'frc971.controls.LocalizerOutput');
+
+  const statsPlot = aosPlotter.addPlot(element);
+  statsPlot.plot.getAxisLabels().setTitle('Statistics');
+  statsPlot.plot.getAxisLabels().setXLabel(TIME);
+  statsPlot.plot.getAxisLabels().setYLabel('[bool, enum]');
+
+  statsPlot
+      .addMessageLine(localizerStatus, ['statistics[]', 'total_accepted'])
+      .setDrawLine(false)
+      .setColor(BLUE);
+  statsPlot
+      .addMessageLine(localizerStatus, ['statistics[]', 'total_candidates'])
+      .setDrawLine(false)
+      .setColor(RED);
+
+  const rejectionPlot = aosPlotter.addPlot(element);
+  rejectionPlot.plot.getAxisLabels().setTitle('Rejection Reasons');
+  rejectionPlot.plot.getAxisLabels().setXLabel(TIME);
+  rejectionPlot.plot.getAxisLabels().setYLabel('[bool, enum]');
+
+  for (let ii = 0; ii < targets.length; ++ii) {
+    rejectionPlot.addMessageLine(targets[ii], ['rejection_reason'])
+        .setDrawLine(false)
+        .setColor(PI_COLORS[ii])
+        .setLabel(targetLabels[ii]);
+  }
+
+  const xPlot = aosPlotter.addPlot(element);
+  xPlot.plot.getAxisLabels().setTitle('X Position');
+  xPlot.plot.getAxisLabels().setXLabel(TIME);
+  xPlot.plot.getAxisLabels().setYLabel('[m]');
+
+  for (let ii = 0; ii < targets.length; ++ii) {
+    xPlot.addMessageLine(targets[ii], ['implied_robot_x'])
+        .setDrawLine(false)
+        .setColor(PI_COLORS[ii])
+        .setLabel(targetLabels[ii]);
+  }
+  xPlot.addMessageLine(localizerOutput, ['x'])
+      .setDrawLine(false)
+      .setColor(BLUE);
+
+  const correctionXPlot = aosPlotter.addPlot(element);
+  correctionXPlot.plot.getAxisLabels().setTitle('X Corrections');
+  correctionXPlot.plot.getAxisLabels().setXLabel(TIME);
+  correctionXPlot.plot.getAxisLabels().setYLabel('[m]');
+
+  for (let ii = 0; ii < targets.length; ++ii) {
+    correctionXPlot.addMessageLine(targets[ii], ['correction_x'])
+        .setDrawLine(false)
+        .setColor(PI_COLORS[ii])
+        .setLabel(targetLabels[ii]);
+  }
+
+  const yPlot = aosPlotter.addPlot(element);
+  yPlot.plot.getAxisLabels().setTitle('Y Position');
+  yPlot.plot.getAxisLabels().setXLabel(TIME);
+  yPlot.plot.getAxisLabels().setYLabel('[m]');
+
+  for (let ii = 0; ii < targets.length; ++ii) {
+    yPlot.addMessageLine(targets[ii], ['implied_robot_y'])
+        .setDrawLine(false)
+        .setColor(PI_COLORS[ii])
+        .setLabel(targetLabels[ii]);
+  }
+  yPlot.addMessageLine(localizerOutput, ['y'])
+      .setDrawLine(false)
+      .setColor(BLUE);
+
+  const correctionYPlot = aosPlotter.addPlot(element);
+  correctionYPlot.plot.getAxisLabels().setTitle('Y Corrections');
+  correctionYPlot.plot.getAxisLabels().setXLabel(TIME);
+  correctionYPlot.plot.getAxisLabels().setYLabel('[m]');
+
+  for (let ii = 0; ii < targets.length; ++ii) {
+    correctionYPlot.addMessageLine(targets[ii], ['correction_y'])
+        .setDrawLine(false)
+        .setColor(PI_COLORS[ii])
+        .setLabel(targetLabels[ii]);
+  }
+
+  const thetaPlot = aosPlotter.addPlot(element);
+  thetaPlot.plot.getAxisLabels().setTitle('Yaw');
+  thetaPlot.plot.getAxisLabels().setXLabel(TIME);
+  thetaPlot.plot.getAxisLabels().setYLabel('[m]');
+
+  for (let ii = 0; ii < targets.length; ++ii) {
+    thetaPlot.addMessageLine(targets[ii], ['implied_robot_theta'])
+        .setDrawLine(false)
+        .setColor(PI_COLORS[ii])
+        .setLabel(targetLabels[ii]);
+  }
+  thetaPlot.addMessageLine(localizerOutput, ['theta'])
+      .setDrawLine(false)
+      .setColor(BLUE);
+
+  const aprilTagPlot = aosPlotter.addPlot(element);
+  aprilTagPlot.plot.getAxisLabels().setTitle('April Tag IDs');
+  aprilTagPlot.plot.getAxisLabels().setXLabel(TIME);
+  aprilTagPlot.plot.getAxisLabels().setYLabel('[id]');
+
+  for (let ii = 0; ii < targets.length; ++ii) {
+    aprilTagPlot.addMessageLine(targets[ii], ['april_tag'])
+        .setDrawLine(false)
+        .setColor(PI_COLORS[ii])
+        .setLabel(targetLabels[ii]);
+  }
+
+  const imageAgePlot = aosPlotter.addPlot(element);
+  imageAgePlot.plot.getAxisLabels().setTitle('Image Age');
+  imageAgePlot.plot.getAxisLabels().setXLabel(TIME);
+  imageAgePlot.plot.getAxisLabels().setYLabel('[sec]');
+
+  for (let ii = 0; ii < targets.length; ++ii) {
+    imageAgePlot.addMessageLine(targets[ii], ['image_age_sec'])
+        .setDrawLine(false)
+        .setColor(PI_COLORS[ii])
+        .setLabel(targetLabels[ii]);
+  }
+}
diff --git a/y2024/localizer/localizer.cc b/y2024/localizer/localizer.cc
new file mode 100644
index 0000000..a177188
--- /dev/null
+++ b/y2024/localizer/localizer.cc
@@ -0,0 +1,601 @@
+#include "y2024/localizer/localizer.h"
+
+#include "gflags/gflags.h"
+
+#include "aos/containers/sized_array.h"
+#include "frc971/control_loops/drivetrain/localizer_generated.h"
+#include "frc971/control_loops/pose.h"
+#include "frc971/vision/target_map_utils.h"
+#include "y2024/constants.h"
+
+DEFINE_double(max_pose_error, 1e-6,
+              "Throw out target poses with a higher pose error than this");
+DEFINE_double(
+    max_pose_error_ratio, 0.4,
+    "Throw out target poses with a higher pose error ratio than this");
+DEFINE_double(distortion_noise_scalar, 1.0,
+              "Scale the target pose distortion factor by this when computing "
+              "the noise.");
+DEFINE_double(
+    max_implied_yaw_error, 3.0,
+    "Reject target poses that imply a robot yaw of more than this many degrees "
+    "off from our estimate.");
+DEFINE_double(
+    max_implied_teleop_yaw_error, 30.0,
+    "Reject target poses that imply a robot yaw of more than this many degrees "
+    "off from our estimate.");
+DEFINE_double(max_distance_to_target, 5.0,
+              "Reject target poses that have a 3d distance of more than this "
+              "many meters.");
+DEFINE_double(max_auto_image_robot_speed, 2.0,
+              "Reject target poses when the robot is travelling faster than "
+              "this speed in auto.");
+
+namespace y2024::localizer {
+namespace {
+constexpr std::array<std::string_view, Localizer::kNumCameras>
+    kDetectionChannels{"/orin1/camera0", "/orin1/camera1", "/imu/camera0",
+                       "/imu/camera1"};
+
+size_t CameraIndexForName(std::string_view name) {
+  for (size_t index = 0; index < kDetectionChannels.size(); ++index) {
+    if (name == kDetectionChannels.at(index)) {
+      return index;
+    }
+  }
+  LOG(FATAL) << "No camera channel named " << name;
+}
+
+std::map<uint64_t, Localizer::Transform> GetTargetLocations(
+    const Constants &constants) {
+  CHECK(constants.has_common());
+  CHECK(constants.common()->has_target_map());
+  CHECK(constants.common()->target_map()->has_target_poses());
+  std::map<uint64_t, Localizer::Transform> transforms;
+  for (const frc971::vision::TargetPoseFbs *target :
+       *constants.common()->target_map()->target_poses()) {
+    CHECK(target->has_id());
+    CHECK(target->has_position());
+    CHECK(target->has_orientation());
+    CHECK_EQ(0u, transforms.count(target->id()));
+    transforms[target->id()] = PoseToTransform(target);
+  }
+  return transforms;
+}
+}  // namespace
+
+std::array<Localizer::CameraState, Localizer::kNumCameras>
+Localizer::MakeCameras(const Constants &constants, aos::EventLoop *event_loop) {
+  CHECK(constants.has_cameras());
+  std::array<Localizer::CameraState, Localizer::kNumCameras> cameras;
+  for (const CameraConfiguration *camera : *constants.cameras()) {
+    CHECK(camera->has_calibration());
+    const frc971::vision::calibration::CameraCalibration *calibration =
+        camera->calibration();
+    CHECK(!calibration->has_turret_extrinsics())
+        << "The 2024 robot does not have cameras on a turret.";
+    CHECK(calibration->has_node_name());
+    const std::string channel_name =
+        absl::StrFormat("/%s/camera%d", calibration->node_name()->string_view(),
+                        calibration->camera_number());
+    const size_t index = CameraIndexForName(channel_name);
+    // We default-construct the extrinsics matrix to all-zeros; use that to
+    // sanity-check whether we have populated the matrix yet or not.
+    CHECK(cameras.at(index).extrinsics.norm() == 0)
+        << "Got multiple calibrations for "
+        << calibration->node_name()->string_view();
+    CHECK(calibration->has_fixed_extrinsics());
+    cameras.at(index).extrinsics =
+        frc971::control_loops::drivetrain::FlatbufferToTransformationMatrix(
+            *calibration->fixed_extrinsics());
+    cameras.at(index).debug_sender =
+        event_loop->MakeSender<VisualizationStatic>(channel_name);
+  }
+  for (const CameraState &camera : cameras) {
+    CHECK(camera.extrinsics.norm() != 0) << "Missing a camera calibration.";
+  }
+  return cameras;
+}
+
+Localizer::Localizer(aos::EventLoop *event_loop)
+    : event_loop_(event_loop),
+      constants_fetcher_(event_loop),
+      dt_config_(
+          frc971::control_loops::drivetrain::DrivetrainConfig<double>::
+              FromFlatbuffer(*CHECK_NOTNULL(
+                  constants_fetcher_.constants().common()->drivetrain()))),
+      cameras_(MakeCameras(constants_fetcher_.constants(), event_loop)),
+      target_poses_(GetTargetLocations(constants_fetcher_.constants())),
+      down_estimator_(dt_config_),
+      ekf_(dt_config_),
+      observations_(&ekf_),
+      imu_watcher_(event_loop, dt_config_,
+                   y2024::constants::Values::DrivetrainEncoderToMeters(1),
+                   std::bind(&Localizer::HandleImu, this, std::placeholders::_1,
+                             std::placeholders::_2, std::placeholders::_3,
+                             std::placeholders::_4, std::placeholders::_5),
+                   frc971::controls::ImuWatcher::TimestampSource::kPi),
+      utils_(event_loop),
+      status_sender_(event_loop->MakeSender<Status>("/localizer")),
+      output_sender_(event_loop->MakeSender<frc971::controls::LocalizerOutput>(
+          "/localizer")),
+      server_statistics_fetcher_(
+          event_loop_->MakeFetcher<aos::message_bridge::ServerStatistics>(
+              "/aos")),
+      client_statistics_fetcher_(
+          event_loop_->MakeFetcher<aos::message_bridge::ClientStatistics>(
+              "/aos")) {
+  if (dt_config_.is_simulated) {
+    down_estimator_.assume_perfect_gravity();
+  }
+
+  for (size_t camera_index = 0; camera_index < kNumCameras; ++camera_index) {
+    const std::string_view channel_name = kDetectionChannels.at(camera_index);
+    const aos::Channel *const channel = CHECK_NOTNULL(
+        event_loop->GetChannel<frc971::vision::TargetMap>(channel_name));
+    event_loop->MakeWatcher(
+        channel_name, [this, channel,
+                       camera_index](const frc971::vision::TargetMap &targets) {
+          CHECK(targets.has_target_poses());
+          CHECK(targets.has_monotonic_timestamp_ns());
+          const std::optional<aos::monotonic_clock::duration> clock_offset =
+              utils_.ClockOffset(channel->source_node()->string_view());
+          if (!clock_offset.has_value()) {
+            VLOG(1) << "Rejecting image due to disconnected message bridge at "
+                    << event_loop_->monotonic_now();
+            cameras_.at(camera_index)
+                .rejection_counter.IncrementError(
+                    RejectionReason::MESSAGE_BRIDGE_DISCONNECTED);
+            return;
+          }
+          const aos::monotonic_clock::time_point orin_capture_time(
+              std::chrono::nanoseconds(targets.monotonic_timestamp_ns()) -
+              clock_offset.value());
+          if (orin_capture_time > event_loop_->context().monotonic_event_time) {
+            VLOG(1) << "Rejecting image due to being from future at "
+                    << event_loop_->monotonic_now() << " with timestamp of "
+                    << orin_capture_time << " and event time pf "
+                    << event_loop_->context().monotonic_event_time;
+            cameras_.at(camera_index)
+                .rejection_counter.IncrementError(
+                    RejectionReason::IMAGE_FROM_FUTURE);
+            return;
+          }
+          auto debug_builder =
+              cameras_.at(camera_index).debug_sender.MakeStaticBuilder();
+          auto target_debug_list = debug_builder->add_targets();
+          // The static_length should already be 20.
+          CHECK(target_debug_list->reserve(20));
+          for (const frc971::vision::TargetPoseFbs *target :
+               *targets.target_poses()) {
+            VLOG(1) << "Handling target from " << camera_index;
+            HandleTarget(camera_index, orin_capture_time, *target,
+                         target_debug_list->emplace_back());
+          }
+          StatisticsForCamera(cameras_.at(camera_index),
+                              debug_builder->add_statistics());
+          debug_builder.CheckOk(debug_builder.Send());
+          SendStatus();
+        });
+  }
+
+  event_loop_->AddPhasedLoop([this](int) { SendOutput(); },
+                             std::chrono::milliseconds(20));
+
+  event_loop_->MakeWatcher(
+      "/drivetrain",
+      [this](
+          const frc971::control_loops::drivetrain::LocalizerControl &control) {
+        // This is triggered whenever we need to force the X/Y/(maybe theta)
+        // position of the robot to a particular point---e.g., during pre-match
+        // setup, or when commanded by a button on the driverstation.
+
+        // For some forms of reset, we choose to keep our current yaw estimate
+        // rather than overriding it from the control message.
+        const double theta = control.keep_current_theta()
+                                 ? ekf_.X_hat(StateIdx::kTheta)
+                                 : control.theta();
+        // Encoder values need to be reset based on the current values to ensure
+        // that we don't get weird corrections on the next encoder update.
+        const double left_encoder = ekf_.X_hat(StateIdx::kLeftEncoder);
+        const double right_encoder = ekf_.X_hat(StateIdx::kRightEncoder);
+        ekf_.ResetInitialState(
+            t_,
+            (HybridEkf::State() << control.x(), control.y(), theta,
+             left_encoder, 0, right_encoder, 0, 0, 0, 0, 0, 0)
+                .finished(),
+            ekf_.P());
+      });
+
+  ekf_.set_ignore_accel(true);
+  // Priority should be lower than the imu reading process, but non-zero.
+  event_loop->SetRuntimeRealtimePriority(10);
+  event_loop->OnRun([this, event_loop]() {
+    ekf_.ResetInitialState(event_loop->monotonic_now(),
+                           HybridEkf::State::Zero(), ekf_.P());
+  });
+}
+
+void Localizer::HandleImu(aos::monotonic_clock::time_point /*sample_time_pico*/,
+                          aos::monotonic_clock::time_point sample_time_orin,
+                          std::optional<Eigen::Vector2d> /*encoders*/,
+                          Eigen::Vector3d gyro, Eigen::Vector3d accel) {
+  std::optional<Eigen::Vector2d> encoders = utils_.Encoders(sample_time_orin);
+  last_encoder_readings_ = encoders;
+  // Ignore invalid readings; the HybridEkf will handle it reasonably.
+  if (!encoders.has_value()) {
+    return;
+  }
+  if (t_ == aos::monotonic_clock::min_time) {
+    t_ = sample_time_orin;
+  }
+  if (t_ + 10 * frc971::controls::ImuWatcher::kNominalDt < sample_time_orin) {
+    t_ = sample_time_orin;
+    ++clock_resets_;
+  }
+  const aos::monotonic_clock::duration dt = sample_time_orin - t_;
+  t_ = sample_time_orin;
+  // We don't actually use the down estimator currently, but it's really
+  // convenient for debugging.
+  down_estimator_.Predict(gyro, accel, dt);
+  const double yaw_rate = (dt_config_.imu_transform * gyro)(2);
+  ekf_.UpdateEncodersAndGyro(encoders.value()(0), encoders.value()(1), yaw_rate,
+                             utils_.VoltageOrZero(sample_time_orin), accel, t_);
+  SendStatus();
+}
+
+void Localizer::RejectImage(int camera_index, RejectionReason reason,
+                            TargetEstimateDebugStatic *builder) {
+  if (builder != nullptr) {
+    builder->set_accepted(false);
+    builder->set_rejection_reason(reason);
+  }
+  cameras_.at(camera_index).rejection_counter.IncrementError(reason);
+}
+
+// Only use april tags present in the target map; this method has also been used
+// (in the past) for ignoring april tags that tend to produce problematic
+// readings.
+bool Localizer::UseAprilTag(uint64_t target_id) {
+  return target_poses_.count(target_id) != 0;
+}
+
+namespace {
+// Converts a camera transformation matrix from treating the +Z axis from
+// pointing straight out the lens to having the +X pointing straight out the
+// lens, with +Z going "up" (i.e., -Y in the normal convention) and +Y going
+// leftwards (i.e., -X in the normal convention).
+Localizer::Transform ZToXCamera(const Localizer::Transform &transform) {
+  return transform *
+         Eigen::Matrix4d{
+             {0, -1, 0, 0}, {0, 0, -1, 0}, {1, 0, 0, 0}, {0, 0, 0, 1}};
+}
+}  // namespace
+
+void Localizer::HandleTarget(
+    int camera_index, const aos::monotonic_clock::time_point capture_time,
+    const frc971::vision::TargetPoseFbs &target,
+    TargetEstimateDebugStatic *debug_builder) {
+  ++total_candidate_targets_;
+  ++cameras_.at(camera_index).total_candidate_targets;
+  const uint64_t target_id = target.id();
+
+  if (debug_builder == nullptr) {
+    AOS_LOG(ERROR, "Dropped message from debug vector.");
+  } else {
+    debug_builder->set_camera(camera_index);
+    debug_builder->set_image_age_sec(aos::time::DurationInSeconds(
+        event_loop_->monotonic_now() - capture_time));
+    debug_builder->set_image_monotonic_timestamp_ns(
+        std::chrono::duration_cast<std::chrono::nanoseconds>(
+            capture_time.time_since_epoch())
+            .count());
+    debug_builder->set_april_tag(target_id);
+  }
+  VLOG(2) << aos::FlatbufferToJson(&target);
+  if (!UseAprilTag(target_id)) {
+    VLOG(1) << "Rejecting target due to invalid ID " << target_id;
+    RejectImage(camera_index, RejectionReason::NO_SUCH_TARGET, debug_builder);
+    return;
+  }
+
+  const Transform &H_field_target = target_poses_.at(target_id);
+  const Transform &H_robot_camera = cameras_.at(camera_index).extrinsics;
+
+  const Transform H_camera_target = PoseToTransform(&target);
+
+  // In order to do the EKF correction, we determine the expected state based
+  // on the state at the time the image was captured; however, we insert the
+  // correction update itself at the current time. This is technically not
+  // quite correct, but saves substantial CPU usage & code complexity by
+  // making it so that we don't have to constantly rewind the entire EKF
+  // history.
+  const std::optional<State> state_at_capture =
+      ekf_.LastStateBeforeTime(capture_time);
+
+  if (!state_at_capture.has_value()) {
+    VLOG(1) << "Rejecting image due to being too old.";
+    return RejectImage(camera_index, RejectionReason::IMAGE_TOO_OLD,
+                       debug_builder);
+  } else if (target.pose_error() > FLAGS_max_pose_error) {
+    VLOG(1) << "Rejecting target due to high pose error "
+            << target.pose_error();
+    return RejectImage(camera_index, RejectionReason::HIGH_POSE_ERROR,
+                       debug_builder);
+  } else if (target.pose_error_ratio() > FLAGS_max_pose_error_ratio) {
+    VLOG(1) << "Rejecting target due to high pose error ratio "
+            << target.pose_error_ratio();
+    return RejectImage(camera_index, RejectionReason::HIGH_POSE_ERROR_RATIO,
+                       debug_builder);
+  }
+
+  Corrector corrector(state_at_capture.value(), H_field_target, H_robot_camera,
+                      H_camera_target);
+  const double distance_to_target = corrector.observed()(Corrector::kDistance);
+
+  // Heading, distance, skew at 1 meter.
+  Eigen::Matrix<double, 3, 1> noises(0.01, 0.05, 0.05);
+  const double distance_noise_scalar = std::pow(distance_to_target, 2.0);
+  noises(Corrector::kDistance) *= distance_noise_scalar;
+  noises(Corrector::kSkew) *= distance_noise_scalar;
+  // TODO(james): This is leftover from last year; figure out if we want it.
+  // Scale noise by the distortion factor for this detection
+  noises *= (1.0 + FLAGS_distortion_noise_scalar * target.distortion_factor());
+
+  Eigen::Matrix3d R = Eigen::Matrix3d::Zero();
+  R.diagonal() = noises.cwiseAbs2();
+  if (debug_builder != nullptr) {
+    const Eigen::Vector3d camera_position =
+        corrector.observed_camera_pose().abs_pos();
+    debug_builder->set_camera_x(camera_position.x());
+    debug_builder->set_camera_y(camera_position.y());
+    debug_builder->set_camera_theta(
+        corrector.observed_camera_pose().abs_theta());
+    // Calculate the camera-to-robot transformation matrix ignoring the
+    // pitch/roll of the camera.
+    const Transform H_camera_robot_stripped =
+        frc971::control_loops::Pose(ZToXCamera(H_robot_camera))
+            .AsTransformationMatrix()
+            .inverse();
+    const frc971::control_loops::Pose measured_pose(
+        corrector.observed_camera_pose().AsTransformationMatrix() *
+        H_camera_robot_stripped);
+    debug_builder->set_implied_robot_x(measured_pose.rel_pos().x());
+    debug_builder->set_implied_robot_y(measured_pose.rel_pos().y());
+    debug_builder->set_implied_robot_theta(measured_pose.rel_theta());
+
+    Corrector::PopulateMeasurement(corrector.expected(),
+                                   debug_builder->add_expected_observation());
+    Corrector::PopulateMeasurement(corrector.observed(),
+                                   debug_builder->add_actual_observation());
+    Corrector::PopulateMeasurement(noises, debug_builder->add_modeled_noise());
+  }
+
+  const double camera_yaw_error =
+      aos::math::NormalizeAngle(corrector.expected_camera_pose().abs_theta() -
+                                corrector.observed_camera_pose().abs_theta());
+  constexpr double kDegToRad = M_PI / 180.0;
+
+  const double robot_speed =
+      (state_at_capture.value()(StateIdx::kLeftVelocity) +
+       state_at_capture.value()(StateIdx::kRightVelocity)) /
+      2.0;
+  const double yaw_threshold =
+      (utils_.MaybeInAutonomous() ? FLAGS_max_implied_yaw_error
+                                  : FLAGS_max_implied_teleop_yaw_error) *
+      kDegToRad;
+
+  if (utils_.MaybeInAutonomous() &&
+      (std::abs(robot_speed) > FLAGS_max_auto_image_robot_speed)) {
+    return RejectImage(camera_index, RejectionReason::ROBOT_TOO_FAST,
+                       debug_builder);
+  } else if (std::abs(camera_yaw_error) > yaw_threshold) {
+    return RejectImage(camera_index, RejectionReason::HIGH_IMPLIED_YAW_ERROR,
+                       debug_builder);
+  } else if (distance_to_target > FLAGS_max_distance_to_target) {
+    return RejectImage(camera_index, RejectionReason::HIGH_DISTANCE_TO_TARGET,
+                       debug_builder);
+  }
+
+  const Input U = ekf_.MostRecentInput();
+  VLOG(1) << "previous state " << ekf_.X_hat().topRows<3>().transpose();
+  const State prior_state = ekf_.X_hat();
+  // For the correction step, instead of passing in the measurement directly,
+  // we pass in (0, 0, 0) as the measurement and then for the expected
+  // measurement (Zhat) we calculate the error between the pose implied by
+  // the camera measurement and the current estimate of the
+  // pose. This doesn't affect any of the math, it just makes the code a bit
+  // more convenient to write given the Correct() interface we already have.
+  observations_.CorrectKnownH(Eigen::Vector3d::Zero(), &U, corrector, R, t_);
+  ++total_accepted_targets_;
+  ++cameras_.at(camera_index).total_accepted_targets;
+  VLOG(1) << "new state " << ekf_.X_hat().topRows<3>().transpose();
+  if (debug_builder != nullptr) {
+    debug_builder->set_correction_x(ekf_.X_hat()(StateIdx::kX) -
+                                    prior_state(StateIdx::kX));
+    debug_builder->set_correction_y(ekf_.X_hat()(StateIdx::kY) -
+                                    prior_state(StateIdx::kY));
+    debug_builder->set_correction_theta(ekf_.X_hat()(StateIdx::kTheta) -
+                                        prior_state(StateIdx::kTheta));
+    debug_builder->set_accepted(true);
+  }
+}
+
+void Localizer::SendOutput() {
+  auto builder = output_sender_.MakeBuilder();
+  frc971::controls::LocalizerOutput::Builder output_builder =
+      builder.MakeBuilder<frc971::controls::LocalizerOutput>();
+  output_builder.add_monotonic_timestamp_ns(
+      std::chrono::duration_cast<std::chrono::nanoseconds>(
+          event_loop_->context().monotonic_event_time.time_since_epoch())
+          .count());
+  output_builder.add_x(ekf_.X_hat(StateIdx::kX));
+  output_builder.add_y(ekf_.X_hat(StateIdx::kY));
+  output_builder.add_theta(ekf_.X_hat(StateIdx::kTheta));
+  output_builder.add_zeroed(imu_watcher_.zeroer().Zeroed());
+  output_builder.add_image_accepted_count(total_accepted_targets_);
+  const Eigen::Quaterniond &orientation =
+      Eigen::AngleAxis<double>(ekf_.X_hat(StateIdx::kTheta),
+                               Eigen::Vector3d::UnitZ()) *
+      down_estimator_.X_hat();
+  frc971::controls::Quaternion quaternion;
+  quaternion.mutate_x(orientation.x());
+  quaternion.mutate_y(orientation.y());
+  quaternion.mutate_z(orientation.z());
+  quaternion.mutate_w(orientation.w());
+  output_builder.add_orientation(&quaternion);
+  server_statistics_fetcher_.Fetch();
+  client_statistics_fetcher_.Fetch();
+
+  bool orins_connected = true;
+
+  if (server_statistics_fetcher_.get()) {
+    for (const auto *orin_server_status :
+         *server_statistics_fetcher_->connections()) {
+      if (orin_server_status->state() ==
+          aos::message_bridge::State::DISCONNECTED) {
+        orins_connected = false;
+      }
+    }
+  }
+
+  if (client_statistics_fetcher_.get()) {
+    for (const auto *pi_client_status :
+         *client_statistics_fetcher_->connections()) {
+      if (pi_client_status->state() ==
+          aos::message_bridge::State::DISCONNECTED) {
+        orins_connected = false;
+      }
+    }
+  }
+
+  // The output message is year-agnostic, and retains "pi" naming for histrocial
+  // reasons.
+  output_builder.add_all_pis_connected(orins_connected);
+  builder.CheckOk(builder.Send(output_builder.Finish()));
+}
+
+flatbuffers::Offset<frc971::control_loops::drivetrain::LocalizerState>
+Localizer::PopulateState(const State &X_hat,
+                         flatbuffers::FlatBufferBuilder *fbb) {
+  frc971::control_loops::drivetrain::LocalizerState::Builder builder(*fbb);
+  builder.add_x(X_hat(StateIdx::kX));
+  builder.add_y(X_hat(StateIdx::kY));
+  builder.add_theta(X_hat(StateIdx::kTheta));
+  builder.add_left_velocity(X_hat(StateIdx::kLeftVelocity));
+  builder.add_right_velocity(X_hat(StateIdx::kRightVelocity));
+  builder.add_left_encoder(X_hat(StateIdx::kLeftEncoder));
+  builder.add_right_encoder(X_hat(StateIdx::kRightEncoder));
+  builder.add_left_voltage_error(X_hat(StateIdx::kLeftVoltageError));
+  builder.add_right_voltage_error(X_hat(StateIdx::kRightVoltageError));
+  builder.add_angular_error(X_hat(StateIdx::kAngularError));
+  builder.add_longitudinal_velocity_offset(
+      X_hat(StateIdx::kLongitudinalVelocityOffset));
+  builder.add_lateral_velocity(X_hat(StateIdx::kLateralVelocity));
+  return builder.Finish();
+}
+
+flatbuffers::Offset<ImuStatus> Localizer::PopulateImu(
+    flatbuffers::FlatBufferBuilder *fbb) const {
+  const auto zeroer_offset = imu_watcher_.zeroer().PopulateStatus(fbb);
+  const auto failures_offset = imu_watcher_.PopulateImuFailures(fbb);
+  ImuStatus::Builder builder(*fbb);
+  builder.add_zeroed(imu_watcher_.zeroer().Zeroed());
+  builder.add_faulted_zero(imu_watcher_.zeroer().Faulted());
+  builder.add_zeroing(zeroer_offset);
+  if (imu_watcher_.pico_offset().has_value()) {
+    builder.add_board_offset_ns(imu_watcher_.pico_offset().value().count());
+    builder.add_board_offset_error_ns(imu_watcher_.pico_offset_error().count());
+  }
+  if (last_encoder_readings_.has_value()) {
+    builder.add_left_encoder(last_encoder_readings_.value()(0));
+    builder.add_right_encoder(last_encoder_readings_.value()(1));
+  }
+  builder.add_imu_failures(failures_offset);
+  return builder.Finish();
+}
+
+flatbuffers::Offset<CumulativeStatistics> Localizer::StatisticsForCamera(
+    const CameraState &camera, flatbuffers::FlatBufferBuilder *fbb) {
+  const auto counts_offset = camera.rejection_counter.PopulateCounts(fbb);
+  CumulativeStatistics::Builder stats_builder(*fbb);
+  stats_builder.add_total_accepted(camera.total_accepted_targets);
+  stats_builder.add_total_candidates(camera.total_candidate_targets);
+  stats_builder.add_rejection_reasons(counts_offset);
+  return stats_builder.Finish();
+}
+
+void Localizer::StatisticsForCamera(const CameraState &camera,
+                                    CumulativeStatisticsStatic *builder) {
+  camera.rejection_counter.PopulateCountsStaticFbs(
+      builder->add_rejection_reasons());
+  builder->set_total_accepted(camera.total_accepted_targets);
+  builder->set_total_candidates(camera.total_candidate_targets);
+}
+
+void Localizer::SendStatus() {
+  auto builder = status_sender_.MakeBuilder();
+  std::array<flatbuffers::Offset<CumulativeStatistics>, kNumCameras>
+      stats_offsets;
+  for (size_t ii = 0; ii < kNumCameras; ++ii) {
+    stats_offsets.at(ii) = StatisticsForCamera(cameras_.at(ii), builder.fbb());
+  }
+  auto stats_offset =
+      builder.fbb()->CreateVector(stats_offsets.data(), stats_offsets.size());
+  auto down_estimator_offset =
+      down_estimator_.PopulateStatus(builder.fbb(), t_);
+  auto imu_offset = PopulateImu(builder.fbb());
+  auto state_offset = PopulateState(ekf_.X_hat(), builder.fbb());
+  Status::Builder status_builder = builder.MakeBuilder<Status>();
+  status_builder.add_state(state_offset);
+  status_builder.add_down_estimator(down_estimator_offset);
+  status_builder.add_imu(imu_offset);
+  status_builder.add_statistics(stats_offset);
+  builder.CheckOk(builder.Send(status_builder.Finish()));
+}
+
+Eigen::Vector3d Localizer::Corrector::HeadingDistanceSkew(
+    const Pose &relative_pose) {
+  const double heading = relative_pose.heading();
+  const double distance = relative_pose.xy_norm();
+  const double skew =
+      ::aos::math::NormalizeAngle(relative_pose.rel_theta() - heading);
+  return {heading, distance, skew};
+}
+
+Localizer::Corrector Localizer::Corrector::CalculateHeadingDistanceSkewH(
+    const State &state_at_capture, const Transform &H_field_target,
+    const Transform &H_robot_camera, const Transform &H_camera_target) {
+  const Transform H_field_camera = H_field_target * H_camera_target.inverse();
+  const Pose expected_robot_pose(
+      {state_at_capture(StateIdx::kX), state_at_capture(StateIdx::kY), 0.0},
+      state_at_capture(StateIdx::kTheta));
+  // Observed position on the field, reduced to just the 2-D pose.
+  const Pose observed_camera(ZToXCamera(H_field_camera));
+  const Pose expected_camera(expected_robot_pose.AsTransformationMatrix() *
+                             ZToXCamera(H_robot_camera));
+  const Pose nominal_target(ZToXCamera(H_field_target));
+  const Pose observed_target = nominal_target.Rebase(&observed_camera);
+  const Pose expected_target = nominal_target.Rebase(&expected_camera);
+  return Localizer::Corrector{
+      expected_robot_pose,
+      observed_camera,
+      expected_camera,
+      HeadingDistanceSkew(expected_target),
+      HeadingDistanceSkew(observed_target),
+      frc971::control_loops::drivetrain::HMatrixForCameraHeadingDistanceSkew(
+          nominal_target, observed_camera)};
+}
+
+Localizer::Corrector::Corrector(const State &state_at_capture,
+                                const Transform &H_field_target,
+                                const Transform &H_robot_camera,
+                                const Transform &H_camera_target)
+    : Corrector(CalculateHeadingDistanceSkewH(
+          state_at_capture, H_field_target, H_robot_camera, H_camera_target)) {}
+
+Localizer::Output Localizer::Corrector::H(const State &, const Input &) {
+  return expected_ - observed_;
+}
+
+}  // namespace y2024::localizer
diff --git a/y2024/localizer/localizer.h b/y2024/localizer/localizer.h
new file mode 100644
index 0000000..8235f13
--- /dev/null
+++ b/y2024/localizer/localizer.h
@@ -0,0 +1,158 @@
+#ifndef Y2024_LOCALIZER_LOCALIZER_H_
+#define Y2024_LOCALIZER_LOCALIZER_H_
+
+#include <array>
+#include <map>
+
+#include "aos/network/message_bridge_client_generated.h"
+#include "aos/network/message_bridge_server_generated.h"
+#include "frc971/constants/constants_sender_lib.h"
+#include "frc971/control_loops/drivetrain/hybrid_ekf.h"
+#include "frc971/control_loops/drivetrain/improved_down_estimator.h"
+#include "frc971/control_loops/drivetrain/localization/localizer_output_generated.h"
+#include "frc971/control_loops/drivetrain/localization/utils.h"
+#include "frc971/imu_reader/imu_watcher.h"
+#include "frc971/vision/target_map_generated.h"
+#include "y2024/constants/constants_generated.h"
+#include "y2024/localizer/status_generated.h"
+#include "y2024/localizer/visualization_static.h"
+
+namespace y2024::localizer {
+
+class Localizer {
+ public:
+  static constexpr size_t kNumCameras = 4;
+  using Pose = frc971::control_loops::Pose;
+  typedef Eigen::Matrix<double, 4, 4> Transform;
+  typedef frc971::control_loops::drivetrain::HybridEkf<double> HybridEkf;
+  typedef HybridEkf::State State;
+  typedef HybridEkf::Output Output;
+  typedef HybridEkf::Input Input;
+  typedef HybridEkf::StateIdx StateIdx;
+  Localizer(aos::EventLoop *event_loop);
+
+ private:
+  class Corrector : public HybridEkf::ExpectedObservationFunctor {
+   public:
+    // Indices used for each of the members of the output vector for this
+    // Corrector.
+    enum OutputIdx {
+      kHeading = 0,
+      kDistance = 1,
+      kSkew = 2,
+    };
+    Corrector(const State &state_at_capture, const Transform &H_field_target,
+              const Transform &H_robot_camera,
+              const Transform &H_camera_target);
+
+    using HMatrix = Eigen::Matrix<double, Localizer::HybridEkf::kNOutputs,
+                                  Localizer::HybridEkf::kNStates>;
+
+    Output H(const State &, const Input &) final;
+    HMatrix DHDX(const State &) final { return H_; }
+    const Eigen::Vector3d &observed() const { return observed_; }
+    const Eigen::Vector3d &expected() const { return expected_; }
+    const Pose &expected_robot_pose() const { return expected_robot_pose_; }
+    const Pose &expected_camera_pose() const { return expected_camera_; }
+    const Pose &observed_camera_pose() const { return observed_camera_; }
+
+    static Eigen::Vector3d HeadingDistanceSkew(const Pose &relative_pose);
+
+    static Corrector CalculateHeadingDistanceSkewH(
+        const State &state_at_capture, const Transform &H_field_target,
+        const Transform &H_robot_camera, const Transform &H_camera_target);
+
+    static void PopulateMeasurement(const Eigen::Vector3d &vector,
+                                    MeasurementStatic *builder) {
+      builder->set_heading(vector(kHeading));
+      builder->set_distance(vector(kDistance));
+      builder->set_skew(vector(kSkew));
+    }
+
+   private:
+    Corrector(const Pose &expected_robot_pose, const Pose &observed_camera,
+              const Pose &expected_camera, const Eigen::Vector3d &expected,
+              const Eigen::Vector3d &observed, const HMatrix &H)
+        : expected_robot_pose_(expected_robot_pose),
+          observed_camera_(observed_camera),
+          expected_camera_(expected_camera),
+          expected_(expected),
+          observed_(observed),
+          H_(H) {}
+    // For debugging.
+    const Pose expected_robot_pose_;
+    const Pose observed_camera_;
+    const Pose expected_camera_;
+    // Actually used.
+    const Eigen::Vector3d expected_;
+    const Eigen::Vector3d observed_;
+    const HMatrix H_;
+  };
+
+  struct CameraState {
+    aos::Sender<VisualizationStatic> debug_sender;
+    Transform extrinsics = Transform::Zero();
+    aos::util::ArrayErrorCounter<RejectionReason, RejectionCount>
+        rejection_counter;
+    size_t total_candidate_targets = 0;
+    size_t total_accepted_targets = 0;
+  };
+
+  static std::array<CameraState, kNumCameras> MakeCameras(
+      const Constants &constants, aos::EventLoop *event_loop);
+  void HandleTarget(int camera_index,
+                    const aos::monotonic_clock::time_point capture_time,
+                    const frc971::vision::TargetPoseFbs &target,
+                    TargetEstimateDebugStatic *debug_builder);
+  void HandleImu(aos::monotonic_clock::time_point sample_time_pico,
+                 aos::monotonic_clock::time_point sample_time_pi,
+                 std::optional<Eigen::Vector2d> encoders, Eigen::Vector3d gyro,
+                 Eigen::Vector3d accel);
+  void RejectImage(int camera_index, RejectionReason reason,
+                   TargetEstimateDebugStatic *builder);
+
+  void SendOutput();
+  static flatbuffers::Offset<frc971::control_loops::drivetrain::LocalizerState>
+  PopulateState(const State &X_hat, flatbuffers::FlatBufferBuilder *fbb);
+  flatbuffers::Offset<ImuStatus> PopulateImu(
+      flatbuffers::FlatBufferBuilder *fbb) const;
+  void SendStatus();
+  static flatbuffers::Offset<CumulativeStatistics> StatisticsForCamera(
+      const CameraState &camera, flatbuffers::FlatBufferBuilder *fbb);
+  static void StatisticsForCamera(const CameraState &camera,
+                                  CumulativeStatisticsStatic *builder);
+
+  bool UseAprilTag(uint64_t target_id);
+
+  aos::EventLoop *const event_loop_;
+  frc971::constants::ConstantsFetcher<Constants> constants_fetcher_;
+  const frc971::control_loops::drivetrain::DrivetrainConfig<double> dt_config_;
+  std::array<CameraState, kNumCameras> cameras_;
+  const std::array<Transform, kNumCameras> camera_extrinsics_;
+  const std::map<uint64_t, Transform> target_poses_;
+
+  frc971::control_loops::drivetrain::DrivetrainUkf down_estimator_;
+  HybridEkf ekf_;
+  HybridEkf::ExpectedObservationAllocator<Corrector> observations_;
+
+  frc971::controls::ImuWatcher imu_watcher_;
+  frc971::control_loops::drivetrain::LocalizationUtils utils_;
+
+  aos::Sender<Status> status_sender_;
+  aos::Sender<frc971::controls::LocalizerOutput> output_sender_;
+  aos::monotonic_clock::time_point t_ = aos::monotonic_clock::min_time;
+  size_t clock_resets_ = 0;
+
+  size_t total_candidate_targets_ = 0;
+  size_t total_accepted_targets_ = 0;
+
+  // For the status message.
+  std::optional<Eigen::Vector2d> last_encoder_readings_;
+
+  aos::Fetcher<aos::message_bridge::ServerStatistics>
+      server_statistics_fetcher_;
+  aos::Fetcher<aos::message_bridge::ClientStatistics>
+      client_statistics_fetcher_;
+};
+}  // namespace y2024::localizer
+#endif  // Y2024_LOCALIZER_LOCALIZER_H_
diff --git a/y2024/localizer/localizer_logger.cc b/y2024/localizer/localizer_logger.cc
new file mode 100644
index 0000000..ec8d3ac
--- /dev/null
+++ b/y2024/localizer/localizer_logger.cc
@@ -0,0 +1,80 @@
+#include <sys/resource.h>
+#include <sys/time.h>
+
+#include "gflags/gflags.h"
+#include "glog/logging.h"
+
+#include "aos/configuration.h"
+#include "aos/events/logging/log_writer.h"
+#include "aos/events/logging/snappy_encoder.h"
+#include "aos/events/shm_event_loop.h"
+#include "aos/init.h"
+#include "aos/logging/log_namer.h"
+
+DEFINE_string(config, "aos_config.json", "Config file to use.");
+
+DEFINE_double(rotate_every, 30.0,
+              "If set, rotate the logger after this many seconds");
+
+DECLARE_int32(flush_size);
+
+int main(int argc, char *argv[]) {
+  gflags::SetUsageMessage(
+      "This program provides a simple logger binary that logs all SHMEM data "
+      "directly to a file specified at the command line. It does not manage "
+      "filenames, so it will just crash if you attempt to overwrite an "
+      "existing file, and the user must specify the logfile manually at the "
+      "command line.");
+  aos::InitGoogle(&argc, &argv);
+
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config =
+      aos::configuration::ReadConfig(FLAGS_config);
+
+  aos::ShmEventLoop event_loop(&config.message());
+
+  auto log_namer = std::make_unique<aos::logger::MultiNodeFilesLogNamer>(
+      &event_loop,
+      std::make_unique<aos::logger::RenamableFileBackend>(
+          absl::StrCat(aos::logging::GetLogName("localizer_log"), "/"),
+          /*O_DIRECT*/ true));
+
+  log_namer->set_extension(aos::logger::SnappyDecoder::kExtension);
+  log_namer->set_encoder_factory([](size_t max_message_size) {
+    return std::make_unique<aos::logger::SnappyEncoder>(max_message_size,
+                                                        FLAGS_flush_size);
+  });
+
+  aos::monotonic_clock::time_point last_rotation_time =
+      event_loop.monotonic_now();
+  aos::logger::Logger logger(
+      &event_loop, event_loop.configuration(),
+      // Only log channels smaller than ~10 MB / sec.
+      [](const aos::Channel *channel) {
+        return (channel->max_size() * channel->frequency()) < 10e6;
+      });
+
+  if (FLAGS_rotate_every != 0.0) {
+    logger.set_on_logged_period(
+        [&logger, &last_rotation_time](aos::monotonic_clock::time_point t) {
+          if (t > last_rotation_time +
+                      std::chrono::duration<double>(FLAGS_rotate_every)) {
+            logger.Rotate();
+            last_rotation_time = t;
+          }
+        });
+  }
+
+  event_loop.OnRun([&log_namer, &logger]() {
+    errno = 0;
+    setpriority(PRIO_PROCESS, 0, -20);
+    PCHECK(errno == 0)
+        << ": Renicing to -20 failed, use --skip_renicing to skip renicing.";
+    logger.StartLogging(std::move(log_namer));
+  });
+
+  event_loop.Run();
+
+  LOG(INFO) << "Shutting down";
+
+  return 0;
+}
diff --git a/y2024/localizer/localizer_main.cc b/y2024/localizer/localizer_main.cc
new file mode 100644
index 0000000..25466d2
--- /dev/null
+++ b/y2024/localizer/localizer_main.cc
@@ -0,0 +1,23 @@
+#include "aos/events/shm_event_loop.h"
+#include "aos/init.h"
+#include "frc971/constants/constants_sender_lib.h"
+#include "y2024/control_loops/drivetrain/drivetrain_base.h"
+#include "y2024/localizer/localizer.h"
+
+DEFINE_string(config, "aos_config.json", "Path to the config file to use.");
+
+int main(int argc, char *argv[]) {
+  aos::InitGoogle(&argc, &argv);
+
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config =
+      aos::configuration::ReadConfig(FLAGS_config);
+
+  frc971::constants::WaitForConstants<y2024::Constants>(&config.message());
+
+  aos::ShmEventLoop event_loop(&config.message());
+  y2024::localizer::Localizer localizer(&event_loop);
+
+  event_loop.Run();
+
+  return 0;
+}
diff --git a/y2024/localizer/localizer_plotter.ts b/y2024/localizer/localizer_plotter.ts
new file mode 100644
index 0000000..b5e764b
--- /dev/null
+++ b/y2024/localizer/localizer_plotter.ts
@@ -0,0 +1,203 @@
+// Provides a plot for debugging drivetrain-related issues.
+import {AosPlotter} from '../../aos/network/www/aos_plotter';
+import {ImuMessageHandler} from '../../frc971/wpilib/imu_plot_utils';
+import * as proxy from '../../aos/network/www/proxy';
+import {BLUE, BROWN, CYAN, GREEN, PINK, RED, WHITE} from '../../aos/network/www/colors';
+
+import Connection = proxy.Connection;
+
+const TIME = AosPlotter.TIME;
+const DEFAULT_WIDTH = AosPlotter.DEFAULT_WIDTH;
+const DEFAULT_HEIGHT = AosPlotter.DEFAULT_HEIGHT;
+
+export function plotLocalizer(conn: Connection, element: Element): void {
+  const aosPlotter = new AosPlotter(conn);
+
+  const position = aosPlotter.addMessageSource("/drivetrain",
+      "frc971.control_loops.drivetrain.Position");
+  const status = aosPlotter.addMessageSource(
+      '/drivetrain', 'frc971.control_loops.drivetrain.Status');
+  const output = aosPlotter.addMessageSource(
+      '/drivetrain', 'frc971.control_loops.drivetrain.Output');
+  const localizer = aosPlotter.addMessageSource(
+      '/localizer', 'y2024.localizer.Status');
+  const imu = aosPlotter.addRawMessageSource(
+      '/localizer', 'frc971.IMUValuesBatch',
+      new ImuMessageHandler(conn.getSchema('frc971.IMUValuesBatch')));
+
+  // Drivetrain Status estimated relative position
+  const positionPlot = aosPlotter.addPlot(element);
+  positionPlot.plot.getAxisLabels().setTitle("Estimated Relative Position " +
+                                             "of the Drivetrain");
+  positionPlot.plot.getAxisLabels().setXLabel(TIME);
+  positionPlot.plot.getAxisLabels().setYLabel("Relative Position (m)");
+  const leftPosition =
+      positionPlot.addMessageLine(status, ["estimated_left_position"]);
+  leftPosition.setColor(RED);
+  const rightPosition =
+      positionPlot.addMessageLine(status, ["estimated_right_position"]);
+  rightPosition.setColor(GREEN);
+  positionPlot.addMessageLine(position, ['left_encoder'])
+      .setColor(BROWN)
+      .setDrawLine(false);
+  positionPlot.addMessageLine(position, ['right_encoder'])
+      .setColor(CYAN)
+      .setDrawLine(false);
+
+
+  // Drivetrain Velocities
+  const velocityPlot = aosPlotter.addPlot(element);
+  velocityPlot.plot.getAxisLabels().setTitle('Velocity Plots');
+  velocityPlot.plot.getAxisLabels().setXLabel(TIME);
+  velocityPlot.plot.getAxisLabels().setYLabel('Wheel Velocity (m/s)');
+
+  const leftVelocity =
+      velocityPlot.addMessageLine(status, ['estimated_left_velocity']);
+  leftVelocity.setColor(RED);
+  const rightVelocity =
+      velocityPlot.addMessageLine(status, ['estimated_right_velocity']);
+  rightVelocity.setColor(GREEN);
+
+  const leftSpeed = velocityPlot.addMessageLine(position, ["left_speed"]);
+  leftSpeed.setColor(BLUE);
+  const rightSpeed = velocityPlot.addMessageLine(position, ["right_speed"]);
+  rightSpeed.setColor(BROWN);
+
+  const ekfLeftVelocity = velocityPlot.addMessageLine(
+      localizer, ['state', 'left_velocity']);
+  ekfLeftVelocity.setColor(RED);
+  ekfLeftVelocity.setPointSize(0.0);
+  const ekfRightVelocity = velocityPlot.addMessageLine(
+      localizer, ['state', 'right_velocity']);
+  ekfRightVelocity.setColor(GREEN);
+  ekfRightVelocity.setPointSize(0.0);
+
+  // Lateral velocity
+  const lateralPlot = aosPlotter.addPlot(element);
+  lateralPlot.plot.getAxisLabels().setTitle('Lateral Velocity');
+  lateralPlot.plot.getAxisLabels().setXLabel(TIME);
+  lateralPlot.plot.getAxisLabels().setYLabel('Velocity (m/s)');
+
+  lateralPlot.addMessageLine(localizer, ['state', 'lateral_velocity']).setColor(CYAN);
+
+  // Drivetrain Voltage
+  const voltagePlot = aosPlotter.addPlot(element);
+  voltagePlot.plot.getAxisLabels().setTitle('Voltage Plots');
+  voltagePlot.plot.getAxisLabels().setXLabel(TIME);
+  voltagePlot.plot.getAxisLabels().setYLabel('Voltage (V)')
+
+  voltagePlot.addMessageLine(localizer, ['state', 'left_voltage_error'])
+      .setColor(RED)
+      .setDrawLine(false);
+  voltagePlot.addMessageLine(localizer, ['state', 'right_voltage_error'])
+      .setColor(GREEN)
+      .setDrawLine(false);
+  voltagePlot.addMessageLine(output, ['left_voltage'])
+      .setColor(RED)
+      .setPointSize(0);
+  voltagePlot.addMessageLine(output, ['right_voltage'])
+      .setColor(GREEN)
+      .setPointSize(0);
+
+  // Heading
+  const yawPlot = aosPlotter.addPlot(element);
+  yawPlot.plot.getAxisLabels().setTitle('Robot Yaw');
+  yawPlot.plot.getAxisLabels().setXLabel(TIME);
+  yawPlot.plot.getAxisLabels().setYLabel('Yaw (rad)');
+
+  yawPlot.addMessageLine(status, ['localizer', 'theta']).setColor(GREEN);
+
+  yawPlot.addMessageLine(localizer, ['down_estimator', 'yaw']).setColor(BLUE);
+
+  yawPlot.addMessageLine(localizer, ['state', 'theta']).setColor(RED);
+
+  // Pitch/Roll
+  const orientationPlot = aosPlotter.addPlot(element);
+  orientationPlot.plot.getAxisLabels().setTitle('Orientation');
+  orientationPlot.plot.getAxisLabels().setXLabel(TIME);
+  orientationPlot.plot.getAxisLabels().setYLabel('Angle (rad)');
+
+  orientationPlot.addMessageLine(localizer, ['down_estimator', 'lateral_pitch'])
+      .setColor(RED)
+      .setLabel('roll');
+  orientationPlot
+      .addMessageLine(localizer, ['down_estimator', 'longitudinal_pitch'])
+      .setColor(GREEN)
+      .setLabel('pitch');
+
+  const stillPlot = aosPlotter.addPlot(element, [DEFAULT_WIDTH, DEFAULT_HEIGHT / 3]);
+  stillPlot.plot.getAxisLabels().setTitle('Still Plot');
+  stillPlot.plot.getAxisLabels().setXLabel(TIME);
+  stillPlot.plot.getAxisLabels().setYLabel('bool, g\'s');
+  stillPlot.plot.setDefaultYRange([-0.1, 1.1]);
+
+  stillPlot.addMessageLine(localizer, ['down_estimator', 'gravity_magnitude'])
+      .setColor(WHITE)
+      .setDrawLine(false);
+
+  // Absolute X Position
+  const xPositionPlot = aosPlotter.addPlot(element);
+  xPositionPlot.plot.getAxisLabels().setTitle('X Position');
+  xPositionPlot.plot.getAxisLabels().setXLabel(TIME);
+  xPositionPlot.plot.getAxisLabels().setYLabel('X Position (m)');
+
+  xPositionPlot.addMessageLine(status, ['x']).setColor(RED);
+  xPositionPlot.addMessageLine(localizer, ['down_estimator', 'position_x'])
+      .setColor(BLUE);
+  xPositionPlot.addMessageLine(localizer, ['state', 'x']).setColor(CYAN);
+
+  // Absolute Y Position
+  const yPositionPlot = aosPlotter.addPlot(element);
+  yPositionPlot.plot.getAxisLabels().setTitle('Y Position');
+  yPositionPlot.plot.getAxisLabels().setXLabel(TIME);
+  yPositionPlot.plot.getAxisLabels().setYLabel('Y Position (m)');
+
+  const localizerY = yPositionPlot.addMessageLine(status, ['y']);
+  localizerY.setColor(RED);
+  yPositionPlot.addMessageLine(localizer, ['down_estimator', 'position_y'])
+      .setColor(BLUE);
+  yPositionPlot.addMessageLine(localizer, ['state', 'y']).setColor(CYAN);
+
+  // Gyro
+  const gyroPlot = aosPlotter.addPlot(element);
+  gyroPlot.plot.getAxisLabels().setTitle('Gyro Readings');
+  gyroPlot.plot.getAxisLabels().setYLabel('Angular Velocity (rad / sec)');
+  gyroPlot.plot.getAxisLabels().setXLabel('Monotonic Reading Time (sec)');
+
+  const gyroX = gyroPlot.addMessageLine(imu, ['gyro_x']);
+  gyroX.setColor(RED);
+  const gyroY = gyroPlot.addMessageLine(imu, ['gyro_y']);
+  gyroY.setColor(GREEN);
+  const gyroZ = gyroPlot.addMessageLine(imu, ['gyro_z']);
+  gyroZ.setColor(BLUE);
+
+
+  const timingPlot =
+      aosPlotter.addPlot(element, [DEFAULT_WIDTH, DEFAULT_HEIGHT]);
+  timingPlot.plot.getAxisLabels().setTitle('Fault Counting');
+  timingPlot.plot.getAxisLabels().setXLabel(TIME);
+
+  timingPlot
+      .addMessageLine(
+          localizer, ['imu', 'imu_failures', 'imu_to_pico_checksum_mismatch'])
+      .setColor(BLUE)
+      .setDrawLine(false);
+
+  timingPlot
+      .addMessageLine(
+          localizer, ['imu', 'imu_failures', 'pico_to_pi_checksum_mismatch'])
+      .setColor(RED)
+      .setDrawLine(false);
+
+  timingPlot
+      .addMessageLine(
+          localizer, ['imu', 'imu_failures', 'other_zeroing_faults'])
+      .setColor(CYAN)
+      .setDrawLine(false);
+
+  timingPlot
+      .addMessageLine(
+          localizer, ['imu', 'imu_failures', 'missed_messages'])
+      .setColor(PINK)
+      .setDrawLine(false);
+}
diff --git a/y2024/localizer/localizer_replay.cc b/y2024/localizer/localizer_replay.cc
new file mode 100644
index 0000000..c2c4c16
--- /dev/null
+++ b/y2024/localizer/localizer_replay.cc
@@ -0,0 +1,68 @@
+#include "gflags/gflags.h"
+
+#include "aos/configuration.h"
+#include "aos/events/logging/log_reader.h"
+#include "aos/events/logging/log_writer.h"
+#include "aos/events/simulated_event_loop.h"
+#include "aos/init.h"
+#include "aos/json_to_flatbuffer.h"
+#include "aos/network/team_number.h"
+#include "aos/util/simulation_logger.h"
+#include "y2024/control_loops/drivetrain/drivetrain_base.h"
+#include "y2024/localizer/localizer.h"
+
+DEFINE_string(config, "y2024/aos_config.json",
+              "Name of the config file to replay using.");
+DEFINE_int32(team, 9971, "Team number to use for logfile replay.");
+DEFINE_string(output_folder, "/tmp/replayed",
+              "Name of the folder to write replayed logs to.");
+
+int main(int argc, char **argv) {
+  aos::InitGoogle(&argc, &argv);
+
+  aos::network::OverrideTeamNumber(FLAGS_team);
+
+  const aos::FlatbufferDetachedBuffer<aos::Configuration> config =
+      aos::configuration::ReadConfig(FLAGS_config);
+
+  // sort logfiles
+  const std::vector<aos::logger::LogFile> logfiles =
+      aos::logger::SortParts(aos::logger::FindLogs(argc, argv));
+
+  // open logfiles
+  aos::logger::LogReader reader(logfiles, &config.message());
+
+  reader.RemapLoggedChannel("/localizer", "y2024.localizer.Status");
+  for (const auto orin : {"orin1", "imu"}) {
+    for (const auto camera : {"camera0", "camera1"}) {
+      reader.RemapLoggedChannel(absl::StrCat("/", orin, "/", camera),
+                                "y2024.localizer.Visualization");
+    }
+  }
+  reader.RemapLoggedChannel("/localizer", "frc971.controls.LocalizerOutput");
+
+  auto factory =
+      std::make_unique<aos::SimulatedEventLoopFactory>(reader.configuration());
+
+  reader.RegisterWithoutStarting(factory.get());
+
+  const aos::Node *node = nullptr;
+  if (aos::configuration::MultiNode(reader.configuration())) {
+    node = aos::configuration::GetNode(reader.configuration(), "imu");
+  }
+  std::vector<std::unique_ptr<aos::util::LoggerState>> loggers;
+
+  reader.OnStart(node, [&factory, node, &loggers]() {
+    aos::NodeEventLoopFactory *node_factory =
+        factory->GetNodeEventLoopFactory(node);
+    node_factory->AlwaysStart<y2024::localizer::Localizer>("localizer");
+    loggers.push_back(std::make_unique<aos::util::LoggerState>(
+        factory.get(), node, FLAGS_output_folder));
+  });
+
+  reader.event_loop_factory()->Run();
+
+  reader.Deregister();
+
+  return 0;
+}
diff --git a/y2024/localizer/localizer_test.cc b/y2024/localizer/localizer_test.cc
new file mode 100644
index 0000000..a2fc200
--- /dev/null
+++ b/y2024/localizer/localizer_test.cc
@@ -0,0 +1,569 @@
+#include "y2024/localizer/localizer.h"
+
+#include "gtest/gtest.h"
+
+#include "aos/events/logging/log_writer.h"
+#include "aos/events/simulated_event_loop.h"
+#include "frc971/control_loops/drivetrain/drivetrain_test_lib.h"
+#include "frc971/control_loops/drivetrain/localizer_generated.h"
+#include "frc971/control_loops/drivetrain/rio_localizer_inputs_static.h"
+#include "frc971/control_loops/pose.h"
+#include "frc971/vision/target_map_generated.h"
+#include "frc971/vision/target_map_utils.h"
+#include "y2024/constants/simulated_constants_sender.h"
+#include "y2024/control_loops/drivetrain/drivetrain_base.h"
+#include "y2024/localizer/status_generated.h"
+
+DEFINE_string(output_folder, "",
+              "If set, logs all channels to the provided logfile.");
+DECLARE_double(max_distance_to_target);
+
+namespace y2024::localizer::testing {
+
+using frc971::control_loops::drivetrain::Output;
+
+class LocalizerTest : public ::testing::Test {
+ protected:
+  static constexpr uint64_t kTargetId = 1;
+  LocalizerTest()
+      : configuration_(aos::configuration::ReadConfig("y2024/aos_config.json")),
+        event_loop_factory_(&configuration_.message()),
+        roborio_node_([this]() {
+          // Get the constants sent before anything else happens.
+          // It has nothing to do with the roborio node.
+          SendSimulationConstants(&event_loop_factory_, 7971,
+                                  "y2024/constants/test_constants.json");
+          return aos::configuration::GetNode(&configuration_.message(),
+                                             "roborio");
+        }()),
+        imu_node_(
+            aos::configuration::GetNode(&configuration_.message(), "imu")),
+        camera_node_(
+            aos::configuration::GetNode(&configuration_.message(), "orin1")),
+        roborio_test_event_loop_(
+            event_loop_factory_.MakeEventLoop("test", roborio_node_)),
+        dt_config_(y2024::control_loops::drivetrain::GetDrivetrainConfig(
+            roborio_test_event_loop_.get())),
+        localizer_event_loop_(
+            event_loop_factory_.MakeEventLoop("localizer", imu_node_)),
+        localizer_(localizer_event_loop_.get()),
+        drivetrain_plant_event_loop_(event_loop_factory_.MakeEventLoop(
+            "drivetrain_plant", roborio_node_)),
+        drivetrain_plant_imu_event_loop_(
+            event_loop_factory_.MakeEventLoop("drivetrain_plant", imu_node_)),
+        drivetrain_plant_(drivetrain_plant_event_loop_.get(),
+                          drivetrain_plant_imu_event_loop_.get(), dt_config_,
+                          std::chrono::microseconds(1000)),
+        imu_test_event_loop_(
+            event_loop_factory_.MakeEventLoop("test", imu_node_)),
+        camera_test_event_loop_(
+            event_loop_factory_.MakeEventLoop("test", camera_node_)),
+        constants_fetcher_(imu_test_event_loop_.get()),
+        output_sender_(
+            roborio_test_event_loop_->MakeSender<Output>("/drivetrain")),
+        combined_sender_(
+            roborio_test_event_loop_->MakeSender<
+                frc971::control_loops::drivetrain::RioLocalizerInputsStatic>(
+                "/drivetrain")),
+        target_sender_(
+            camera_test_event_loop_->MakeSender<frc971::vision::TargetMap>(
+                "/camera0")),
+        control_sender_(roborio_test_event_loop_->MakeSender<
+                        frc971::control_loops::drivetrain::LocalizerControl>(
+            "/drivetrain")),
+        output_fetcher_(
+            roborio_test_event_loop_
+                ->MakeFetcher<frc971::controls::LocalizerOutput>("/localizer")),
+        status_fetcher_(
+            imu_test_event_loop_->MakeFetcher<Status>("/localizer")) {
+    FLAGS_max_distance_to_target = 100.0;
+    {
+      aos::TimerHandler *timer = roborio_test_event_loop_->AddTimer([this]() {
+        {
+          auto builder = output_sender_.MakeBuilder();
+          auto output_builder = builder.MakeBuilder<Output>();
+          output_builder.add_left_voltage(output_voltages_(0));
+          output_builder.add_right_voltage(output_voltages_(1));
+          builder.CheckOk(builder.Send(output_builder.Finish()));
+        }
+        {
+          auto builder = combined_sender_.MakeStaticBuilder();
+          builder->set_left_voltage(output_voltages_(0));
+          builder->set_right_voltage(output_voltages_(1));
+          builder->set_left_encoder(drivetrain_plant_.GetLeftPosition());
+          builder->set_right_encoder(drivetrain_plant_.GetRightPosition());
+          builder.CheckOk(builder.Send());
+        }
+      });
+      roborio_test_event_loop_->OnRun([timer, this]() {
+        timer->Schedule(roborio_test_event_loop_->monotonic_now(),
+                        std::chrono::milliseconds(5));
+      });
+    }
+    {
+      // Sanity check that the test calibration files look like what we
+      // expect.
+      CHECK_EQ("orin1", constants_fetcher_.constants()
+                            .cameras()
+                            ->Get(0)
+                            ->calibration()
+                            ->node_name()
+                            ->string_view());
+      CHECK_EQ(0, constants_fetcher_.constants()
+                      .cameras()
+                      ->Get(0)
+                      ->calibration()
+                      ->camera_number());
+      const Eigen::Matrix<double, 4, 4> H_robot_camera =
+          frc971::control_loops::drivetrain::FlatbufferToTransformationMatrix(
+              *constants_fetcher_.constants()
+                   .cameras()
+                   ->Get(0)
+                   ->calibration()
+                   ->fixed_extrinsics());
+
+      CHECK(constants_fetcher_.constants().common()->has_target_map());
+      CHECK(constants_fetcher_.constants()
+                .common()
+                ->target_map()
+                ->has_target_poses());
+      CHECK_LE(1u, constants_fetcher_.constants()
+                       .common()
+                       ->target_map()
+                       ->target_poses()
+                       ->size());
+      CHECK_EQ(kTargetId, constants_fetcher_.constants()
+                              .common()
+                              ->target_map()
+                              ->target_poses()
+                              ->Get(0)
+                              ->id());
+      const Eigen::Matrix<double, 4, 4> H_field_target =
+          PoseToTransform(constants_fetcher_.constants()
+                              .common()
+                              ->target_map()
+                              ->target_poses()
+                              ->Get(0));
+      // For reference, the camera should pointed straight forwards on the
+      // robot, offset by 1 meter.
+      aos::TimerHandler *timer = camera_test_event_loop_->AddTimer(
+          [this, H_robot_camera, H_field_target]() {
+            if (!send_targets_) {
+              return;
+            }
+            const frc971::control_loops::Pose robot_pose(
+                {drivetrain_plant_.GetPosition().x(),
+                 drivetrain_plant_.GetPosition().y(), 0.0},
+                drivetrain_plant_.state()(2, 0) + implied_yaw_error_);
+
+            const Eigen::Matrix<double, 4, 4> H_field_camera =
+                robot_pose.AsTransformationMatrix() * H_robot_camera;
+            const Eigen::Matrix<double, 4, 4> H_camera_target =
+                H_field_camera.inverse() * H_field_target;
+
+            Eigen::Quaterniond quat(H_camera_target.block<3, 3>(0, 0));
+            quat.normalize();
+            const Eigen::Vector3d translation(
+                H_camera_target.block<3, 1>(0, 3));
+
+            auto builder = target_sender_.MakeBuilder();
+            frc971::vision::Quaternion::Builder quat_builder(*builder.fbb());
+            quat_builder.add_w(quat.w());
+            quat_builder.add_x(quat.x());
+            quat_builder.add_y(quat.y());
+            quat_builder.add_z(quat.z());
+            auto quat_offset = quat_builder.Finish();
+            frc971::vision::Position::Builder position_builder(*builder.fbb());
+            position_builder.add_x(translation.x());
+            position_builder.add_y(translation.y());
+            position_builder.add_z(translation.z());
+            auto position_offset = position_builder.Finish();
+
+            frc971::vision::TargetPoseFbs::Builder target_builder(
+                *builder.fbb());
+            target_builder.add_id(send_target_id_);
+            target_builder.add_position(position_offset);
+            target_builder.add_orientation(quat_offset);
+            target_builder.add_pose_error(pose_error_);
+            target_builder.add_pose_error_ratio(pose_error_ratio_);
+            auto target_offset = target_builder.Finish();
+
+            auto targets_offset = builder.fbb()->CreateVector({target_offset});
+            frc971::vision::TargetMap::Builder map_builder(*builder.fbb());
+            map_builder.add_target_poses(targets_offset);
+            map_builder.add_monotonic_timestamp_ns(
+                std::chrono::duration_cast<std::chrono::nanoseconds>(
+                    camera_test_event_loop_->monotonic_now().time_since_epoch())
+                    .count());
+
+            builder.CheckOk(builder.Send(map_builder.Finish()));
+          });
+      camera_test_event_loop_->OnRun([timer, this]() {
+        timer->Schedule(camera_test_event_loop_->monotonic_now(),
+                        std::chrono::milliseconds(50));
+      });
+    }
+
+    localizer_control_send_timer_ =
+        roborio_test_event_loop_->AddTimer([this]() {
+          auto builder = control_sender_.MakeBuilder();
+          auto control_builder = builder.MakeBuilder<
+              frc971::control_loops::drivetrain::LocalizerControl>();
+          control_builder.add_x(localizer_control_x_);
+          control_builder.add_y(localizer_control_y_);
+          control_builder.add_theta(localizer_control_theta_);
+          control_builder.add_theta_uncertainty(0.01);
+          control_builder.add_keep_current_theta(false);
+          builder.CheckOk(builder.Send(control_builder.Finish()));
+        });
+
+    // Get things zeroed.
+    event_loop_factory_.RunFor(std::chrono::seconds(10));
+    CHECK(status_fetcher_.Fetch());
+    CHECK(status_fetcher_->imu()->zeroed());
+
+    if (!FLAGS_output_folder.empty()) {
+      logger_event_loop_ =
+          event_loop_factory_.MakeEventLoop("logger", imu_node_);
+      logger_ = std::make_unique<aos::logger::Logger>(logger_event_loop_.get());
+      logger_->StartLoggingOnRun(FLAGS_output_folder);
+    }
+  }
+
+  void SendLocalizerControl(double x, double y, double theta) {
+    localizer_control_x_ = x;
+    localizer_control_y_ = y;
+    localizer_control_theta_ = theta;
+    localizer_control_send_timer_->Schedule(
+        roborio_test_event_loop_->monotonic_now());
+  }
+
+  ::testing::AssertionResult IsNear(double expected, double actual,
+                                    double epsilon) {
+    if (std::abs(expected - actual) < epsilon) {
+      return ::testing::AssertionSuccess();
+    } else {
+      return ::testing::AssertionFailure()
+             << "Expected " << expected << " but got " << actual
+             << " with a max difference of " << epsilon
+             << " and an actual difference of " << std::abs(expected - actual);
+    }
+  }
+  ::testing::AssertionResult VerifyEstimatorAccurate(double eps) {
+    const Eigen::Matrix<double, 5, 1> true_state = drivetrain_plant_.state();
+    ::testing::AssertionResult result(true);
+    status_fetcher_.Fetch();
+    if (!(result = IsNear(status_fetcher_->state()->x(), true_state(0), eps))) {
+      return result;
+    }
+    if (!(result = IsNear(status_fetcher_->state()->y(), true_state(1), eps))) {
+      return result;
+    }
+    if (!(result =
+              IsNear(status_fetcher_->state()->theta(), true_state(2), eps))) {
+      return result;
+    }
+    return result;
+  }
+
+  aos::FlatbufferDetachedBuffer<aos::Configuration> configuration_;
+  aos::SimulatedEventLoopFactory event_loop_factory_;
+  const aos::Node *const roborio_node_;
+  const aos::Node *const imu_node_;
+  const aos::Node *const camera_node_;
+  std::unique_ptr<aos::EventLoop> roborio_test_event_loop_;
+  const frc971::control_loops::drivetrain::DrivetrainConfig<double> dt_config_;
+  std::unique_ptr<aos::EventLoop> localizer_event_loop_;
+  Localizer localizer_;
+
+  std::unique_ptr<aos::EventLoop> drivetrain_plant_event_loop_;
+  std::unique_ptr<aos::EventLoop> drivetrain_plant_imu_event_loop_;
+  frc971::control_loops::drivetrain::testing::DrivetrainSimulation
+      drivetrain_plant_;
+
+  std::unique_ptr<aos::EventLoop> imu_test_event_loop_;
+  std::unique_ptr<aos::EventLoop> camera_test_event_loop_;
+
+  frc971::constants::ConstantsFetcher<Constants> constants_fetcher_;
+
+  aos::Sender<Output> output_sender_;
+  aos::Sender<frc971::control_loops::drivetrain::RioLocalizerInputsStatic>
+      combined_sender_;
+  aos::Sender<frc971::vision::TargetMap> target_sender_;
+  aos::Sender<frc971::control_loops::drivetrain::LocalizerControl>
+      control_sender_;
+  aos::Fetcher<frc971::controls::LocalizerOutput> output_fetcher_;
+  aos::Fetcher<Status> status_fetcher_;
+
+  Eigen::Vector2d output_voltages_ = Eigen::Vector2d::Zero();
+
+  aos::TimerHandler *localizer_control_send_timer_;
+
+  bool send_targets_ = false;
+
+  double localizer_control_x_ = 0.0;
+  double localizer_control_y_ = 0.0;
+  double localizer_control_theta_ = 0.0;
+
+  std::unique_ptr<aos::EventLoop> logger_event_loop_;
+  std::unique_ptr<aos::logger::Logger> logger_;
+
+  uint64_t send_target_id_ = kTargetId;
+  double pose_error_ = 1e-7;
+  double pose_error_ratio_ = 0.1;
+  double implied_yaw_error_ = 0.0;
+
+  gflags::FlagSaver flag_saver_;
+};
+
+// Test a simple scenario with no errors where the robot should just drive
+// straight forwards.
+TEST_F(LocalizerTest, Nominal) {
+  output_voltages_ << 1.0, 1.0;
+  event_loop_factory_.RunFor(std::chrono::seconds(2));
+  CHECK(output_fetcher_.Fetch());
+  CHECK(status_fetcher_.Fetch());
+  // The two can be different because they may've been sent at different
+  // times.
+  EXPECT_NEAR(output_fetcher_->x(), status_fetcher_->state()->x(), 1e-2);
+  EXPECT_NEAR(output_fetcher_->y(), status_fetcher_->state()->y(), 1e-6);
+  EXPECT_NEAR(output_fetcher_->theta(), status_fetcher_->state()->theta(),
+              1e-6);
+  // Confirm that we did indeed drive forwards (and straight), as expected.
+  EXPECT_LT(0.1, output_fetcher_->x());
+  EXPECT_NEAR(0.0, output_fetcher_->y(), 1e-10);
+  EXPECT_NEAR(0.0, output_fetcher_->theta(), 1e-10);
+  EXPECT_NEAR(0.0, status_fetcher_->state()->left_voltage_error(), 1e-1);
+  EXPECT_NEAR(0.0, status_fetcher_->state()->right_voltage_error(), 1e-1);
+
+  // And check that we actually think that we are near where the simulator
+  // says we are.
+  EXPECT_TRUE(VerifyEstimatorAccurate(1e-2));
+}
+
+// Confirm that when the robot drives backwards that we localize correctly.
+TEST_F(LocalizerTest, NominalReverse) {
+  output_voltages_ << -1.0, -1.0;
+  event_loop_factory_.RunFor(std::chrono::seconds(2));
+  CHECK(output_fetcher_.Fetch());
+  CHECK(status_fetcher_.Fetch());
+  // The two can be different because they may've been sent at different
+  // times.
+  EXPECT_NEAR(output_fetcher_->x(), status_fetcher_->state()->x(), 1e-2);
+  EXPECT_NEAR(output_fetcher_->y(), status_fetcher_->state()->y(), 1e-6);
+  EXPECT_NEAR(output_fetcher_->theta(), status_fetcher_->state()->theta(),
+              1e-6);
+  // Confirm that we did indeed drive backwards (and straight), as expected.
+  EXPECT_GT(-0.1, output_fetcher_->x());
+  EXPECT_NEAR(0.0, output_fetcher_->y(), 1e-10);
+  EXPECT_NEAR(0.0, output_fetcher_->theta(), 1e-10);
+  EXPECT_NEAR(0.0, status_fetcher_->state()->left_voltage_error(), 1e-1);
+  EXPECT_NEAR(0.0, status_fetcher_->state()->right_voltage_error(), 1e-1);
+
+  // And check that we actually think that we are near where the simulator
+  // says we are.
+  EXPECT_TRUE(VerifyEstimatorAccurate(1e-2));
+}
+
+// Confirm that when the robot turns counter-clockwise that we localize
+// correctly.
+TEST_F(LocalizerTest, NominalSpinInPlace) {
+  output_voltages_ << -1.0, 1.0;
+  // Go 1 ms over 2 sec to make sure we actually see relatively recent messages
+  // on each channel.
+  event_loop_factory_.RunFor(std::chrono::milliseconds(2001));
+  CHECK(output_fetcher_.Fetch());
+  CHECK(status_fetcher_.Fetch());
+  // The two can be different because they may've been sent at different
+  // times.
+  EXPECT_NEAR(output_fetcher_->x(), status_fetcher_->state()->x(), 1e-6);
+  EXPECT_NEAR(output_fetcher_->y(), status_fetcher_->state()->y(), 1e-6);
+  EXPECT_NEAR(output_fetcher_->theta(), status_fetcher_->state()->theta(),
+              1e-2);
+  // Confirm that we did indeed turn counter-clockwise.
+  EXPECT_NEAR(0.0, output_fetcher_->x(), 1e-10);
+  EXPECT_NEAR(0.0, output_fetcher_->y(), 1e-10);
+  EXPECT_LT(0.1, output_fetcher_->theta());
+  EXPECT_NEAR(0.0, status_fetcher_->state()->left_voltage_error(), 1e-1);
+  EXPECT_NEAR(0.0, status_fetcher_->state()->right_voltage_error(), 1e-1);
+
+  // And check that we actually think that we are near where the simulator
+  // says we are.
+  EXPECT_TRUE(VerifyEstimatorAccurate(1e-2));
+}
+
+// Confirm that when the robot drives in a curve that we localize
+// successfully.
+TEST_F(LocalizerTest, NominalCurve) {
+  output_voltages_ << 2.0, 3.0;
+  event_loop_factory_.RunFor(std::chrono::seconds(4));
+  CHECK(output_fetcher_.Fetch());
+  CHECK(status_fetcher_.Fetch());
+  // The two can be different because they may've been sent at different
+  // times.
+  EXPECT_NEAR(output_fetcher_->x(), status_fetcher_->state()->x(), 2e-2);
+  EXPECT_NEAR(output_fetcher_->y(), status_fetcher_->state()->y(), 2e-2);
+  EXPECT_NEAR(output_fetcher_->theta(), status_fetcher_->state()->theta(),
+              2e-2);
+  // Confirm that we did indeed drive in a rough, counter-clockwise, curve.
+  EXPECT_LT(0.1, output_fetcher_->x());
+  EXPECT_LT(0.1, output_fetcher_->y());
+  EXPECT_LT(0.1, output_fetcher_->theta());
+
+  // And check that we actually think that we are near where the simulator
+  // says we are.
+  EXPECT_TRUE(VerifyEstimatorAccurate(1e-2));
+}
+
+// Tests that, in the presence of a non-zero voltage error, that we correct
+// for it.
+TEST_F(LocalizerTest, VoltageErrorDisabled) {
+  output_voltages_ << 0.0, 0.0;
+  drivetrain_plant_.set_left_voltage_offset(2.0);
+  drivetrain_plant_.set_right_voltage_offset(2.0);
+
+  event_loop_factory_.RunFor(std::chrono::seconds(2));
+  CHECK(output_fetcher_.Fetch());
+  CHECK(status_fetcher_.Fetch());
+  // We should've just ended up driving straight forwards.
+  EXPECT_LT(0.1, output_fetcher_->x());
+  EXPECT_NEAR(0.0, output_fetcher_->y(), 1e-10);
+  EXPECT_NEAR(0.0, output_fetcher_->theta(), 1e-10);
+  EXPECT_NEAR(2.0, status_fetcher_->state()->left_voltage_error(), 1.0);
+  EXPECT_NEAR(2.0, status_fetcher_->state()->right_voltage_error(), 1.0);
+
+  // And check that we actually think that we are near where the simulator
+  // says we are.
+  EXPECT_TRUE(VerifyEstimatorAccurate(0.05));
+}
+
+// Tests that image corrections in the nominal case (no errors) causes no
+// issues.
+TEST_F(LocalizerTest, NominalImageCorrections) {
+  output_voltages_ << 3.0, 2.0;
+  send_targets_ = true;
+
+  event_loop_factory_.RunFor(std::chrono::seconds(4));
+  CHECK(status_fetcher_.Fetch());
+  EXPECT_TRUE(VerifyEstimatorAccurate(1e-2));
+  ASSERT_TRUE(status_fetcher_->has_statistics());
+  ASSERT_EQ(4u /* number of cameras */, status_fetcher_->statistics()->size());
+  ASSERT_EQ(status_fetcher_->statistics()->Get(0)->total_candidates(),
+            status_fetcher_->statistics()->Get(0)->total_accepted());
+  ASSERT_LT(10, status_fetcher_->statistics()->Get(0)->total_candidates());
+}
+
+// Tests that image corrections when there is an error at the start results
+// in us actually getting corrected over time.
+TEST_F(LocalizerTest, ImageCorrections) {
+  output_voltages_ << 0.0, 0.0;
+  // Put ourselves somewhat near the target so that we don't ignore its
+  // corrections too much.
+  drivetrain_plant_.mutable_state()->x() = 3.0;
+  drivetrain_plant_.mutable_state()->y() = -2.0;
+  SendLocalizerControl(5.0, 0.0, 0.0);
+  event_loop_factory_.RunFor(std::chrono::seconds(4));
+  ASSERT_TRUE(output_fetcher_.Fetch());
+  EXPECT_NEAR(5.0, output_fetcher_->x(), 1e-5);
+  EXPECT_NEAR(0.0, output_fetcher_->y(), 1e-5);
+  EXPECT_NEAR(0.0, output_fetcher_->theta(), 1e-5);
+
+  send_targets_ = true;
+
+  event_loop_factory_.RunFor(std::chrono::seconds(10));
+  CHECK(status_fetcher_.Fetch());
+  EXPECT_TRUE(VerifyEstimatorAccurate(0.1));
+  ASSERT_TRUE(status_fetcher_->has_statistics());
+  ASSERT_EQ(4u /* number of cameras */, status_fetcher_->statistics()->size());
+  ASSERT_EQ(status_fetcher_->statistics()->Get(0)->total_candidates(),
+            status_fetcher_->statistics()->Get(0)->total_accepted());
+  ASSERT_LT(10, status_fetcher_->statistics()->Get(0)->total_candidates());
+}
+
+// Tests that we correctly reject an invalid target.
+TEST_F(LocalizerTest, InvalidTargetId) {
+  output_voltages_ << 0.0, 0.0;
+  send_targets_ = true;
+  send_target_id_ = 100;
+
+  event_loop_factory_.RunFor(std::chrono::seconds(4));
+  CHECK(status_fetcher_.Fetch());
+  ASSERT_TRUE(status_fetcher_->has_statistics());
+  ASSERT_EQ(4u /* number of cameras */, status_fetcher_->statistics()->size());
+  ASSERT_EQ(0, status_fetcher_->statistics()->Get(0)->total_accepted());
+  ASSERT_LT(10, status_fetcher_->statistics()->Get(0)->total_candidates());
+  ASSERT_EQ(status_fetcher_->statistics()
+                ->Get(0)
+                ->rejection_reasons()
+                ->Get(static_cast<size_t>(RejectionReason::NO_SUCH_TARGET))
+                ->count(),
+            status_fetcher_->statistics()->Get(0)->total_candidates());
+}
+
+// Tests that we correctly reject a detection with a high pose error.
+TEST_F(LocalizerTest, HighPoseError) {
+  output_voltages_ << 0.0, 0.0;
+  send_targets_ = true;
+  // Send the minimum pose error to be rejected
+  constexpr double kEps = 1e-9;
+  pose_error_ = 1e-6 + kEps;
+
+  event_loop_factory_.RunFor(std::chrono::seconds(4));
+  CHECK(status_fetcher_.Fetch());
+  ASSERT_TRUE(status_fetcher_->has_statistics());
+  ASSERT_EQ(4u /* number of cameras */, status_fetcher_->statistics()->size());
+  ASSERT_EQ(0, status_fetcher_->statistics()->Get(0)->total_accepted());
+  ASSERT_LT(10, status_fetcher_->statistics()->Get(0)->total_candidates());
+  ASSERT_EQ(status_fetcher_->statistics()
+                ->Get(0)
+                ->rejection_reasons()
+                ->Get(static_cast<size_t>(RejectionReason::HIGH_POSE_ERROR))
+                ->count(),
+            status_fetcher_->statistics()->Get(0)->total_candidates());
+}
+
+// Tests that we correctly reject a detection with a high implied yaw error.
+TEST_F(LocalizerTest, HighImpliedYawError) {
+  output_voltages_ << 0.0, 0.0;
+  send_targets_ = true;
+  implied_yaw_error_ = 31.0 * M_PI / 180.0;
+
+  event_loop_factory_.RunFor(std::chrono::seconds(4));
+  CHECK(status_fetcher_.Fetch());
+  ASSERT_TRUE(status_fetcher_->has_statistics());
+  ASSERT_EQ(4u /* number of cameras */, status_fetcher_->statistics()->size());
+  ASSERT_EQ(0, status_fetcher_->statistics()->Get(0)->total_accepted());
+  ASSERT_LT(10, status_fetcher_->statistics()->Get(0)->total_candidates());
+  ASSERT_EQ(
+      status_fetcher_->statistics()
+          ->Get(0)
+          ->rejection_reasons()
+          ->Get(static_cast<size_t>(RejectionReason::HIGH_IMPLIED_YAW_ERROR))
+          ->count(),
+      status_fetcher_->statistics()->Get(0)->total_candidates());
+}
+
+// Tests that we correctly reject a detection with a high pose error ratio.
+TEST_F(LocalizerTest, HighPoseErrorRatio) {
+  output_voltages_ << 0.0, 0.0;
+  send_targets_ = true;
+  // Send the minimum pose error to be rejected
+  constexpr double kEps = 1e-9;
+  pose_error_ratio_ = 0.4 + kEps;
+
+  event_loop_factory_.RunFor(std::chrono::seconds(4));
+  CHECK(status_fetcher_.Fetch());
+  ASSERT_TRUE(status_fetcher_->has_statistics());
+  ASSERT_EQ(4u /* number of cameras */, status_fetcher_->statistics()->size());
+  ASSERT_EQ(0, status_fetcher_->statistics()->Get(0)->total_accepted());
+  ASSERT_LT(10, status_fetcher_->statistics()->Get(0)->total_candidates());
+  ASSERT_EQ(
+      status_fetcher_->statistics()
+          ->Get(0)
+          ->rejection_reasons()
+          ->Get(static_cast<size_t>(RejectionReason::HIGH_POSE_ERROR_RATIO))
+          ->count(),
+      status_fetcher_->statistics()->Get(0)->total_candidates());
+}
+
+}  // namespace y2024::localizer::testing
diff --git a/y2024/localizer/status.fbs b/y2024/localizer/status.fbs
new file mode 100644
index 0000000..ad2ac72
--- /dev/null
+++ b/y2024/localizer/status.fbs
@@ -0,0 +1,73 @@
+include "frc971/control_loops/drivetrain/drivetrain_status.fbs";
+include "frc971/imu_reader/imu_failures.fbs";
+
+namespace y2024.localizer;
+
+attribute "static_length";
+
+enum RejectionReason : uint8 {
+  // For some reason, the image timestamp indicates that the image was taken
+  // in the future.
+  IMAGE_FROM_FUTURE = 0,
+  // The image was too old for the buffer of old state estimates that we
+  // maintain.
+  IMAGE_TOO_OLD = 1,
+  // Message bridge is not yet connected, and so we can't get accurate
+  // time offsets betwee nnodes.
+  MESSAGE_BRIDGE_DISCONNECTED = 2,
+  // The target ID does not exist.
+  NO_SUCH_TARGET = 3,
+  // Pose estimation error was higher than any normal detection.
+  HIGH_POSE_ERROR = 4,
+  // Pose estimate implied a robot yaw far off from our estimate.
+  HIGH_IMPLIED_YAW_ERROR = 5,
+  // Pose estimate had a high distance to target.
+  // We don't trust estimates very far out.
+  HIGH_DISTANCE_TO_TARGET = 6,
+  // The robot was travelling too fast; we don't trust the target.
+  ROBOT_TOO_FAST = 7,
+  // Pose estimation error ratio was higher than any normal detection.
+  HIGH_POSE_ERROR_RATIO = 8,
+}
+
+table RejectionCount {
+  error:RejectionReason (id: 0);
+  count:uint (id: 1);
+}
+
+table CumulativeStatistics {
+  total_accepted:int (id: 0);
+  total_candidates:int (id: 1);
+  rejection_reasons:[RejectionCount] (id: 2, static_length: 9);
+}
+
+table ImuStatus {
+  // Whether the IMU is zeroed or not.
+  zeroed:bool (id: 0);
+  // Whether the IMU zeroing is faulted or not.
+  faulted_zero:bool (id: 1);
+  zeroing:frc971.control_loops.drivetrain.ImuZeroerState (id: 2);
+  // Offset between the IMU board clock and the orin clock, such that
+  // board_timestamp + board_offset_ns = orin_timestamp
+  board_offset_ns:int64 (id: 3);
+  // Error in the offset, if we assume that the orin/board clocks are
+  // identical and that there is a perfectly consistent latency between the
+  // two. Will be zero for the very first cycle, and then referenced off of
+  // the initial offset thereafter. If greater than zero, implies that the
+  // board is "behind", whether due to unusually large latency or due to
+  // clock drift.
+  board_offset_error_ns:int64 (id: 4);
+  left_encoder:double (id: 5);
+  right_encoder:double (id: 6);
+  imu_failures:frc971.controls.ImuFailures (id: 7);
+}
+
+table Status {
+  state: frc971.control_loops.drivetrain.LocalizerState (id: 0);
+  down_estimator:frc971.control_loops.drivetrain.DownEstimatorState (id: 1);
+  imu:ImuStatus (id: 2);
+  // Statistics are per-camera, by camera index.
+  statistics:[CumulativeStatistics] (id: 3);
+}
+
+root_type Status;
diff --git a/y2024/localizer/visualization.fbs b/y2024/localizer/visualization.fbs
new file mode 100644
index 0000000..d903e3a
--- /dev/null
+++ b/y2024/localizer/visualization.fbs
@@ -0,0 +1,47 @@
+include "y2024/localizer/status.fbs";
+
+namespace y2024.localizer;
+
+attribute "static_length";
+
+table Measurement {
+  heading:double (id: 0);
+  distance:double (id: 1);
+  skew:double (id: 2);
+}
+
+table TargetEstimateDebug {
+  camera:uint8 (id: 0);
+  camera_x:double (id: 1);
+  camera_y:double (id: 2);
+  camera_theta:double (id: 3);
+  implied_robot_x:double (id: 4);
+  implied_robot_y:double (id: 5);
+  implied_robot_theta:double (id: 6);
+  accepted:bool (id: 7);
+  rejection_reason:RejectionReason  (id: 8);
+  // Image age (more human-readable than trying to interpret raw nanosecond
+  // values).
+  image_age_sec:double (id: 9);
+  // Time at which the image was captured.
+  image_monotonic_timestamp_ns:uint64 (id: 10);
+  // April tag ID used for this image detection.
+  april_tag:uint (id: 11);
+  // If the image was accepted, the total correction that occurred as a result.
+  // These numbers will be equal to the value after the correction - the value
+  // before.
+  correction_x: double (id: 12);
+  correction_y: double (id: 13);
+  correction_theta: double (id: 14);
+  // The expected observation given the current estimate of the robot pose.
+  expected_observation:Measurement (id: 15);
+  actual_observation:Measurement (id: 16);
+  modeled_noise:Measurement (id: 17);
+}
+
+table Visualization {
+  targets:[TargetEstimateDebug] (id: 0, static_length: 20);
+  statistics:CumulativeStatistics (id: 1);
+}
+
+root_type Visualization;
diff --git a/y2024/vision/BUILD b/y2024/vision/BUILD
index 4b515cf..d36c956 100644
--- a/y2024/vision/BUILD
+++ b/y2024/vision/BUILD
@@ -16,6 +16,38 @@
 )
 
 cc_binary(
+    name = "target_mapping",
+    srcs = [
+        "target_mapping.cc",
+        "vision_util.cc",
+        "vision_util.h",
+    ],
+    data = [
+        "//y2024:aos_config",
+        "//y2024/constants:constants.json",
+        "//y2024/vision:maps",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//y2023:__subpackages__"],
+    deps = [
+        "//aos:init",
+        "//aos/events:simulated_event_loop",
+        "//aos/events/logging:log_reader",
+        "//aos/util:mcap_logger",
+        "//frc971/constants:constants_sender_lib",
+        "//frc971/control_loops:pose",
+        "//frc971/vision:calibration_fbs",
+        "//frc971/vision:charuco_lib",
+        "//frc971/vision:target_mapper",
+        "//frc971/vision:vision_util_lib",
+        "//frc971/vision:visualize_robot",
+        "//third_party:opencv",
+        "//y2024/constants:constants_fbs",
+        "//y2024/constants:simulated_constants_sender",
+    ],
+)
+
+cc_binary(
     name = "image_logger",
     srcs = [
         "image_logger.cc",
@@ -115,3 +147,22 @@
         "@org_tuxfamily_eigen//:eigen",
     ],
 )
+
+cc_binary(
+    name = "modify_extrinsics",
+    srcs = [
+        "modify_extrinsics.cc",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//y2024:__subpackages__"],
+    deps = [
+        "//aos:configuration",
+        "//aos:init",
+        "//aos/events:event_loop",
+        "//frc971/vision:calibration_fbs",
+        "//frc971/vision:vision_util_lib",
+        "//y2024/constants:constants_fbs",
+        "@com_google_absl//absl/strings:str_format",
+        "@org_tuxfamily_eigen//:eigen",
+    ],
+)
diff --git a/y2024/vision/apriltag_detector.cc b/y2024/vision/apriltag_detector.cc
index 96ff869..5edcf36 100644
--- a/y2024/vision/apriltag_detector.cc
+++ b/y2024/vision/apriltag_detector.cc
@@ -20,10 +20,12 @@
   const frc971::constants::ConstantsFetcher<y2024::Constants> calibration_data(
       &event_loop);
 
+  CHECK(FLAGS_channel.length() == 8);
+  int camera_id = std::stoi(FLAGS_channel.substr(7, 1));
   const frc971::vision::calibration::CameraCalibration *calibration =
       y2024::vision::FindCameraCalibration(
           calibration_data.constants(),
-          event_loop.node()->name()->string_view());
+          event_loop.node()->name()->string_view(), camera_id);
 
   frc971::apriltag::ApriltagDetector detector(&event_loop, FLAGS_channel,
                                               calibration);
diff --git a/y2024/vision/calibrate_multi_cameras.cc b/y2024/vision/calibrate_multi_cameras.cc
index 9148217..8a4bbaa 100644
--- a/y2024/vision/calibrate_multi_cameras.cc
+++ b/y2024/vision/calibrate_multi_cameras.cc
@@ -31,7 +31,7 @@
             "If true, show visualization from field level, rather than above");
 DEFINE_string(config, "",
               "If set, override the log's config file with this one.");
-DEFINE_string(constants_path, "y2023/constants/constants.json",
+DEFINE_string(constants_path, "y2024/constants/constants.json",
               "Path to the constant file");
 DEFINE_double(max_pose_error, 5e-5,
               "Throw out target poses with a higher pose error than this");
@@ -44,12 +44,14 @@
               "Type of target being used [aruco, charuco, charuco_diamond]");
 DEFINE_int32(team_number, 0,
              "Required: Use the calibration for a node with this team number");
-DEFINE_bool(use_full_logs, false,
-            "If true, extract data from logs with images");
 DEFINE_uint64(
     wait_key, 1,
     "Time in ms to wait between images (0 to wait indefinitely until click)");
 
+DEFINE_bool(robot, false,
+            "If true we're calibrating extrinsics for the robot, use the "
+            "correct node path for the robot.");
+
 DECLARE_int32(min_target_id);
 DECLARE_int32(max_target_id);
 
@@ -64,11 +66,11 @@
 // and then map each subsequent camera based on the data collected and
 // the extrinsic poses computed here.
 
-// TODO<Jim>: Not currently using estimate from pi1->pi4-- should do full
-// estimation, and probably also include camera->imu extrinsics from all
-// cameras, not just pi1
+// TODO<Jim>: Not currently using estimate from first camera to last camera--
+// should do full estimation, and probably also include camera->imu extrinsics
+// from all cameras, not just first camera
 
-namespace y2023::vision {
+namespace y2024::vision {
 using frc971::vision::DataAdapter;
 using frc971::vision::ImageCallback;
 using frc971::vision::PoseUtils;
@@ -78,7 +80,7 @@
 namespace calibration = frc971::vision::calibration;
 
 static constexpr double kImagePeriodMs =
-    1.0 / 30.0 * 1000.0;  // Image capture period in ms
+    1.0 / 60.0 * 1000.0;  // Image capture period in ms
 
 // Change reference frame from camera to robot
 Eigen::Affine3d CameraToRobotDetection(Eigen::Affine3d H_camera_target,
@@ -97,6 +99,38 @@
   int board_id;
 };
 
+struct CameraNode {
+  std::string node_name;
+  int camera_number;
+
+  inline const std::string camera_name() const {
+    return "/" + node_name + "/camera" + std::to_string(camera_number);
+  }
+};
+
+std::vector<CameraNode> CreateNodeList() {
+  std::vector<CameraNode> list;
+
+  list.push_back({.node_name = "orin1", .camera_number = 0});
+  list.push_back({.node_name = "orin1", .camera_number = 1});
+  list.push_back({.node_name = "imu", .camera_number = 0});
+  list.push_back({.node_name = "imu", .camera_number = 1});
+
+  return list;
+}
+
+std::vector<CameraNode> node_list(CreateNodeList());
+std::map<std::string, int> CreateOrderingMap() {
+  std::map<std::string, int> map;
+
+  for (uint i = 0; i < node_list.size(); i++) {
+    map.insert({node_list.at(i).camera_name(), i});
+  }
+
+  return map;
+}
+std::map<std::string, int> ordering_map(CreateOrderingMap());
+
 TimestampedPiDetection last_observation;
 std::vector<std::pair<TimestampedPiDetection, TimestampedPiDetection>>
     detection_list;
@@ -252,14 +286,14 @@
         kImagePeriodMs / 2.0 * 1000000.0) {
       // Sort by pi numbering, since this is how we will handle them
       std::pair<TimestampedPiDetection, TimestampedPiDetection> new_pair;
-      if (last_observation.pi_name < new_observation.pi_name) {
+      if (ordering_map.at(last_observation.pi_name) <
+          ordering_map.at(new_observation.pi_name)) {
         new_pair = std::pair(last_observation, new_observation);
-      } else if (last_observation.pi_name > new_observation.pi_name) {
+      } else if (ordering_map.at(last_observation.pi_name) >
+                 ordering_map.at(new_observation.pi_name)) {
         new_pair = std::pair(new_observation, last_observation);
-      } else {
-        LOG(WARNING) << "Got 2 observations in a row from same pi. Probably "
-                        "not too much of an issue???";
       }
+
       detection_list.push_back(new_pair);
 
       // This bit is just for visualization and checking purposes-- use the
@@ -366,7 +400,7 @@
 
     Eigen::Affine3d H_camera_target =
         PoseUtils::Pose3dToAffine3d(target_pose.pose);
-    VLOG(2) << node_name << " saw target " << target_pose.id
+    VLOG(1) << node_name << " saw target " << target_pose.id
             << " from TargetMap at timestamp " << distributed_eof
             << " with pose = " << H_camera_target.matrix();
   }
@@ -425,26 +459,18 @@
       aos::logger::SortParts(aos::logger::FindLogs(argc, argv)),
       config.has_value() ? &config->message() : nullptr);
 
-  constexpr size_t kNumPis = 4;
-  for (size_t i = 1; i <= kNumPis; i++) {
-    reader.RemapLoggedChannel(absl::StrFormat("/pi%u/constants", i),
-                              "y2023.Constants");
+  reader.RemapLoggedChannel("/imu/constants", "y2024.Constants");
+  reader.RemapLoggedChannel("/orin1/constants", "y2024.Constants");
+  if (FLAGS_robot) {
+    reader.RemapLoggedChannel("/roborio/constants", "y2024.Constants");
   }
-
-  reader.RemapLoggedChannel("/imu/constants", "y2023.Constants");
-  reader.RemapLoggedChannel("/logger/constants", "y2023.Constants");
-  reader.RemapLoggedChannel("/roborio/constants", "y2023.Constants");
   reader.Register();
 
   y2024::SendSimulationConstants(reader.event_loop_factory(), FLAGS_team_number,
                                  FLAGS_constants_path);
 
   VLOG(1) << "Using target type " << FLAGS_target_type;
-  std::vector<std::string> node_list;
-  node_list.push_back("pi1");
-  node_list.push_back("pi2");
-  node_list.push_back("pi3");
-  node_list.push_back("pi4");
+
   std::vector<const calibration::CameraCalibration *> calibration_list;
 
   std::vector<std::unique_ptr<aos::EventLoop>> detection_event_loops;
@@ -452,29 +478,23 @@
   std::vector<frc971::vision::ImageCallback *> image_callbacks;
   std::vector<Eigen::Affine3d> default_extrinsics;
 
-  for (uint i = 0; i < node_list.size(); i++) {
-    std::string node = node_list[i];
-    const aos::Node *pi =
-        aos::configuration::GetNode(reader.configuration(), node.c_str());
+  for (const CameraNode &camera_node : node_list) {
+    const aos::Node *pi = aos::configuration::GetNode(
+        reader.configuration(), camera_node.node_name.c_str());
 
     detection_event_loops.emplace_back(
         reader.event_loop_factory()->MakeEventLoop(
-            (node + "_detection").c_str(), pi));
-
+            (camera_node.camera_name() + "_detection").c_str(), pi));
     frc971::constants::ConstantsFetcher<y2024::Constants> constants_fetcher(
         detection_event_loops.back().get());
-
+    // Get the calibration for this orin/camera pair
     const calibration::CameraCalibration *calibration =
         y2024::vision::FindCameraCalibration(constants_fetcher.constants(),
-                                             node);
+                                             camera_node.node_name,
+                                             camera_node.camera_number);
     calibration_list.push_back(calibration);
 
-    frc971::vision::TargetType target_type =
-        frc971::vision::TargetTypeFromString(FLAGS_target_type);
-    frc971::vision::CharucoExtractor *charuco_ext =
-        new frc971::vision::CharucoExtractor(calibration, target_type);
-    charuco_extractors.emplace_back(charuco_ext);
-
+    // Extract the extrinsics from the calibration, and save as "defaults"
     cv::Mat extrinsics_cv =
         frc971::vision::CameraExtrinsics(calibration).value();
     Eigen::Matrix4d extrinsics_matrix;
@@ -482,45 +502,25 @@
     const auto ext_H_robot_pi = Eigen::Affine3d(extrinsics_matrix);
     default_extrinsics.emplace_back(ext_H_robot_pi);
 
-    VLOG(1) << "Got extrinsics for " << node << " as\n"
+    VLOG(1) << "Got extrinsics for " << camera_node.camera_name() << " as\n"
             << default_extrinsics.back().matrix();
 
-    if (FLAGS_use_full_logs) {
-      LOG(INFO) << "Set up image callback for node " << node_list[i];
-      frc971::vision::ImageCallback *image_callback =
-          new frc971::vision::ImageCallback(
-              detection_event_loops[i].get(), "/" + node_list[i] + "/camera",
-              [&reader, &charuco_extractors, &detection_event_loops, &node_list,
-               i](cv::Mat rgb_image,
-                  const aos::monotonic_clock::time_point eof) {
-                aos::distributed_clock::time_point pi_distributed_time =
-                    reader.event_loop_factory()
-                        ->GetNodeEventLoopFactory(
-                            detection_event_loops[i].get()->node())
-                        ->ToDistributedClock(eof);
-                HandleImage(detection_event_loops[i].get(), rgb_image, eof,
-                            pi_distributed_time, *charuco_extractors[i],
-                            node_list[i]);
-              });
+    detection_event_loops.back()->MakeWatcher(
+        camera_node.camera_name(),
+        [&reader, &detection_event_loops, camera_node](const TargetMap &map) {
+          aos::distributed_clock::time_point pi_distributed_time =
+              reader.event_loop_factory()
+                  ->GetNodeEventLoopFactory(
+                      detection_event_loops.back().get()->node())
+                  ->ToDistributedClock(aos::monotonic_clock::time_point(
+                      aos::monotonic_clock::duration(
+                          map.monotonic_timestamp_ns())));
 
-      image_callbacks.emplace_back(image_callback);
-    } else {
-      detection_event_loops[i]->MakeWatcher(
-          "/camera", [&reader, &detection_event_loops, &node_list,
-                      i](const TargetMap &map) {
-            aos::distributed_clock::time_point pi_distributed_time =
-                reader.event_loop_factory()
-                    ->GetNodeEventLoopFactory(detection_event_loops[i]->node())
-                    ->ToDistributedClock(aos::monotonic_clock::time_point(
-                        aos::monotonic_clock::duration(
-                            map.monotonic_timestamp_ns())));
-
-            HandleTargetMap(map, pi_distributed_time, node_list[i]);
-          });
-      LOG(INFO) << "Created watcher for using the detection event loop for "
-                << node_list[i] << " with i = " << i << " and size "
-                << detection_event_loops.size();
-    }
+          HandleTargetMap(map, pi_distributed_time, camera_node.camera_name());
+        });
+    LOG(INFO) << "Created watcher for using the detection event loop for "
+              << camera_node.camera_name() << " and size "
+              << detection_event_loops.size();
   }
 
   reader.event_loop_factory()->Run();
@@ -531,26 +531,27 @@
       << "Must have at least one view of both boards";
   int base_target_id = two_board_extrinsics_list[0].board_id;
   VLOG(1) << "Base id for two_board_extrinsics_list is " << base_target_id;
-  for (auto node : node_list) {
+
+  for (auto camera_node : node_list) {
     std::vector<TimestampedPiDetection> pose_list;
     for (auto ext : two_board_extrinsics_list) {
       CHECK_EQ(base_target_id, ext.board_id)
           << " All boards should have same reference id";
-      if (ext.pi_name == node) {
+      if (ext.pi_name == camera_node.camera_name()) {
         pose_list.push_back(ext);
       }
     }
     Eigen::Affine3d avg_pose_from_pi = ComputeAveragePose(pose_list);
-    VLOG(1) << "Estimate from " << node << " with " << pose_list.size()
-            << " observations is:\n"
+    VLOG(1) << "Estimate from " << camera_node.camera_name() << " with "
+            << pose_list.size() << " observations is:\n"
             << avg_pose_from_pi.matrix();
   }
   Eigen::Affine3d H_boardA_boardB_avg =
       ComputeAveragePose(two_board_extrinsics_list);
   // TODO: Should probably do some outlier rejection
-  LOG(INFO) << "Estimate of two board pose using all nodes with "
-            << two_board_extrinsics_list.size() << " observations is:\n"
-            << H_boardA_boardB_avg.matrix() << "\n";
+  VLOG(1) << "Estimate of two board pose using all nodes with "
+          << two_board_extrinsics_list.size() << " observations is:\n"
+          << H_boardA_boardB_avg.matrix() << "\n";
 
   // Next, compute the relative camera poses
   LOG(INFO) << "Got " << detection_list.size() << " extrinsic observations";
@@ -558,14 +559,14 @@
   std::vector<Eigen::Affine3d> updated_extrinsics;
   // Use the first node's extrinsics as our base, and fix from there
   updated_extrinsics.push_back(default_extrinsics[0]);
-  LOG(INFO) << "Default extrinsic for node " << node_list[0] << " is "
-            << default_extrinsics[0].matrix();
+  LOG(INFO) << "Default extrinsic for camera " << node_list.at(0).camera_name()
+            << " is " << default_extrinsics[0].matrix();
   for (uint i = 0; i < node_list.size() - 1; i++) {
     H_camera1_camera2_list.clear();
     // Go through the list, and find successive pairs of cameras
     for (auto [pose1, pose2] : detection_list) {
-      if ((pose1.pi_name == node_list[i]) &&
-          (pose2.pi_name == node_list[i + 1])) {
+      if ((pose1.pi_name == node_list.at(i).camera_name()) &&
+          (pose2.pi_name == node_list.at(i + 1).camera_name())) {
         Eigen::Affine3d H_camera1_boardA = pose1.H_camera_target;
         // If pose1 isn't referenced to base_target_id, correct that
         if (pose1.board_id != base_target_id) {
@@ -613,12 +614,14 @@
     // TODO<Jim>: If we don't get any matches, we could just use default
     // extrinsics
     CHECK(H_camera1_camera2_list.size() > 0)
-        << "Failed with zero poses for node " << node_list[i];
+        << "Failed with zero poses for node " << node_list.at(i).camera_name()
+        << " and " << node_list.at(i + 1).camera_name();
     if (H_camera1_camera2_list.size() > 0) {
       Eigen::Affine3d H_camera1_camera2_avg =
           ComputeAveragePose(H_camera1_camera2_list);
-      LOG(INFO) << "From " << node_list[i] << " to " << node_list[i + 1]
-                << " found " << H_camera1_camera2_list.size()
+      LOG(INFO) << "From " << node_list.at(i).camera_name() << " to "
+                << node_list.at(i + 1).camera_name() << " found "
+                << H_camera1_camera2_list.size()
                 << " observations, and the average pose is:\n"
                 << H_camera1_camera2_avg.matrix();
       Eigen::Affine3d H_camera1_camera2_default =
@@ -640,9 +643,11 @@
       Eigen::Affine3d next_extrinsic =
           updated_extrinsics.back() * H_camera1_camera2_avg;
       updated_extrinsics.push_back(next_extrinsic);
-      LOG(INFO) << "Default Extrinsic for " << node_list[i + 1] << " is \n"
+      LOG(INFO) << "Default Extrinsic for " << node_list.at(i + 1).camera_name()
+                << " is \n"
                 << default_extrinsics[i + 1].matrix();
-      LOG(INFO) << "--> Updated Extrinsic for " << node_list[i + 1] << " is \n"
+      LOG(INFO) << "--> Updated Extrinsic for "
+                << node_list.at(i + 1).camera_name() << " is \n"
                 << next_extrinsic.matrix();
 
       // Wirte out this extrinsic to a file
@@ -677,13 +682,16 @@
       std::stringstream time_ss;
       time_ss << realtime_now;
 
-      // Assumes node_list name is of form "pi#" to create camera id
+      // TODO: This breaks because we're naming orin1 and imu as nodes
+      // Assumes camera_list name is of form "/orin#/cameraX" to create
+      // calibration filename
       const std::string calibration_filename =
           FLAGS_output_folder +
-          absl::StrFormat("/calibration_pi-%d-%s_cam-%s_%s.json",
-                          FLAGS_team_number, node_list[i + 1].substr(2, 3),
-                          calibration_list[i + 1]->camera_id()->data(),
-                          time_ss.str());
+          absl::StrFormat(
+              "/calibration_orin-%d-%s-%d_cam-%s_%s.json", FLAGS_team_number,
+              node_list.at(i + 1).camera_name().substr(5, 1),
+              calibration_list[i + 1]->camera_number(),
+              calibration_list[i + 1]->camera_id()->data(), time_ss.str());
 
       LOG(INFO) << calibration_filename << " -> "
                 << aos::FlatbufferToJson(merged_calibration,
@@ -701,9 +709,9 @@
     delete image_callbacks[i];
   }
 }
-}  // namespace y2023::vision
+}  // namespace y2024::vision
 
 int main(int argc, char **argv) {
   aos::InitGoogle(&argc, &argv);
-  y2023::vision::ExtrinsicsMain(argc, argv);
+  y2024::vision::ExtrinsicsMain(argc, argv);
 }
diff --git a/y2024/vision/image_logger.cc b/y2024/vision/image_logger.cc
index 45e25f6..55a4e12 100644
--- a/y2024/vision/image_logger.cc
+++ b/y2024/vision/image_logger.cc
@@ -25,7 +25,7 @@
 std::unique_ptr<aos::logger::MultiNodeFilesLogNamer> MakeLogNamer(
     aos::EventLoop *event_loop) {
   std::optional<std::string> log_name =
-      aos::logging::MaybeGetLogName("fbs_log");
+      aos::logging::MaybeGetLogName("image_log");
 
   if (!log_name.has_value()) {
     return nullptr;
diff --git a/y2024/vision/maps/target_map.json b/y2024/vision/maps/target_map.json
index a87e9a3..2a8dfef 100644
--- a/y2024/vision/maps/target_map.json
+++ b/y2024/vision/maps/target_map.json
@@ -1,41 +1,46 @@
 {
-/* Targets have positive Z pointing out of the board, positive X to the left and
-   positive Y is down. This means that the camera lines up with the target if the
-   board is upright and the camera is facing away from the board.*/
+/* Targets have positive Z axis pointing into the board, positive X to the right
+   when looking at the board, and positive Y is down when looking at the board.
+   This means that you will get an identity rotation from the camera to target
+   frame when the target is upright, flat, and centered in the camera's view.
+
+   The global frame as the origin at the center of the field, positive X points
+   at the red driver's station, and positive Z points straight up.
+   */
     "target_poses": [
         {
             "id": 1,
             "position": {
-                "x": -6.809,
-                "y": 3.860,
+                "x": 6.809,
+                "y": -3.860,
                 "z": 1.361
             },
             "orientation": {
-                "w": -0.18,
-                "x": -0.68,
-                "y": 0.68,
-                "z": 0.18
+                "w": 0.1830127,
+                "x": -0.1830127,
+                "y": 0.6830127,
+                "z": -0.6830127
             }
         },
         {
             "id": 2,
             "position": {
-                "x": -7.915,
-                "y": 3.223,
+                "x": 7.915,
+                "y": -3.223,
                 "z": 1.361
             },
             "orientation": {
-                "w": -0.18,
-                "x": -0.68,
-                "y": 0.68,
-                "z": 0.18
+                "w": 0.1830127,
+                "x": -0.1830127,
+                "y": 0.6830127,
+                "z": -0.6830127
             }
         },
         {
             "id": 3,
             "position": {
-                "x": -8.309,
-                "y": -0.877,
+                "x": 8.309,
+                "y": 0.877,
                 "z": 1.456
             },
             "orientation": {
@@ -48,8 +53,8 @@
         {
             "id": 4,
             "position": {
-                "x": -8.309,
-                "y": -1.442,
+                "x": 8.309,
+                "y": 1.442,
                 "z": 1.456
             },
             "orientation": {
@@ -62,120 +67,120 @@
         {
             "id": 5,
             "position": {
-                "x": -6.428,
-                "y": -4.099,
+                "x": 6.428,
+                "y": 4.099,
                 "z": 1.361
             },
             "orientation": {
-                "w": 0.707,
-                "x": 0.0,
-                "y": -0.707,
+                "w": 0.7071068,
+                "x": -0.7071068,
+                "y": 0.0,
                 "z": 0.0
             }
         },
         {
             "id": 6,
             "position": {
-                "x": 6.430,
-                "y": -4.099,
+                "x": -6.430,
+                "y": 4.099,
                 "z": 1.361
             },
             "orientation": {
-                "w": 0.707,
-                "x": 0.0,
-                "y": -0.707,
+                "w": 0.7071068,
+                "x": -0.7071068,
+                "y": 0.0,
                 "z": 0.0
             }
         },
         {
             "id": 7,
             "position": {
-                "x": 8.309,
-                "y": -1.442,
+                "x": -8.309,
+                "y": 1.442,
                 "z": 1.474
             },
             "orientation": {
                 "w": 0.5,
-                "x": 0.5,
+                "x": -0.5,
                 "y": -0.5,
-                "z": -0.5
+                "z": 0.5
             }
         },
         {
             "id": 8,
             "position": {
-                "x": 8.309,
-                "y": -0.877,
+                "x": -8.309,
+                "y": 0.877,
                 "z": 1.474
             },
             "orientation": {
                 "w": 0.5,
-                "x": 0.5,
+                "x": -0.5,
                 "y": -0.5,
-                "z": -0.5
+                "z": 0.5
             }
         },
         {
             "id": 9,
             "position": {
-                "x": 7.915,
-                "y": 3.223,
+                "x": -7.915,
+                "y": -3.223,
                 "z": 1.361
             },
             "orientation": {
-                "w": -0.18,
-                "x": -0.68,
-                "y": 0.68,
-                "z": 0.18
+                "w": 0.1830127,
+                "x": -0.1830127,
+                "y": -0.6830127,
+                "z": 0.6830127
             }
         },
         {
             "id": 10,
             "position": {
-                "x": 6.809,
-                "y": 3.860,
+                "x": -6.809,
+                "y": -3.860,
                 "z": 1.361
             },
             "orientation": {
-                "w": -0.18,
-                "x": -0.68,
-                "y": 0.68,
-                "z": 0.18
+                "w": 0.1830127,
+                "x": -0.1830127,
+                "y": -0.6830127,
+                "z": 0.6830127
             }
         },
         {
             "id": 11,
             "position": {
-                "x": -3.629,
-                "y": 0.393,
+                "x": 3.629,
+                "y": -0.393,
                 "z": 1.326
             },
             "orientation": {
-                "w": 0.68,
-                "x": 0.18,
-                "y": -0.18,
-                "z": -0.68
+                "w": 0.6830127,
+                "x": -0.6830127,
+                "y": -0.1830127,
+                "z": 0.1830127
             }
         },
         {
             "id": 12,
             "position": {
-                "x": -3.630,
-                "y": -0.392,
+                "x": 3.630,
+                "y": 0.392,
                 "z": 1.326
             },
             "orientation": {
-                "w": -0.18,
-                "x": -0.68,
-                "y": 0.68,
-                "z": 0.18
+                "w": 0.1830127,
+                "x": -0.1830127,
+                "y": -0.6830127,
+                "z": 0.6830127
             }
         },
         {
             "id": 13,
             "position": {
-                "x": -2.949,
-                "y": 0.000,
+                "x": 2.949,
+                "y": -0.000,
                 "z": 1.326
             },
             "orientation": {
@@ -188,43 +193,43 @@
         {
             "id": 14,
             "position": {
-                "x": 2.949,
-                "y": 0.000,
+                "x": -2.949,
+                "y": -0.000,
                 "z": 1.326
             },
             "orientation": {
                 "w": 0.5,
-                "x": 0.5,
+                "x": -0.5,
                 "y": -0.5,
-                "z": -0.5
+                "z": 0.5
             }
         },
         {
             "id": 15,
             "position": {
-                "x": 3.629,
-                "y": -0.393,
+                "x": -3.629,
+                "y": 0.393,
                 "z": 1.326
             },
             "orientation": {
-                "w": -0.18,
-                "x": -0.68,
-                "y": 0.68,
-                "z": 0.18
+                "w": 0.1830127,
+                "x": -0.1830127,
+                "y": 0.6830127,
+                "z": -0.6830127
             }
         },
         {
             "id": 16,
             "position": {
-                "x": 3.630,
-                "y": 0.392,
+                "x": -3.630,
+                "y": -0.392,
                 "z": 1.326
             },
             "orientation": {
-                "w": 0.68,
-                "x": 0.18,
-                "y": -0.18,
-                "z": -0.68
+                "w": 0.6830127,
+                "x": -0.6830127,
+                "y": 0.1830127,
+                "z": -0.1830127
             }
         }
     ]
diff --git a/y2024/vision/modify_extrinsics.cc b/y2024/vision/modify_extrinsics.cc
new file mode 100644
index 0000000..9122191
--- /dev/null
+++ b/y2024/vision/modify_extrinsics.cc
@@ -0,0 +1,189 @@
+#include <cmath>
+#include <filesystem>
+#include <regex>
+
+#include "Eigen/Dense"
+#include "Eigen/Geometry"
+#include "absl/strings/str_format.h"
+
+#include "aos/configuration.h"
+#include "aos/events/event_loop.h"
+#include "aos/flatbuffer_merge.h"
+#include "aos/init.h"
+#include "aos/network/team_number.h"
+#include "aos/time/time.h"
+#include "aos/util/file.h"
+#include "frc971/vision/calibration_generated.h"
+
+// This is a helper program to build and rename calibration files
+// You can:
+// (1) pass it in a new set of orin #, team #, camera #, to rename the file
+// (2) Pass in extrinsics to set the extrinsics
+// By default, writes to /tmp, but will write to calib_files folder if
+// full path is given and calibration_folder is blank
+
+DEFINE_string(orig_calib_file, "",
+              "Intrinsics to use for estimating board pose prior to solving "
+              "for the new intrinsics.");
+DEFINE_string(calibration_folder, "/tmp", "Folder to place calibration files.");
+DEFINE_string(node_name, "",
+              "Node name to use, e.g. orin1, imu; unchanged if blank");
+DEFINE_int32(team_number, -1, "Team number to use; unchanged if -1");
+DEFINE_int32(camera_number, -1, "Camera number to use; unchanged if -1");
+
+DEFINE_bool(set_extrinsics, true, "Set to false to ignore extrinsic data");
+DEFINE_bool(use_inches, true,
+            "Whether to use inches as units (meters if false)");
+DEFINE_bool(use_degrees, true,
+            "Whether to use degrees as units (radians if false)");
+DEFINE_double(camera_x, 0.0, "x location of camera");
+DEFINE_double(camera_y, 0.0, "y location of camera");
+DEFINE_double(camera_z, 0.0, "z location of camera");
+// Don't currently allow for roll of cameras
+DEFINE_double(camera_yaw, 0.0, "yaw of camera about robot z axis");
+DEFINE_double(camera_pitch, 0.0, "pitch of camera relative to robot y axis");
+// TODO: This could be done by setting the pixel size and using the intrinsics
+DEFINE_double(focal_length, 0.002, "Focal length in meters");
+
+namespace frc971::vision {
+namespace {
+
+// TODO: Put this in vision_util_lib?  Except, it depends on Eigen
+std::vector<float> MatrixToVector(const Eigen::Matrix<double, 4, 4> &H) {
+  std::vector<float> data;
+  for (int row = 0; row < 4; ++row) {
+    for (int col = 0; col < 4; ++col) {
+      data.push_back(H(row, col));
+    }
+  }
+  return data;
+}
+
+// Merge the original calibration file with all its changes
+aos::FlatbufferDetachedBuffer<calibration::CameraCalibration> BuildCalibration(
+    const frc971::vision::calibration::CameraCalibration *calibration,
+    std::string node_name, uint16_t camera_number, uint16_t team_number) {
+  aos::FlatbufferDetachedBuffer<frc971::vision::calibration::CameraCalibration>
+      cal_copy = aos::RecursiveCopyFlatBuffer(calibration);
+
+  flatbuffers::FlatBufferBuilder fbb;
+  flatbuffers::Offset<flatbuffers::String> node_name_offset =
+      fbb.CreateString(absl::StrFormat("%s", node_name.c_str()));
+
+  // If we're told to set the extrinsics, clear old and add in new
+  flatbuffers::Offset<calibration::TransformationMatrix>
+      fixed_extrinsics_offset;
+  if (FLAGS_set_extrinsics) {
+    cal_copy.mutable_message()->clear_fixed_extrinsics();
+    Eigen::Affine3d extrinsic_matrix;
+    // Convert to metric
+    double translation_scale = (FLAGS_use_inches ? 0.0254 : 1.0);
+    Eigen::Translation3d translation(FLAGS_camera_x * translation_scale,
+                                     FLAGS_camera_y * translation_scale,
+                                     FLAGS_camera_z * translation_scale);
+
+    // convert to radians
+    double angle_scale = (FLAGS_use_degrees ? M_PI / 180.0 : 1.0);
+    // The rotation that takes robot coordinates (x forward, z up) to camera
+    // coordiantes (z forward, x right)
+    Eigen::Quaterniond R_robo_cam =
+        Eigen::AngleAxisd(-M_PI / 2.0, Eigen::Vector3d::UnitZ()) *
+        Eigen::AngleAxisd(-M_PI / 2.0, Eigen::Vector3d::UnitX());
+    Eigen::AngleAxisd pitchAngle(FLAGS_camera_pitch * angle_scale,
+                                 Eigen::Vector3d::UnitY());
+    Eigen::AngleAxisd yawAngle(FLAGS_camera_yaw * angle_scale,
+                               Eigen::Vector3d::UnitZ());
+
+    Eigen::Quaterniond rotation = yawAngle * pitchAngle * R_robo_cam;
+    Eigen::Vector3d focal_length_offset =
+        (rotation * Eigen::Translation3d(0.0, 0.0, FLAGS_focal_length))
+            .translation();
+    translation = translation * Eigen::Translation3d(focal_length_offset);
+
+    extrinsic_matrix = translation * rotation;
+    flatbuffers::Offset<flatbuffers::Vector<float>> data_offset =
+        fbb.CreateVector(
+            frc971::vision::MatrixToVector(extrinsic_matrix.matrix()));
+    calibration::TransformationMatrix::Builder matrix_builder(fbb);
+    matrix_builder.add_data(data_offset);
+    fixed_extrinsics_offset = matrix_builder.Finish();
+  }
+
+  calibration::CameraCalibration::Builder camera_calibration_builder(fbb);
+  camera_calibration_builder.add_node_name(node_name_offset);
+  camera_calibration_builder.add_team_number(team_number);
+  if (FLAGS_set_extrinsics) {
+    camera_calibration_builder.add_fixed_extrinsics(fixed_extrinsics_offset);
+  }
+  camera_calibration_builder.add_camera_number(camera_number);
+  fbb.Finish(camera_calibration_builder.Finish());
+
+  aos::FlatbufferDetachedBuffer<calibration::CameraCalibration> updated_cal =
+      fbb.Release();
+
+  aos::FlatbufferDetachedBuffer<calibration::CameraCalibration>
+      merged_calibration =
+          aos::MergeFlatBuffers(&cal_copy.message(), &updated_cal.message());
+  return merged_calibration;
+}
+
+void Main(std::string orig_calib_filename) {
+  LOG(INFO) << "Reading from file: " << orig_calib_filename;
+  aos::FlatbufferDetachedBuffer<calibration::CameraCalibration>
+      base_calibration =
+          aos::JsonFileToFlatbuffer<calibration::CameraCalibration>(
+              orig_calib_filename);
+
+  // Populate the new variables from command-line or from base_calibration
+  std::string node_name =
+      (FLAGS_node_name == "" ? base_calibration.message().node_name()->str()
+                             : FLAGS_node_name);
+  int team_number =
+      (FLAGS_team_number == -1 ? base_calibration.message().team_number()
+                               : FLAGS_team_number);
+  int camera_number =
+      (FLAGS_camera_number == -1 ? base_calibration.message().camera_number()
+                                 : FLAGS_camera_number);
+  aos::FlatbufferDetachedBuffer<calibration::CameraCalibration>
+      new_calibration = BuildCalibration(&base_calibration.message(), node_name,
+                                         camera_number, team_number);
+
+  // Set a new timestamp on the file, but leave calibration_timestamp unchanged
+  const aos::realtime_clock::time_point realtime_now =
+      aos::realtime_clock::now();
+  std::stringstream time_ss;
+  time_ss << realtime_now;
+  // Use the camera_id that is set in the json file (not the filename)
+  std::string camera_id = base_calibration.message().camera_id()->str();
+
+  const std::string dirname =
+      (FLAGS_calibration_folder == ""
+           ? std::filesystem::path(orig_calib_filename).parent_path().string()
+           : FLAGS_calibration_folder);
+  const std::string new_calib_filename =
+      dirname + "/" +
+      absl::StrFormat("calibration_%s-%d-%d_cam-%s_%s.json", node_name.c_str(),
+                      team_number, camera_number, camera_id.c_str(),
+                      time_ss.str());
+
+  VLOG(1) << "From: " << orig_calib_filename << " -> "
+          << aos::FlatbufferToJson(base_calibration, {.multi_line = true});
+
+  VLOG(1) << "Writing: " << new_calib_filename << " -> "
+          << aos::FlatbufferToJson(new_calibration, {.multi_line = true});
+
+  LOG(INFO) << "Writing to file: " << new_calib_filename;
+  aos::util::WriteStringToFileOrDie(
+      new_calib_filename,
+      aos::FlatbufferToJson(new_calibration, {.multi_line = true}));
+}
+
+}  // namespace
+}  // namespace frc971::vision
+
+int main(int argc, char **argv) {
+  aos::InitGoogle(&argc, &argv);
+  CHECK(argc == 2) << "Must supply a starting calibration filename";
+  std::string filename = argv[1];
+  frc971::vision::Main(filename);
+}
diff --git a/y2024/vision/rename_calibration_file.sh b/y2024/vision/rename_calibration_file.sh
new file mode 100755
index 0000000..dcdf2f2
--- /dev/null
+++ b/y2024/vision/rename_calibration_file.sh
@@ -0,0 +1,86 @@
+#!/bin/bash
+
+# Helper script to rename the camera calibration file when moving to new robot
+
+# grep isn't happy with set
+# set -e
+
+
+usage_and_exit () {
+    echo
+    echo "Usage:"
+    echo "$0 ORIG_FILENAME NEW_TEAM_NUMBER NEW_ORIN_NUMBER NEW_CAMERA_NUMBER"
+    echo
+    exit 2
+}
+
+if [[ $# -ne 4 ]]; then
+    echo "ERROR: Requires 4 parameters"
+    usage_and_exit
+fi
+
+ORIG_FILENAME=$1
+NEW_TEAM_NUMBER=$2
+NEW_ORIN_NUMBER=$3
+NEW_CAMERA_NUMBER=$4
+
+if [[ ! -x ${ORIG_FILENAME} ]]; then
+    echo "${ORIG_FILENAME} does not exist"
+    usage_and_exit
+fi
+
+check_971=`echo "${NEW_TEAM_NUMBER}" | grep "971"`
+if [[ ${check_971} == "" ]]; then
+    echo "NEW_TEAM_NUMBER (${NEW_TEAM_NUMBER}) does not contain '971'"
+    usage_and_exit
+fi
+
+if [[ ${NEW_ORIN_NUMBER} != 1 && ${NEW_ORIN_NUMBER} != 2 ]]; then
+    echo "NEW_ORIN_NUMBER (${NEW_ORIN_NUMBER}) must be either 1 or 2"
+    usage_and_exit
+fi
+
+if [[ ${NEW_CAMERA_NUMBER} != 0 && ${NEW_CAMERA_NUMBER} != 1 ]]; then
+    echo "NEW_CAMERA_NUMBER (${NEW_CAMERA_NUMBER}) must be either 0 or 1"
+    usage_and_exit
+fi
+
+# Extract parts of the filename, based on just the basename
+# This assumes filenames of the form:
+# calibration_orin-971-1-0_cam-24-01_2024-02-07_20-11-35.566609408.json
+IFS='_' read -r -a name_parts <<< `basename "${ORIG_FILENAME}"`
+
+echo "For ${ORIG_FILENAME}:"
+for element in "${name_parts[@]}"
+do
+    echo "$element"
+done
+
+# Rename file based on this new info (be sure to handle paths properly)
+NEW_FILENAME=`dirname ${ORIG_FILENAME}`"/${name_parts[0]}_orin-${NEW_TEAM_NUMBER}-${NEW_ORIN_NUMBER}-${NEW_CAMERA_NUMBER}_${name_parts[2]}_${name_parts[3]}_${name_parts[4]}"
+
+echo
+echo "For camera id: ${name_parts[2]}"
+echo "Renaming from:"
+echo "${ORIG_FILENAME} to: "
+echo "${NEW_FILENAME}"
+echo
+echo "and changing from "
+echo "${name_parts[1]} to: "
+echo "orin-${NEW_TEAM_NUMBER}-${NEW_ORIN_NUMBER}-${NEW_CAMERA_NUMBER}"
+echo 
+
+mv ${ORIG_FILENAME} ${NEW_FILENAME}
+
+
+echo "REPLACING ORIN_NUMBER"
+sed -i s/orin./orin${NEW_ORIN_NUMBER}/ ${NEW_FILENAME}
+
+echo "Replacing TEAM NUMBER"
+sed -i s/\"team_number\"\:\ [1-9]*\,/\"team_number\"\:\ ${NEW_TEAM_NUMBER},/ ${NEW_FILENAME}
+
+echo "REPLACING CAMERA_NUMBER"
+sed -i s/\"camera_number\"\:\ [0-9]/\"camera_number\"\:\ ${NEW_CAMERA_NUMBER}/ ${NEW_FILENAME}
+
+
+
diff --git a/y2024/vision/target_mapping.cc b/y2024/vision/target_mapping.cc
new file mode 100644
index 0000000..7548445
--- /dev/null
+++ b/y2024/vision/target_mapping.cc
@@ -0,0 +1,438 @@
+#include <string>
+
+#include "Eigen/Dense"
+#include "opencv2/aruco.hpp"
+#include "opencv2/calib3d.hpp"
+#include "opencv2/core/eigen.hpp"
+#include "opencv2/features2d.hpp"
+#include "opencv2/highgui.hpp"
+#include "opencv2/highgui/highgui.hpp"
+#include "opencv2/imgproc.hpp"
+
+#include "aos/configuration.h"
+#include "aos/events/logging/log_reader.h"
+#include "aos/events/simulated_event_loop.h"
+#include "aos/init.h"
+#include "aos/util/mcap_logger.h"
+#include "frc971/constants/constants_sender_lib.h"
+#include "frc971/control_loops/pose.h"
+#include "frc971/vision/calibration_generated.h"
+#include "frc971/vision/charuco_lib.h"
+#include "frc971/vision/target_mapper.h"
+#include "frc971/vision/vision_util_lib.h"
+#include "frc971/vision/visualize_robot.h"
+#include "y2024/constants/simulated_constants_sender.h"
+#include "y2024/vision/vision_util.h"
+
+DEFINE_string(config, "",
+              "If set, override the log's config file with this one.");
+DEFINE_string(constants_path, "y2024/constants/constants.json",
+              "Path to the constant file");
+DEFINE_string(dump_constraints_to, "/tmp/mapping_constraints.txt",
+              "Write the target constraints to this path");
+DEFINE_string(dump_stats_to, "/tmp/mapping_stats.txt",
+              "Write the mapping stats to this path");
+DEFINE_string(field_name, "charged_up",
+              "Field name, for the output json filename and flatbuffer field");
+DEFINE_string(json_path, "y2024/vision/maps/target_map.json",
+              "Specify path for json with initial pose guesses.");
+DEFINE_double(max_pose_error, 1e-6,
+              "Throw out target poses with a higher pose error than this");
+DEFINE_double(
+    max_pose_error_ratio, 0.4,
+    "Throw out target poses with a higher pose error ratio than this");
+DEFINE_string(mcap_output_path, "", "Log to output.");
+DEFINE_string(output_dir, "y2024/vision/maps",
+              "Directory to write solved target map to");
+DEFINE_double(pause_on_distance, 2.0,
+              "Pause if two consecutive implied robot positions differ by more "
+              "than this many meters");
+DEFINE_string(orin, "orin1",
+              "Orin name to generate mcap log for; defaults to pi1.");
+DEFINE_uint64(skip_to, 1,
+              "Start at combined image of this number (1 is the first image)");
+DEFINE_bool(solve, true, "Whether to solve for the field's target map.");
+DEFINE_int32(team_number, 0,
+             "Required: Use the calibration for a node with this team number");
+DEFINE_uint64(wait_key, 1,
+              "Time in ms to wait between images, if no click (0 to wait "
+              "indefinitely until click).");
+
+DECLARE_int32(frozen_target_id);
+DECLARE_int32(min_target_id);
+DECLARE_int32(max_target_id);
+DECLARE_bool(visualize_solver);
+
+namespace y2024::vision {
+using frc971::vision::DataAdapter;
+using frc971::vision::ImageCallback;
+using frc971::vision::PoseUtils;
+using frc971::vision::TargetMap;
+using frc971::vision::TargetMapper;
+using frc971::vision::VisualizeRobot;
+namespace calibration = frc971::vision::calibration;
+
+// Class to handle reading target poses from a replayed log,
+// displaying various debug info, and passing the poses to
+// frc971::vision::TargetMapper for field mapping.
+class TargetMapperReplay {
+ public:
+  TargetMapperReplay(aos::logger::LogReader *reader);
+
+  // Solves for the target poses with the accumulated detections if FLAGS_solve.
+  void MaybeSolve();
+
+ private:
+  static constexpr int kImageWidth = 1280;
+  // Map from pi node name to color for drawing
+  static const std::map<std::string, cv::Scalar> kOrinColors;
+  // Contains fixed target poses without solving, for use with visualization
+  static const TargetMapper kFixedTargetMapper;
+
+  // Change reference frame from camera to robot
+  static Eigen::Affine3d CameraToRobotDetection(Eigen::Affine3d H_camera_target,
+                                                Eigen::Affine3d extrinsics);
+
+  // Adds april tag detections into the detection list, and handles
+  // visualization
+  void HandleAprilTags(const TargetMap &map,
+                       aos::distributed_clock::time_point node_distributed_time,
+                       std::string camera_name, Eigen::Affine3d extrinsics);
+  // Gets images from the given pi and passes apriltag positions to
+  // HandleAprilTags()
+  void HandleNodeCaptures(
+      aos::EventLoop *mapping_event_loop,
+      frc971::constants::ConstantsFetcher<y2024::Constants> *constants_fetcher,
+      int camera_number);
+
+  aos::logger::LogReader *reader_;
+  // April tag detections from all pis
+  std::vector<DataAdapter::TimestampedDetection> timestamped_target_detections_;
+
+  VisualizeRobot vis_robot_;
+  // Set of node names which are currently drawn on the display
+  std::set<std::string> drawn_nodes_;
+  // Number of frames displayed
+  size_t display_count_;
+  // Last time we drew onto the display image.
+  // This is different from when we actually call imshow() to update
+  // the display window
+  aos::distributed_clock::time_point last_draw_time_;
+
+  Eigen::Affine3d last_H_world_robot_;
+  // Maximum distance between consecutive T_world_robot's in one display frame,
+  // used to determine if we need to pause for the user to see this frame
+  // clearly
+  double max_delta_T_world_robot_;
+
+  std::vector<std::unique_ptr<aos::EventLoop>> mapping_event_loops_;
+
+  std::unique_ptr<aos::EventLoop> mcap_event_loop_;
+  std::unique_ptr<aos::McapLogger> relogger_;
+};
+
+const auto TargetMapperReplay::kOrinColors = std::map<std::string, cv::Scalar>{
+    {"/orin1/camera0", cv::Scalar(255, 0, 255)},
+    {"/orin1/camera1", cv::Scalar(255, 255, 0)},
+    {"/imu/camera0", cv::Scalar(0, 255, 255)},
+    {"/imu/camera1", cv::Scalar(255, 165, 0)},
+};
+
+const auto TargetMapperReplay::kFixedTargetMapper =
+    TargetMapper(FLAGS_json_path, ceres::examples::VectorOfConstraints{});
+
+Eigen::Affine3d TargetMapperReplay::CameraToRobotDetection(
+    Eigen::Affine3d H_camera_target, Eigen::Affine3d extrinsics) {
+  const Eigen::Affine3d H_robot_camera = extrinsics;
+  const Eigen::Affine3d H_robot_target = H_robot_camera * H_camera_target;
+  return H_robot_target;
+}
+
+TargetMapperReplay::TargetMapperReplay(aos::logger::LogReader *reader)
+    : reader_(reader),
+      timestamped_target_detections_(),
+      vis_robot_(cv::Size(1280, 1000)),
+      drawn_nodes_(),
+      display_count_(0),
+      last_draw_time_(aos::distributed_clock::min_time),
+      last_H_world_robot_(Eigen::Matrix4d::Identity()),
+      max_delta_T_world_robot_(0.0) {
+  reader_->RemapLoggedChannel("/orin1/constants", "y2024.Constants");
+  reader_->RemapLoggedChannel("/imu/constants", "y2024.Constants");
+  reader_->Register();
+
+  SendSimulationConstants(reader_->event_loop_factory(), FLAGS_team_number,
+                          FLAGS_constants_path);
+
+  std::vector<std::string> node_list;
+  node_list.push_back("imu");
+  node_list.push_back("orin1");
+
+  for (std::string node : node_list) {
+    const aos::Node *pi =
+        aos::configuration::GetNode(reader->configuration(), node);
+
+    mapping_event_loops_.emplace_back(
+        reader_->event_loop_factory()->MakeEventLoop(node + "_mapping_camera0",
+                                                     pi));
+    mapping_event_loops_.emplace_back(
+        reader_->event_loop_factory()->MakeEventLoop(node + "_mapping_camera1",
+                                                     pi));
+    frc971::constants::ConstantsFetcher<y2024::Constants> constants_fetcher(
+        mapping_event_loops_[mapping_event_loops_.size() - 1].get());
+    HandleNodeCaptures(
+        mapping_event_loops_[mapping_event_loops_.size() - 2].get(),
+        &constants_fetcher, 0);
+    HandleNodeCaptures(
+        mapping_event_loops_[mapping_event_loops_.size() - 1].get(),
+        &constants_fetcher, 1);
+  }
+
+  if (FLAGS_visualize_solver) {
+    vis_robot_.ClearImage();
+    const double kFocalLength = 500.0;
+    vis_robot_.SetDefaultViewpoint(kImageWidth, kFocalLength);
+  }
+}
+
+// Add detected apriltag poses relative to the robot to
+// timestamped_target_detections
+void TargetMapperReplay::HandleAprilTags(
+    const TargetMap &map,
+    aos::distributed_clock::time_point node_distributed_time,
+    std::string camera_name, Eigen::Affine3d extrinsics) {
+  bool drew = false;
+  std::stringstream label;
+  label << camera_name << " - ";
+
+  for (const auto *target_pose_fbs : *map.target_poses()) {
+    // Skip detections with invalid ids
+    if (static_cast<TargetMapper::TargetId>(target_pose_fbs->id()) <
+            FLAGS_min_target_id ||
+        static_cast<TargetMapper::TargetId>(target_pose_fbs->id()) >
+            FLAGS_max_target_id) {
+      VLOG(1) << "Skipping tag with invalid id of " << target_pose_fbs->id();
+      continue;
+    }
+
+    // Skip detections with high pose errors
+    if (target_pose_fbs->pose_error() > FLAGS_max_pose_error) {
+      VLOG(1) << "Skipping tag " << target_pose_fbs->id()
+              << " due to pose error of " << target_pose_fbs->pose_error();
+      continue;
+    }
+    // Skip detections with high pose error ratios
+    if (target_pose_fbs->pose_error_ratio() > FLAGS_max_pose_error_ratio) {
+      VLOG(1) << "Skipping tag " << target_pose_fbs->id()
+              << " due to pose error ratio of "
+              << target_pose_fbs->pose_error_ratio();
+      continue;
+    }
+
+    const TargetMapper::TargetPose target_pose =
+        PoseUtils::TargetPoseFromFbs(*target_pose_fbs);
+
+    Eigen::Affine3d H_camera_target =
+        Eigen::Translation3d(target_pose.pose.p) * target_pose.pose.q;
+    Eigen::Affine3d H_robot_target =
+        CameraToRobotDetection(H_camera_target, extrinsics);
+
+    ceres::examples::Pose3d target_pose_camera =
+        PoseUtils::Affine3dToPose3d(H_camera_target);
+    double distance_from_camera = target_pose_camera.p.norm();
+    double distortion_factor = target_pose_fbs->distortion_factor();
+
+    if (distance_from_camera > 5.0) {
+      continue;
+    }
+
+    CHECK(map.has_monotonic_timestamp_ns())
+        << "Need detection timestamps for mapping";
+
+    timestamped_target_detections_.emplace_back(
+        DataAdapter::TimestampedDetection{
+            .time = node_distributed_time,
+            .H_robot_target = H_robot_target,
+            .distance_from_camera = distance_from_camera,
+            .distortion_factor = distortion_factor,
+            .id = static_cast<TargetMapper::TargetId>(target_pose.id)});
+
+    if (FLAGS_visualize_solver) {
+      // If we've already drawn this camera_name in the current image,
+      // display the image before clearing and adding the new poses
+      if (drawn_nodes_.count(camera_name) != 0) {
+        display_count_++;
+        cv::putText(vis_robot_.image_,
+                    "Poses #" + std::to_string(display_count_),
+                    cv::Point(600, 10), cv::FONT_HERSHEY_PLAIN, 1.0,
+                    cv::Scalar(255, 255, 255));
+
+        if (display_count_ >= FLAGS_skip_to) {
+          VLOG(1) << "Showing image for node " << camera_name
+                  << " since we've drawn it already";
+          cv::imshow("View", vis_robot_.image_);
+          // Pause if delta_T is too large, but only after first image (to make
+          // sure the delta's are correct
+          if (max_delta_T_world_robot_ > FLAGS_pause_on_distance &&
+              display_count_ > 1) {
+            LOG(INFO) << "Pausing since the delta between robot estimates is "
+                      << max_delta_T_world_robot_ << " which is > threshold of "
+                      << FLAGS_pause_on_distance;
+            cv::waitKey(0);
+          } else {
+            cv::waitKey(FLAGS_wait_key);
+          }
+          max_delta_T_world_robot_ = 0.0;
+        } else {
+          VLOG(1) << "At poses #" << std::to_string(display_count_);
+        }
+        vis_robot_.ClearImage();
+        drawn_nodes_.clear();
+      }
+
+      Eigen::Affine3d H_world_target = PoseUtils::Pose3dToAffine3d(
+          kFixedTargetMapper.GetTargetPoseById(target_pose_fbs->id())->pose);
+      Eigen::Affine3d H_world_robot = H_world_target * H_robot_target.inverse();
+      VLOG(2) << camera_name << ", id " << target_pose_fbs->id()
+              << ", t = " << node_distributed_time
+              << ", pose_error = " << target_pose_fbs->pose_error()
+              << ", pose_error_ratio = " << target_pose_fbs->pose_error_ratio()
+              << ", robot_pos (x,y,z) = "
+              << H_world_robot.translation().transpose();
+
+      label << "id " << target_pose_fbs->id() << ": err (% of max): "
+            << (target_pose_fbs->pose_error() / FLAGS_max_pose_error)
+            << " err_ratio: " << target_pose_fbs->pose_error_ratio() << " ";
+
+      vis_robot_.DrawRobotOutline(H_world_robot, camera_name,
+                                  kOrinColors.at(camera_name));
+      vis_robot_.DrawFrameAxes(H_world_target,
+                               std::to_string(target_pose_fbs->id()),
+                               kOrinColors.at(camera_name));
+
+      double delta_T_world_robot =
+          (H_world_robot.translation() - last_H_world_robot_.translation())
+              .norm();
+      max_delta_T_world_robot_ =
+          std::max(delta_T_world_robot, max_delta_T_world_robot_);
+
+      VLOG(1) << "Drew in info for robot " << camera_name << " and target #"
+              << target_pose_fbs->id();
+      drew = true;
+      last_draw_time_ = node_distributed_time;
+      last_H_world_robot_ = H_world_robot;
+    }
+  }
+  if (FLAGS_visualize_solver) {
+    if (drew) {
+      // Collect all the labels from a given node, and add the text
+      size_t pi_number =
+          static_cast<size_t>(camera_name[camera_name.size() - 1] - '0');
+      cv::putText(vis_robot_.image_, label.str(),
+                  cv::Point(10, 10 + 20 * pi_number), cv::FONT_HERSHEY_PLAIN,
+                  1.0, kOrinColors.at(camera_name));
+
+      drawn_nodes_.emplace(camera_name);
+    } else if (node_distributed_time - last_draw_time_ >
+                   std::chrono::milliseconds(30) &&
+               display_count_ >= FLAGS_skip_to) {
+      cv::putText(vis_robot_.image_, "No detections", cv::Point(10, 0),
+                  cv::FONT_HERSHEY_PLAIN, 1.0, kOrinColors.at(camera_name));
+      // Display and clear the image if we haven't draw in a while
+      VLOG(1) << "Displaying image due to time lapse";
+      cv::imshow("View", vis_robot_.image_);
+      cv::waitKey(FLAGS_wait_key);
+      vis_robot_.ClearImage();
+      max_delta_T_world_robot_ = 0.0;
+      drawn_nodes_.clear();
+    }
+  }
+}
+
+void TargetMapperReplay::HandleNodeCaptures(
+    aos::EventLoop *mapping_event_loop,
+    frc971::constants::ConstantsFetcher<y2024::Constants> *constants_fetcher,
+    int camera_number) {
+  // Get the camera extrinsics
+  const auto *calibration = FindCameraCalibration(
+      constants_fetcher->constants(),
+      mapping_event_loop->node()->name()->string_view(), camera_number);
+  cv::Mat extrinsics_cv = frc971::vision::CameraExtrinsics(calibration).value();
+  Eigen::Matrix4d extrinsics_matrix;
+  cv::cv2eigen(extrinsics_cv, extrinsics_matrix);
+  const auto extrinsics = Eigen::Affine3d(extrinsics_matrix);
+  std::string camera_name = absl::StrFormat(
+      "/%s/camera%d", mapping_event_loop->node()->name()->str(), camera_number);
+
+  mapping_event_loop->MakeWatcher(
+      camera_name.c_str(), [this, mapping_event_loop, extrinsics,
+                            camera_name](const TargetMap &map) {
+        aos::distributed_clock::time_point node_distributed_time =
+            reader_->event_loop_factory()
+                ->GetNodeEventLoopFactory(mapping_event_loop->node())
+                ->ToDistributedClock(aos::monotonic_clock::time_point(
+                    aos::monotonic_clock::duration(
+                        map.monotonic_timestamp_ns())));
+
+        HandleAprilTags(map, node_distributed_time, camera_name, extrinsics);
+      });
+}
+
+void TargetMapperReplay::MaybeSolve() {
+  if (FLAGS_solve) {
+    auto target_constraints =
+        DataAdapter::MatchTargetDetections(timestamped_target_detections_);
+
+    // Remove constraints between the two sides of the field - these are
+    // basically garbage because of how far the camera is. We will use seeding
+    // below to connect the two sides
+    target_constraints.erase(
+        std::remove_if(target_constraints.begin(), target_constraints.end(),
+                       [](const auto &constraint) {
+                         constexpr TargetMapper::TargetId kMaxRedId = 4;
+                         TargetMapper::TargetId min_id =
+                             std::min(constraint.id_begin, constraint.id_end);
+                         TargetMapper::TargetId max_id =
+                             std::max(constraint.id_begin, constraint.id_end);
+                         return (min_id <= kMaxRedId && max_id > kMaxRedId);
+                       }),
+        target_constraints.end());
+
+    LOG(INFO) << "Solving for locations of tags with "
+              << target_constraints.size() << " constraints";
+    TargetMapper mapper(FLAGS_json_path, target_constraints);
+    mapper.Solve(FLAGS_field_name, FLAGS_output_dir);
+
+    if (!FLAGS_dump_constraints_to.empty()) {
+      mapper.DumpConstraints(FLAGS_dump_constraints_to);
+    }
+    if (!FLAGS_dump_stats_to.empty()) {
+      mapper.DumpStats(FLAGS_dump_stats_to);
+    }
+  }
+}
+
+void MappingMain(int argc, char *argv[]) {
+  std::vector<DataAdapter::TimestampedDetection> timestamped_target_detections;
+
+  std::optional<aos::FlatbufferDetachedBuffer<aos::Configuration>> config =
+      (FLAGS_config.empty()
+           ? std::nullopt
+           : std::make_optional(aos::configuration::ReadConfig(FLAGS_config)));
+
+  // Open logfiles
+  aos::logger::LogReader reader(
+      aos::logger::SortParts(aos::logger::FindLogs(argc, argv)),
+      config.has_value() ? &config->message() : nullptr);
+
+  TargetMapperReplay mapper_replay(&reader);
+  reader.event_loop_factory()->Run();
+  mapper_replay.MaybeSolve();
+}
+
+}  // namespace y2024::vision
+
+int main(int argc, char **argv) {
+  aos::InitGoogle(&argc, &argv);
+  y2024::vision::MappingMain(argc, argv);
+}
diff --git a/y2024/vision/viewer.cc b/y2024/vision/viewer.cc
index 6f83ec2..d5ada14 100644
--- a/y2024/vision/viewer.cc
+++ b/y2024/vision/viewer.cc
@@ -85,8 +85,11 @@
 
   frc971::constants::ConstantsFetcher<y2024::Constants> constants_fetcher(
       &event_loop);
+  CHECK(FLAGS_channel.length() == 8);
+  int camera_id = std::stoi(FLAGS_channel.substr(7, 1));
   const auto *calibration_data = FindCameraCalibration(
-      constants_fetcher.constants(), event_loop.node()->name()->string_view());
+      constants_fetcher.constants(), event_loop.node()->name()->string_view(),
+      camera_id);
   const cv::Mat intrinsics = frc971::vision::CameraIntrinsics(calibration_data);
   const cv::Mat dist_coeffs =
       frc971::vision::CameraDistCoeffs(calibration_data);
diff --git a/y2024/vision/vision_util.cc b/y2024/vision/vision_util.cc
index 4c196ce..6acd234 100644
--- a/y2024/vision/vision_util.cc
+++ b/y2024/vision/vision_util.cc
@@ -5,17 +5,20 @@
 namespace y2024::vision {
 
 const frc971::vision::calibration::CameraCalibration *FindCameraCalibration(
-    const y2024::Constants &calibration_data, std::string_view node_name) {
+    const y2024::Constants &calibration_data, std::string_view node_name,
+    int camera_number) {
   CHECK(calibration_data.has_cameras());
   for (const y2024::CameraConfiguration *candidate :
        *calibration_data.cameras()) {
     CHECK(candidate->has_calibration());
-    if (candidate->calibration()->node_name()->string_view() != node_name) {
+    if (candidate->calibration()->node_name()->string_view() != node_name ||
+        candidate->calibration()->camera_number() != camera_number) {
       continue;
     }
     return candidate->calibration();
   }
-  LOG(FATAL) << ": Failed to find camera calibration for " << node_name;
+  LOG(FATAL) << ": Failed to find camera calibration for " << node_name
+             << " and camera number " << camera_number;
 }
 
 }  // namespace y2024::vision
diff --git a/y2024/vision/vision_util.h b/y2024/vision/vision_util.h
index d8fa562..c903760 100644
--- a/y2024/vision/vision_util.h
+++ b/y2024/vision/vision_util.h
@@ -9,7 +9,8 @@
 namespace y2024::vision {
 
 const frc971::vision::calibration::CameraCalibration *FindCameraCalibration(
-    const y2024::Constants &calibration_data, std::string_view node_name);
+    const y2024::Constants &calibration_data, std::string_view node_name,
+    int camera_number);
 
 }  // namespace y2024::vision
 
diff --git a/y2024/wpilib_interface.cc b/y2024/wpilib_interface.cc
index 4a6c00b..d286939 100644
--- a/y2024/wpilib_interface.cc
+++ b/y2024/wpilib_interface.cc
@@ -77,12 +77,24 @@
 
 constexpr double kMaxBringupPower = 12.0;
 
-double intake_pot_translate(double voltage) {
-  return voltage * Values::kIntakePivotPotRadiansPerVolt();
+double climber_pot_translate(double voltage) {
+  return -1 * voltage * Values::kClimberPotMetersPerVolt();
 }
 
-double climber_pot_translate(double voltage) {
-  return voltage * Values::kClimberPotRadiansPerVolt();
+double extend_pot_translate(double voltage) {
+  return voltage * Values::kExtendPotMetersPerVolt();
+}
+
+double catapult_pot_translate(double voltage) {
+  return voltage * Values::kCatapultPotRadiansPerVolt();
+}
+
+double turret_pot_translate(double voltage) {
+  return -1 * voltage * Values::kTurretPotRadiansPerVolt();
+}
+
+double altitude_pot_translate(double voltage) {
+  return -1 * voltage * Values::kAltitudePotRadiansPerVolt();
 }
 
 double drivetrain_velocity_translate(double in) {
@@ -96,7 +108,10 @@
     Values::kMaxDrivetrainEncoderPulsesPerSecond(),
     Values::kMaxIntakePivotEncoderPulsesPerSecond(),
     Values::kMaxClimberEncoderPulsesPerSecond(),
+    Values::kMaxExtendEncoderPulsesPerSecond(),
+    Values::kMaxCatapultEncoderPulsesPerSecond(),
 });
+
 static_assert(kMaxFastEncoderPulsesPerSecond <= 1300000,
               "fast encoders are too fast");
 
@@ -120,8 +135,15 @@
                 ::frc971::control_loops::drivetrain::PositionStatic>(
                 "/drivetrain")),
         gyro_sender_(event_loop->MakeSender<::frc971::sensors::GyroReading>(
-            "/drivetrain")){};
-  void Start() override { AddToDMA(&imu_yaw_rate_reader_); }
+            "/drivetrain")) {
+    UpdateFastEncoderFilterHz(kMaxFastEncoderPulsesPerSecond);
+    event_loop->SetRuntimeAffinity(aos::MakeCpusetFromCpus({0}));
+  };
+  void Start() override {
+    AddToDMA(&imu_yaw_rate_reader_);
+    AddToDMA(&turret_encoder_.reader());
+    AddToDMA(&altitude_encoder_.reader());
+  }
 
   // Auto mode switches.
   void set_autonomous_mode(int i, ::std::unique_ptr<frc::DigitalInput> sensor) {
@@ -140,26 +162,56 @@
 
       CopyPosition(intake_pivot_encoder_, builder->add_intake_pivot(),
                    Values::kIntakePivotEncoderCountsPerRevolution(),
-                   Values::kIntakePivotEncoderRatio(), intake_pot_translate,
-                   true,
-                   robot_constants_->robot()
-                       ->intake_constants()
-                       ->potentiometer_offset());
+                   Values::kIntakePivotEncoderRatio(), /* reversed: */ false);
 
       CopyPosition(climber_encoder_, builder->add_climber(),
                    Values::kClimberEncoderCountsPerRevolution(),
-                   Values::kClimberEncoderRatio(), climber_pot_translate, true,
+                   Values::kClimberEncoderMetersPerRadian(),
+                   climber_pot_translate, false,
                    robot_constants_->robot()
                        ->climber_constants()
                        ->potentiometer_offset());
 
+      CopyPosition(extend_encoder_, builder->add_extend(),
+                   Values::kExtendEncoderCountsPerRevolution(),
+                   Values::kExtendEncoderMetersPerRadian(),
+                   extend_pot_translate, false,
+                   robot_constants_->robot()
+                       ->extend_constants()
+                       ->potentiometer_offset());
+
+      CopyPosition(catapult_encoder_, builder->add_catapult(),
+                   Values::kCatapultEncoderCountsPerRevolution(),
+                   Values::kCatapultEncoderRatio(), catapult_pot_translate,
+                   true,
+                   robot_constants_->robot()
+                       ->catapult_constants()
+                       ->potentiometer_offset());
+
+      CopyPosition(turret_encoder_, builder->add_turret(),
+                   Values::kTurretEncoderCountsPerRevolution(),
+                   Values::kTurretEncoderRatio(), turret_pot_translate, true,
+                   robot_constants_->robot()
+                       ->turret_constants()
+                       ->potentiometer_offset());
+
+      CopyPosition(altitude_encoder_, builder->add_altitude(),
+                   Values::kAltitudeEncoderCountsPerRevolution(),
+                   Values::kAltitudeEncoderRatio(), altitude_pot_translate,
+                   true,
+                   robot_constants_->robot()
+                       ->altitude_constants()
+                       ->potentiometer_offset());
+
       builder->set_transfer_beambreak(transfer_beam_break_->Get());
+      builder->set_extend_beambreak(extend_beam_break_->Get());
+      builder->set_catapult_beambreak(catapult_beam_break_->Get());
       builder.CheckOk(builder.Send());
     }
 
     SendDrivetrainPosition(drivetrain_position_sender_.MakeStaticBuilder(),
                            drivetrain_velocity_translate,
-                           constants::Values::DrivetrainEncoderToMeters, false,
+                           constants::Values::DrivetrainEncoderToMeters, true,
                            false);
 
     {
@@ -212,18 +264,24 @@
   }
 
   void set_intake_pivot(::std::unique_ptr<frc::Encoder> encoder,
-                        ::std::unique_ptr<frc::DigitalInput> absolute_pwm,
-                        ::std::unique_ptr<frc::AnalogInput> potentiometer) {
+                        ::std::unique_ptr<frc::DigitalInput> absolute_pwm) {
     fast_encoder_filter_.Add(encoder.get());
     intake_pivot_encoder_.set_encoder(::std::move(encoder));
     intake_pivot_encoder_.set_absolute_pwm(::std::move(absolute_pwm));
-    intake_pivot_encoder_.set_potentiometer(::std::move(potentiometer));
   }
 
   void set_transfer_beambreak(::std::unique_ptr<frc::DigitalInput> sensor) {
     transfer_beam_break_ = ::std::move(sensor);
   }
 
+  void set_extend_beambreak(::std::unique_ptr<frc::DigitalInput> sensor) {
+    extend_beam_break_ = ::std::move(sensor);
+  }
+
+  void set_catapult_beambreak(::std::unique_ptr<frc::DigitalInput> sensor) {
+    catapult_beam_break_ = ::std::move(sensor);
+  }
+
   void set_climber(::std::unique_ptr<frc::Encoder> encoder,
                    ::std::unique_ptr<frc::DigitalInput> absolute_pwm,
                    ::std::unique_ptr<frc::AnalogInput> potentiometer) {
@@ -233,6 +291,42 @@
     climber_encoder_.set_potentiometer(::std::move(potentiometer));
   }
 
+  void set_extend(::std::unique_ptr<frc::Encoder> encoder,
+                  ::std::unique_ptr<frc::DigitalInput> absolute_pwm,
+                  ::std::unique_ptr<frc::AnalogInput> potentiometer) {
+    fast_encoder_filter_.Add(encoder.get());
+    extend_encoder_.set_encoder(::std::move(encoder));
+    extend_encoder_.set_absolute_pwm(::std::move(absolute_pwm));
+    extend_encoder_.set_potentiometer(::std::move(potentiometer));
+  }
+
+  void set_catapult(::std::unique_ptr<frc::Encoder> encoder,
+                    ::std::unique_ptr<frc::DigitalInput> absolute_pwm,
+                    ::std::unique_ptr<frc::AnalogInput> potentiometer) {
+    fast_encoder_filter_.Add(encoder.get());
+    catapult_encoder_.set_encoder(::std::move(encoder));
+    catapult_encoder_.set_absolute_pwm(::std::move(absolute_pwm));
+    catapult_encoder_.set_potentiometer(::std::move(potentiometer));
+  }
+
+  void set_turret(::std::unique_ptr<frc::Encoder> encoder,
+                  ::std::unique_ptr<frc::DigitalInput> absolute_pwm,
+                  ::std::unique_ptr<frc::AnalogInput> potentiometer) {
+    fast_encoder_filter_.Add(encoder.get());
+    turret_encoder_.set_encoder(::std::move(encoder));
+    turret_encoder_.set_absolute_pwm(::std::move(absolute_pwm));
+    turret_encoder_.set_potentiometer(::std::move(potentiometer));
+  }
+
+  void set_altitude(::std::unique_ptr<frc::Encoder> encoder,
+                    ::std::unique_ptr<frc::DigitalInput> absolute_pwm,
+                    ::std::unique_ptr<frc::AnalogInput> potentiometer) {
+    fast_encoder_filter_.Add(encoder.get());
+    altitude_encoder_.set_encoder(::std::move(encoder));
+    altitude_encoder_.set_absolute_pwm(::std::move(absolute_pwm));
+    altitude_encoder_.set_potentiometer(::std::move(potentiometer));
+  }
+
  private:
   const Constants *robot_constants_;
 
@@ -244,12 +338,51 @@
 
   std::array<std::unique_ptr<frc::DigitalInput>, 2> autonomous_modes_;
 
-  std::unique_ptr<frc::DigitalInput> imu_yaw_rate_input_, transfer_beam_break_;
+  std::unique_ptr<frc::DigitalInput> imu_yaw_rate_input_, transfer_beam_break_,
+      extend_beam_break_, catapult_beam_break_;
 
-  frc971::wpilib::AbsoluteEncoderAndPotentiometer intake_pivot_encoder_;
-  frc971::wpilib::AbsoluteEncoderAndPotentiometer climber_encoder_;
+  frc971::wpilib::AbsoluteEncoder intake_pivot_encoder_;
+  frc971::wpilib::AbsoluteEncoderAndPotentiometer climber_encoder_,
+      catapult_encoder_, extend_encoder_;
 
   frc971::wpilib::DMAPulseWidthReader imu_yaw_rate_reader_;
+
+  frc971::wpilib::DMAAbsoluteEncoderAndPotentiometer turret_encoder_,
+      altitude_encoder_;
+};
+
+class SuperstructurePWMWriter
+    : public ::frc971::wpilib::LoopOutputHandler<superstructure::Output> {
+ public:
+  SuperstructurePWMWriter(aos::EventLoop *event_loop)
+      : frc971::wpilib::LoopOutputHandler<superstructure::Output>(
+            event_loop, "/superstructure") {}
+
+  void set_catapult_kraken_one(::std::unique_ptr<::frc::TalonFX> t) {
+    catapult_kraken_one_ = ::std::move(t);
+  }
+  void set_catapult_kraken_two(::std::unique_ptr<::frc::TalonFX> t) {
+    catapult_kraken_two_ = ::std::move(t);
+  }
+
+ private:
+  void Stop() override {
+    AOS_LOG(WARNING, "Superstructure output too old.\n");
+    catapult_kraken_one_->SetDisabled();
+    catapult_kraken_two_->SetDisabled();
+  }
+
+  void Write(const superstructure::Output &output) override {
+    WritePwm(output.catapult_voltage(), catapult_kraken_one_.get());
+    WritePwm(output.catapult_voltage(), catapult_kraken_two_.get());
+  }
+
+  template <typename T>
+  static void WritePwm(const double voltage, T *motor) {
+    motor->SetSpeed(std::clamp(voltage, -kMaxBringupPower, kMaxBringupPower) /
+                    12.0);
+  }
+  ::std::unique_ptr<::frc::TalonFX> catapult_kraken_one_, catapult_kraken_two_;
 };
 
 class WPILibRobot : public ::frc971::wpilib::WPILibRobotBase {
@@ -270,6 +403,8 @@
         &constant_fetcher_event_loop);
     const Constants *robot_constants = &constants_fetcher.constants();
 
+    AddLoop(&constant_fetcher_event_loop);
+
     // Thread 1.
     ::aos::ShmEventLoop joystick_sender_event_loop(&config.message());
     ::frc971::wpilib::JoystickSender joystick_sender(
@@ -285,18 +420,30 @@
     ::aos::ShmEventLoop sensor_reader_event_loop(&config.message());
     SensorReader sensor_reader(&sensor_reader_event_loop, robot_constants);
     sensor_reader.set_pwm_trigger(true);
-    sensor_reader.set_drivetrain_left_encoder(make_encoder(1));
-    sensor_reader.set_drivetrain_right_encoder(make_encoder(0));
-    sensor_reader.set_yaw_rate_input(make_unique<frc::DigitalInput>(0));
-    // TODO: (niko) change values once robot is wired
-    sensor_reader.set_intake_pivot(make_encoder(4),
-                                   make_unique<frc::DigitalInput>(4),
-                                   make_unique<frc::AnalogInput>(4));
-    sensor_reader.set_transfer_beambreak(make_unique<frc::DigitalInput>(7));
+    sensor_reader.set_drivetrain_left_encoder(
+        std::make_unique<frc::Encoder>(8, 9));
+    sensor_reader.set_drivetrain_right_encoder(
+        std::make_unique<frc::Encoder>(6, 7));
+    sensor_reader.set_yaw_rate_input(make_unique<frc::DigitalInput>(25));
+    sensor_reader.set_intake_pivot(make_encoder(3),
+                                   make_unique<frc::DigitalInput>(3));
+    sensor_reader.set_transfer_beambreak(make_unique<frc::DigitalInput>(23));
+    sensor_reader.set_extend_beambreak(make_unique<frc::DigitalInput>(24));
+    sensor_reader.set_catapult_beambreak(make_unique<frc::DigitalInput>(22));
 
-    sensor_reader.set_climber(make_encoder(5),
-                              make_unique<frc::DigitalInput>(5),
-                              make_unique<frc::AnalogInput>(5));
+    sensor_reader.set_climber(make_encoder(4),
+                              make_unique<frc::DigitalInput>(4),
+                              make_unique<frc::AnalogInput>(4));
+    sensor_reader.set_extend(make_encoder(5), make_unique<frc::DigitalInput>(5),
+                             make_unique<frc::AnalogInput>(5));
+    sensor_reader.set_catapult(make_encoder(0),
+                               make_unique<frc::DigitalInput>(0),
+                               make_unique<frc::AnalogInput>(0));
+    sensor_reader.set_turret(make_encoder(2), make_unique<frc::DigitalInput>(2),
+                             make_unique<frc::AnalogInput>(2));
+    sensor_reader.set_altitude(make_encoder(1),
+                               make_unique<frc::DigitalInput>(1),
+                               make_unique<frc::AnalogInput>(1));
 
     AddLoop(&sensor_reader_event_loop);
 
@@ -307,44 +454,72 @@
       c_Phoenix_Diagnostics_Dispose();
     }
 
-    std::vector<ctre::phoenix6::BaseStatusSignal *> signals_registry;
+    std::vector<ctre::phoenix6::BaseStatusSignal *> canivore_signal_registry;
+    std::vector<ctre::phoenix6::BaseStatusSignal *> rio_signal_registry;
 
     const CurrentLimits *current_limits =
         robot_constants->common()->current_limits();
 
     std::shared_ptr<TalonFX> right_front = std::make_shared<TalonFX>(
-        0, false, "Drivetrain Bus", &signals_registry,
+        2, true, "Drivetrain Bus", &canivore_signal_registry,
         current_limits->drivetrain_supply_current_limit(),
         current_limits->drivetrain_stator_current_limit());
     std::shared_ptr<TalonFX> right_back = std::make_shared<TalonFX>(
-        1, false, "Drivetrain Bus", &signals_registry,
+        1, true, "Drivetrain Bus", &canivore_signal_registry,
         current_limits->drivetrain_supply_current_limit(),
         current_limits->drivetrain_stator_current_limit());
     std::shared_ptr<TalonFX> left_front = std::make_shared<TalonFX>(
-        2, false, "Drivetrain Bus", &signals_registry,
+        4, false, "Drivetrain Bus", &canivore_signal_registry,
         current_limits->drivetrain_supply_current_limit(),
         current_limits->drivetrain_stator_current_limit());
     std::shared_ptr<TalonFX> left_back = std::make_shared<TalonFX>(
-        3, false, "Drivetrain Bus", &signals_registry,
+        5, false, "Drivetrain Bus", &canivore_signal_registry,
         current_limits->drivetrain_supply_current_limit(),
         current_limits->drivetrain_stator_current_limit());
     std::shared_ptr<TalonFX> intake_pivot = std::make_shared<TalonFX>(
-        4, false, "Drivetrain Bus", &signals_registry,
+        6, false, "Drivetrain Bus", &canivore_signal_registry,
         current_limits->intake_pivot_stator_current_limit(),
         current_limits->intake_pivot_supply_current_limit());
-    std::shared_ptr<TalonFX> intake_roller = std::make_shared<TalonFX>(
-        5, false, "Drivetrain Bus", &signals_registry,
-        current_limits->intake_roller_stator_current_limit(),
-        current_limits->intake_roller_supply_current_limit());
-    std::shared_ptr<TalonFX> transfer_roller = std::make_shared<TalonFX>(
-        6, false, "Drivetrain Bus", &signals_registry,
-        current_limits->transfer_roller_stator_current_limit(),
-        current_limits->transfer_roller_supply_current_limit());
-
+    std::shared_ptr<TalonFX> altitude = std::make_shared<TalonFX>(
+        9, false, "Drivetrain Bus", &canivore_signal_registry,
+        current_limits->altitude_stator_current_limit(),
+        current_limits->altitude_supply_current_limit());
+    std::shared_ptr<TalonFX> turret = std::make_shared<TalonFX>(
+        3, true, "Drivetrain Bus", &canivore_signal_registry,
+        current_limits->turret_stator_current_limit(),
+        current_limits->turret_supply_current_limit());
     std::shared_ptr<TalonFX> climber = std::make_shared<TalonFX>(
-        7, false, "Drivetrain Bus", &signals_registry,
+        7, true, "rio", &rio_signal_registry,
         current_limits->climber_stator_current_limit(),
         current_limits->climber_supply_current_limit());
+    std::shared_ptr<TalonFX> extend = std::make_shared<TalonFX>(
+        12, false, "Drivetrain Bus", &canivore_signal_registry,
+        current_limits->extend_stator_current_limit(),
+        current_limits->extend_supply_current_limit());
+    std::shared_ptr<TalonFX> intake_roller = std::make_shared<TalonFX>(
+        8, false, "rio", &rio_signal_registry,
+        current_limits->intake_roller_stator_current_limit(),
+        current_limits->intake_roller_supply_current_limit());
+    std::shared_ptr<TalonFX> retention_roller = std::make_shared<TalonFX>(
+        10, true, "rio", &rio_signal_registry,
+        current_limits->retention_roller_stator_current_limit(),
+        current_limits->retention_roller_supply_current_limit());
+    std::shared_ptr<TalonFX> transfer_roller = std::make_shared<TalonFX>(
+        11, true, "rio", &rio_signal_registry,
+        current_limits->transfer_roller_stator_current_limit(),
+        current_limits->transfer_roller_supply_current_limit());
+    std::shared_ptr<TalonFX> extend_roller = std::make_shared<TalonFX>(
+        13, true, "rio", &rio_signal_registry,
+        current_limits->extend_roller_stator_current_limit(),
+        current_limits->extend_roller_supply_current_limit());
+    std::shared_ptr<TalonFX> catapult_one = std::make_shared<TalonFX>(
+        14, false, "Drivetrain Bus", &canivore_signal_registry,
+        current_limits->catapult_stator_current_limit(),
+        current_limits->catapult_supply_current_limit());
+    std::shared_ptr<TalonFX> catapult_two = std::make_shared<TalonFX>(
+        15, false, "Drivetrain Bus", &canivore_signal_registry,
+        current_limits->catapult_stator_current_limit(),
+        current_limits->catapult_supply_current_limit());
 
     ctre::phoenix::platform::can::CANComm_SetRxSchedPriority(
         constants::Values::kDrivetrainRxPriority, true, "Drivetrain Bus");
@@ -354,18 +529,27 @@
     ::aos::ShmEventLoop can_sensor_reader_event_loop(&config.message());
     can_sensor_reader_event_loop.set_name("CANSensorReader");
 
+    ::aos::ShmEventLoop rio_sensor_reader_event_loop(&config.message());
+    rio_sensor_reader_event_loop.set_name("RioSensorReader");
+
     // Creating list of talonfx for CANSensorReader
     std::vector<std::shared_ptr<TalonFX>> drivetrain_talonfxs;
-    std::vector<std::shared_ptr<TalonFX>> talonfxs;
+    std::vector<std::shared_ptr<TalonFX>> canivore_talonfxs;
+    std::vector<std::shared_ptr<TalonFX>> rio_talonfxs;
 
     for (auto talonfx : {right_front, right_back, left_front, left_back}) {
       drivetrain_talonfxs.push_back(talonfx);
-      talonfxs.push_back(talonfx);
+      canivore_talonfxs.push_back(talonfx);
     }
 
     for (auto talonfx :
-         {intake_pivot, intake_roller, transfer_roller, climber}) {
-      talonfxs.push_back(talonfx);
+         {intake_pivot, turret, altitude, catapult_one, catapult_two, extend}) {
+      canivore_talonfxs.push_back(talonfx);
+    }
+
+    for (auto talonfx : {intake_roller, transfer_roller, climber, extend_roller,
+                         retention_roller}) {
+      rio_talonfxs.push_back(talonfx);
     }
 
     aos::Sender<frc971::control_loops::drivetrain::CANPositionStatic>
@@ -378,22 +562,21 @@
         superstructure_can_position_sender =
             can_sensor_reader_event_loop.MakeSender<
                 y2024::control_loops::superstructure::CANPositionStatic>(
-                "/superstructure");
+                "/superstructure/canivore");
 
-    frc971::wpilib::CANSensorReader can_sensor_reader(
-        &can_sensor_reader_event_loop, std::move(signals_registry), talonfxs,
-        [drivetrain_talonfxs, &intake_pivot, &intake_roller, &transfer_roller,
-         &climber, &drivetrain_can_position_sender,
-         &superstructure_can_position_sender](
-            ctre::phoenix::StatusCode status) {
+    frc971::wpilib::CANSensorReader canivore_can_sensor_reader(
+        &can_sensor_reader_event_loop, std::move(canivore_signal_registry),
+        canivore_talonfxs,
+        [drivetrain_talonfxs, &intake_pivot, &turret, &altitude, &catapult_one,
+         &catapult_two, &drivetrain_can_position_sender,
+         &superstructure_can_position_sender,
+         &extend](ctre::phoenix::StatusCode status) {
           aos::Sender<frc971::control_loops::drivetrain::CANPositionStatic>::
               StaticBuilder drivetrain_can_builder =
                   drivetrain_can_position_sender.MakeStaticBuilder();
 
           auto drivetrain_falcon_vector =
-              drivetrain_can_builder->add_talonfxs();
-
-          CHECK(drivetrain_falcon_vector->reserve(drivetrain_talonfxs.size()));
+              CHECK_NOTNULL(drivetrain_can_builder->add_talonfxs());
 
           for (auto talonfx : drivetrain_talonfxs) {
             talonfx->SerializePosition(
@@ -411,26 +594,69 @@
               StaticBuilder superstructure_can_builder =
                   superstructure_can_position_sender.MakeStaticBuilder();
 
-          intake_roller->SerializePosition(
-              superstructure_can_builder->add_intake_roller(),
-              control_loops::drivetrain::kHighOutputRatio);
           intake_pivot->SerializePosition(
               superstructure_can_builder->add_intake_pivot(),
-              control_loops::drivetrain::kHighOutputRatio);
+              control_loops::superstructure::intake_pivot::kOutputRatio);
+          turret->SerializePosition(
+              superstructure_can_builder->add_turret(),
+              control_loops::superstructure::turret::kOutputRatio);
+          altitude->SerializePosition(
+              superstructure_can_builder->add_altitude(),
+              control_loops::superstructure::altitude::kOutputRatio);
+          catapult_one->SerializePosition(
+              superstructure_can_builder->add_catapult_one(),
+              control_loops::superstructure::catapult::kOutputRatio);
+          catapult_two->SerializePosition(
+              superstructure_can_builder->add_catapult_two(),
+              control_loops::superstructure::catapult::kOutputRatio);
+          extend->SerializePosition(superstructure_can_builder->add_extend(),
+                                    superstructure::extend::kOutputRatio);
+
+          superstructure_can_builder->set_timestamp(
+              intake_pivot->GetTimestamp());
+          superstructure_can_builder->set_status(static_cast<int>(status));
+          superstructure_can_builder.CheckOk(superstructure_can_builder.Send());
+        });
+
+    aos::Sender<y2024::control_loops::superstructure::CANPositionStatic>
+        superstructure_rio_position_sender =
+            rio_sensor_reader_event_loop.MakeSender<
+                y2024::control_loops::superstructure::CANPositionStatic>(
+                "/superstructure/rio");
+
+    frc971::wpilib::CANSensorReader rio_can_sensor_reader(
+        &rio_sensor_reader_event_loop, std::move(rio_signal_registry),
+        rio_talonfxs,
+        [&intake_roller, &transfer_roller, &climber, &extend_roller,
+         &retention_roller, &superstructure_rio_position_sender](
+            ctre::phoenix::StatusCode status) {
+          aos::Sender<y2024::control_loops::superstructure::CANPositionStatic>::
+              StaticBuilder superstructure_can_builder =
+                  superstructure_rio_position_sender.MakeStaticBuilder();
+
+          intake_roller->SerializePosition(
+              superstructure_can_builder->add_intake_roller(),
+              constants::Values::kIntakeRollerOutputRatio);
           transfer_roller->SerializePosition(
               superstructure_can_builder->add_transfer_roller(),
-              control_loops::drivetrain::kHighOutputRatio);
-          climber->SerializePosition(
-              superstructure_can_builder->add_climber(),
-              control_loops::drivetrain::kHighOutputRatio);
+              constants::Values::kIntakeRollerOutputRatio);
+          climber->SerializePosition(superstructure_can_builder->add_climber(),
+                                     superstructure::climber::kOutputRatio);
+          extend_roller->SerializePosition(
+              superstructure_can_builder->add_extend_roller(),
+              constants::Values::kExtendRollerOutputRatio);
+          retention_roller->SerializePosition(
+              superstructure_can_builder->add_retention_roller(), 1.0);
 
           superstructure_can_builder->set_timestamp(
               intake_roller->GetTimestamp());
           superstructure_can_builder->set_status(static_cast<int>(status));
           superstructure_can_builder.CheckOk(superstructure_can_builder.Send());
-        });
+        },
+        frc971::wpilib::CANSensorReader::SignalSync::kNoSync);
 
     AddLoop(&can_sensor_reader_event_loop);
+    AddLoop(&rio_sensor_reader_event_loop);
 
     // Thread 5.
     ::aos::ShmEventLoop can_output_event_loop(&config.message());
@@ -447,21 +673,44 @@
                    &talonfx_map) {
               talonfx_map.find("intake_pivot")
                   ->second->WriteVoltage(output.intake_pivot_voltage());
+              talonfx_map.find("altitude")
+                  ->second->WriteVoltage(output.altitude_voltage());
+              talonfx_map.find("catapult_one")
+                  ->second->WriteVoltage(output.catapult_voltage());
+              talonfx_map.find("catapult_two")
+                  ->second->WriteVoltage(output.catapult_voltage());
+              talonfx_map.find("turret")->second->WriteVoltage(
+                  output.turret_voltage());
+              talonfx_map.find("climber")->second->WriteVoltage(
+                  output.climber_voltage());
+              talonfx_map.find("extend")->second->WriteVoltage(
+                  output.extend_voltage());
               talonfx_map.find("intake_roller")
                   ->second->WriteVoltage(output.intake_roller_voltage());
               talonfx_map.find("transfer_roller")
                   ->second->WriteVoltage(output.transfer_roller_voltage());
-              talonfx_map.find("climber")->second->WriteVoltage(
-                  output.climber_voltage());
+              talonfx_map.find("extend_roller")
+                  ->second->WriteVoltage(output.extend_roller_voltage());
+              talonfx_map.find("retention_roller")
+                  ->second->WriteCurrent(
+                      output.retention_roller_stator_current_limit(),
+                      output.retention_roller_voltage());
             });
 
     can_drivetrain_writer.set_talonfxs({right_front, right_back},
                                        {left_front, left_back});
 
     can_superstructure_writer.add_talonfx("intake_pivot", intake_pivot);
+    can_superstructure_writer.add_talonfx("altitude", altitude);
+    can_superstructure_writer.add_talonfx("catapult_one", catapult_one);
+    can_superstructure_writer.add_talonfx("catapult_two", catapult_two);
+    can_superstructure_writer.add_talonfx("turret", turret);
+    can_superstructure_writer.add_talonfx("climber", climber);
+    can_superstructure_writer.add_talonfx("extend", extend);
     can_superstructure_writer.add_talonfx("intake_roller", intake_roller);
     can_superstructure_writer.add_talonfx("transfer_roller", transfer_roller);
-    can_superstructure_writer.add_talonfx("climber", climber);
+    can_superstructure_writer.add_talonfx("extend_roller", extend_roller);
+    can_superstructure_writer.add_talonfx("retention_roller", retention_roller);
 
     can_output_event_loop.MakeWatcher(
         "/roborio", [&can_drivetrain_writer, &can_superstructure_writer](
@@ -472,6 +721,14 @@
 
     AddLoop(&can_output_event_loop);
 
+    ::aos::ShmEventLoop pwm_event_loop(&config.message());
+    SuperstructurePWMWriter superstructure_pwm_writer(&pwm_event_loop);
+    superstructure_pwm_writer.set_catapult_kraken_one(
+        make_unique<frc::TalonFX>(0));
+    superstructure_pwm_writer.set_catapult_kraken_two(
+        make_unique<frc::TalonFX>(1));
+
+    AddLoop(&pwm_event_loop);
     // Thread 6
 
     RunLoops();
diff --git a/y2024/www/field.html b/y2024/www/field.html
index 8c3b291..cfb8778 100644
--- a/y2024/www/field.html
+++ b/y2024/www/field.html
@@ -1,15 +1,30 @@
 <html>
-  <head>
-    <script src="field_main_bundle.min.js" defer></script>
-    <link rel="stylesheet" href="styles.css">
-  </head>
-  <body>
-    <div id="field"> </div>
-    <div id="legend"> </div>
-    <div id="readouts">
+
+<head>
+  <script src="field_main_bundle.min.js" defer></script>
+  <link rel="stylesheet" href="styles.css">
+</head>
+
+<body>
+  <div style="display: grid;
+  grid-template-columns: auto auto auto; gap: 5px;">
+    <div>
+      <div id="field"> </div>
+      <div id="legend"> </div>
+      <div id="vision_readouts">
+      </div>
+      <div id="message_bridge_status">
+        <div>
+          <div>Node</div>
+          <div>Client</div>
+          <div>Server</div>
+        </div>
+      </div>
+    </div>
+    <div>
       <table>
         <tr>
-          <th colspan="2">Robot State</th>
+          <th colspan="2" style="text-align: center;">Robot State</th>
         </tr>
         <tr>
           <td>X</td>
@@ -24,64 +39,233 @@
           <td id="theta"> NA </td>
         </tr>
       </table>
-
       <table>
         <tr>
-          <th colspan="2">Images</th>
+          <th colspan="2" style="text-align: center;">Images</th>
         </tr>
         <tr>
           <td> Images Accepted </td>
           <td id="images_accepted"> NA </td>
         </tr>
       </table>
-
       <table>
         <tr>
-          <th colspan="2">Superstructure</th>
-    </tr>
-    <!-- TODO: Add superstructure state -->
-  </table>
-  <table>
-    <tr>
-      <th colspan="2">Game Piece</th>
-    </tr>
-    <tr>
-      <td>Game Piece Held</td>
-      <td id="game_piece"> NA </td>
-    </tr>
-    <tr>
-      <td>Game Piece Position (+ = left, 0 = empty)</td>
-      <td id="game_piece_position"> NA </td>
-    </tr>
-  </table>
+          <th colspan="2" style="text-align: center;">Superstructure States</th>
+        </tr>
+        <tr>
+          <td style="font-weight: bold;">Superstructure State</td>
+          <td id="superstructure_state" style="font-weight: bold;"> NA </td>
+        </tr>
+        <tr>
+          <td style="font-weight: bold;">Note Goal</td>
+          <td id="uncompleted_note_goal" style="font-weight: bold;"> NA </td>
+        </tr>
+        <tr>
+          <td>Catapult State</td>
+          <td id="catapult_state"> NA </td>
+        </tr>
+        <tr>
+          <td>Intake Roller State</td>
+          <td id="intake_roller_state"> NA </td>
+        </tr>
+        <tr>
+          <td>Transfer Roller State</td>
+          <td id="transfer_roller_state"> NA </td>
+        </tr>
+        <tr>
+          <td>Extend State</td>
+          <td id="extend_state"> NA </td>
+        </tr>
+        <tr>
+          <td>Extend Roller State</td>
+          <td id="extend_roller_state"> NA </td>
+        </tr>
+        <tr>
+          <th colspan="2">Beambreaks</th>
+        </tr>
+        <tr>
+          <td>Extend Beambreak</td>
+          <td id="extend_beambreak">FALSE</td>
+        </tr>
+        <tr>
+          <td>Catapult Beambreak</td>
+          <td id="catapult_beambreak">FALSE</td>
+        </tr>
+        <tr>
+          <th colspan="2">Subsytems At Position</th>
+        </tr>
+        <tr>
+          <td>Extend At Retracted Position</td>
+          <td id="extend_at_retracted">FALSE</td>
+        </tr>
+        <tr>
+          <td>Extend Ready For Transfer</td>
+          <td id="extend_ready_for_transfer">FALSE</td>
+        </tr>
+        <tr>
+          <td>Extend Ready to Transfer to Catapult</td>
+          <td id="extend_ready_for_catapult_transfer">FALSE</td>
+        </tr>
+        <tr>
+          <td>Turret at Loading Position</td>
+          <td id="turret_ready_for_load">FALSE</td>
+        </tr>
+        <tr>
+          <td>Altitude at Loading Position</td>
+          <td id="altitude_ready_for_load">FALSE</td>
+        </tr>
+      </table>
+      <table>
+        <tr>
+          <th colspan="2" style="text-align: center;">Aimer</th>
+        </tr>
+        <tr>
+          <td>Turret Position</td>
+          <td id="turret_position"> NA </td>
+        </tr>
+        <tr>
+          <td>Turret Velocity </td>
+          <td id="turret_velocity"> NA </td>
+        </tr>
+        <tr>
+          <td>Target Distance</td>
+          <td id="target_distance"> NA </td>
+        </tr>
+        <tr>
+          <td>Shot Distance</td>
+          <td id="shot_distance"> NA </td>
+        </tr>
+      </table>
 
-  <h3>Zeroing Faults:</h3>
-  <p id="zeroing_faults"> NA </p>
-  </div>
-  <div id="middle_readouts">
-    <div id="vision_readouts">
+      <h3>Zeroing Faults:</h3>
+      <p id="zeroing_faults"> NA </p>
     </div>
-    <div id="message_bridge_status">
-      <div>
-        <div>Node</div>
-        <div>Client</div>
-        <div>Server</div>
-      </div>
+    <div>
+      <table>
+        <tr>
+          <th colspan="2" style="text-align: center;">Subsystems</th>
+        </tr>
+        <tr>
+          <th colspan="2">Intake Pivot</th>
+        </tr>
+        <tr>
+          <td>Position</td>
+          <td id="intake_pivot"> NA </td>
+        </tr>
+        <tr>
+          <td>Absolute Position</td>
+          <td id="intake_pivot_abs"> NA </td>
+        </tr>
+        <tr>
+          <th colspan="2">Climber</th>
+        </tr>
+        <tr>
+          <td>Position</td>
+          <td id="climber"> NA </td>
+        </tr>
+        <tr>
+          <td>Absolute Position</td>
+          <td id="climber_abs"> NA </td>
+        </tr>
+        <tr>
+          <td>Pot Position</td>
+          <td id="climber_pot"> NA </td>
+        </tr>
+        <tr>
+          <th colspan="2">Extend</th>
+        </tr>
+        <tr>
+          <td>Position</td>
+          <td id="extend"> NA </td>
+        </tr>
+        <tr>
+          <td>Absolute Position</td>
+          <td id="extend_abs"> NA </td>
+        </tr>
+        <tr>
+          <td>Pot Position</td>
+          <td id="extend_pot"> NA </td>
+        </tr>
+        <tr>
+          <th colspan="2">Turret</th>
+        </tr>
+        <tr>
+          <td>Position</td>
+          <td id="turret"> NA </td>
+        </tr>
+        <tr>
+          <td>Absolute Position</td>
+          <td id="turret_abs"> NA </td>
+        </tr>
+        <tr>
+          <td>Pot Position</td>
+          <td id="turret_pot"> NA </td>
+        </tr>
+        <th colspan="2">Catapult</th>
+        </tr>
+        <tr>
+          <td>Catapult State</td>
+          <td id="catapult_state"> NA </td>
+        </tr>
+        <tr>
+          <td>Position</td>
+          <td id="catapult"> NA </td>
+        </tr>
+        <tr>
+          <td>Absolute Position</td>
+          <td id="catapult_abs"> NA </td>
+        </tr>
+        <tr>
+          <td>Pot Position</td>
+          <td id="catapult_pot"> NA </td>
+        </tr>
+        <tr>
+          <th colspan="2">Altitude</th>
+        </tr>
+        <tr>
+          <td>Position</td>
+          <td id="altitude"> NA </td>
+        </tr>
+        <tr>
+          <td>Absolute Position</td>
+          <td id="altitude_abs"> NA </td>
+        </tr>
+        <tr>
+          <td>Pot Position</td>
+          <td id="altitude_pot"> NA </td>
+        </tr>
+      </table>
+      <table>
+        <tr>
+          <th colspan="2" style="text-align: center;"> Drivetrain Encoder Positions </th>
+        </tr>
+        <tr>
+          <td> Left Encoder Position</td>
+          <td id="left_drivetrain_encoder"> NA </td>
+        </tr>
+        <tr>
+          <td> Right Encoder Position</td>
+          <td id="right_drivetrain_encoder"> NA </td>
+        </tr>
+        <tr>
+          <td> Right Front Falcon CAN Position</td>
+          <td id="falcon_right_front"> NA </td>
+        </tr>
+        <tr>
+          <td> Right Back Falcon CAN Position</td>
+          <td id="falcon_right_back"> NA </td>
+        </tr>
+        <tr>
+          <td> Left Front Falcon CAN Position</td>
+          <td id="falcon_left_front"> NA </td>
+        </tr>
+        <tr>
+          <td> Left Back Falcon CAN Position</td>
+          <td id="falcon_left_back"> NA </td>
+        </tr>
+      </table>
     </div>
   </div>
-  <table>
-      <tr>
-        <th colspan="2"> Drivetrain Encoder Positions </th>
-      </tr>
-      <tr>
-        <td> Left Encoder Position</td>
-        <td id="left_drivetrain_encoder"> NA </td>
-      </tr>
-      <tr>
-        <td> Right Encoder Position</td>
-        <td id="right_drivetrain_encoder"> NA </td>
-      </tr>
-  </table>
-  </body>
-</html>
+</body>
 
+</html>
\ No newline at end of file
diff --git a/y2024/www/field_handler.ts b/y2024/www/field_handler.ts
index f383c08..949b34b 100644
--- a/y2024/www/field_handler.ts
+++ b/y2024/www/field_handler.ts
@@ -6,6 +6,7 @@
 import {Position as DrivetrainPosition} from '../../frc971/control_loops/drivetrain/drivetrain_position_generated'
 import {CANPosition as DrivetrainCANPosition} from '../../frc971/control_loops/drivetrain/drivetrain_can_position_generated'
 import {Status as DrivetrainStatus} from '../../frc971/control_loops/drivetrain/drivetrain_status_generated'
+import {SuperstructureState, IntakeRollerStatus, CatapultState, TransferRollerStatus, ExtendRollerStatus, ExtendStatus, NoteStatus, Status as SuperstructureStatus} from '../control_loops/superstructure/superstructure_status_generated'
 import {LocalizerOutput} from '../../frc971/control_loops/drivetrain/localization/localizer_output_generated'
 import {TargetMap} from '../../frc971/vision/target_map_generated'
 
@@ -21,11 +22,171 @@
 
 export class FieldHandler {
   private canvas = document.createElement('canvas');
+  private localizerOutput: LocalizerOutput|null = null;
+  private drivetrainStatus: DrivetrainStatus|null = null;
+  private drivetrainPosition: DrivetrainPosition|null = null;
+  private drivetrainCANPosition: DrivetrainCANPosition|null = null;
+  private superstructureStatus: SuperstructureStatus|null = null;
+
+  private x: HTMLElement = (document.getElementById('x') as HTMLElement);
+  private y: HTMLElement = (document.getElementById('y') as HTMLElement);
+  private theta: HTMLElement =
+      (document.getElementById('theta') as HTMLElement);
+
   private fieldImage: HTMLImageElement = new Image();
+
+  private zeroingFaults: HTMLElement =
+      (document.getElementById('zeroing_faults') as HTMLElement);
+
+  private superstructureState: HTMLElement =
+    (document.getElementById('superstructure_state') as HTMLElement);
+
+  private intakeRollerState: HTMLElement =
+    (document.getElementById('intake_roller_state') as HTMLElement);
+  private transferRollerState: HTMLElement =
+    (document.getElementById('transfer_roller_state') as HTMLElement);
+  private extendState: HTMLElement =
+    (document.getElementById('extend_state') as HTMLElement);
+  private extendRollerState: HTMLElement =
+    (document.getElementById('extend_roller_state') as HTMLElement);
+  private catapultState: HTMLElement =
+    (document.getElementById('catapult_state') as HTMLElement);
+  private uncompletedNoteGoal: HTMLElement =
+  (document.getElementById('uncompleted_note_goal') as HTMLElement);
+
+  private extend_beambreak: HTMLElement =
+  (document.getElementById('extend_beambreak') as HTMLElement);
+  private catapult_beambreak: HTMLElement =
+  (document.getElementById('catapult_beambreak') as HTMLElement);
+
+  private extend_at_retracted: HTMLElement =
+  (document.getElementById('extend_at_retracted') as HTMLElement);
+  private extend_ready_for_transfer: HTMLElement =
+  (document.getElementById('extend_ready_for_transfer') as HTMLElement);
+  private extend_ready_for_catapult_transfer: HTMLElement =
+  (document.getElementById('extend_ready_for_catapult_transfer') as HTMLElement);
+  private turret_ready_for_load: HTMLElement =
+  (document.getElementById('turret_ready_for_load') as HTMLElement);
+  private altitude_ready_for_load: HTMLElement =
+  (document.getElementById('altitude_ready_for_load') as HTMLElement);
+
+
+  private intakePivot: HTMLElement =
+    (document.getElementById('intake_pivot') as HTMLElement);
+  private intakePivotAbs: HTMLElement =
+    (document.getElementById('intake_pivot_abs') as HTMLElement);
+
+  private climber: HTMLElement =
+    (document.getElementById('climber') as HTMLElement);
+  private climberAbs: HTMLElement =
+    (document.getElementById('climber_abs') as HTMLElement);
+  private climberPot: HTMLElement =
+    (document.getElementById('climber_pot') as HTMLElement);
+
+  private extend: HTMLElement =
+    (document.getElementById('extend') as HTMLElement);
+  private extendAbs: HTMLElement =
+    (document.getElementById('extend_abs') as HTMLElement);
+  private extendPot: HTMLElement =
+    (document.getElementById('extend_pot') as HTMLElement);
+
+  private turret: HTMLElement =
+    (document.getElementById('turret') as HTMLElement);
+  private turretAbs: HTMLElement =
+    (document.getElementById('turret_abs') as HTMLElement);
+  private turretPot: HTMLElement =
+    (document.getElementById('turret_pot') as HTMLElement);
+
+  private catapult: HTMLElement =
+    (document.getElementById('catapult') as HTMLElement);
+  private catapultAbs: HTMLElement =
+    (document.getElementById('catapult_abs') as HTMLElement);
+  private catapultPot: HTMLElement =
+    (document.getElementById('catapult_pot') as HTMLElement);
+
+  private altitude: HTMLElement =
+    (document.getElementById('altitude') as HTMLElement);
+  private altitudeAbs: HTMLElement =
+    (document.getElementById('altitude_abs') as HTMLElement);
+  private altitudePot: HTMLElement =
+    (document.getElementById('altitude_pot') as HTMLElement);
+
+  private turret_position: HTMLElement =
+    (document.getElementById('turret_position') as HTMLElement);
+  private turret_velocity: HTMLElement =
+    (document.getElementById('turret_velocity') as HTMLElement);
+  private target_distance: HTMLElement =
+    (document.getElementById('target_distance') as HTMLElement);
+  private shot_distance: HTMLElement =
+    (document.getElementById('shot_distance') as HTMLElement);
+
+  private leftDrivetrainEncoder: HTMLElement =
+      (document.getElementById('left_drivetrain_encoder') as HTMLElement);
+  private rightDrivetrainEncoder: HTMLElement =
+      (document.getElementById('right_drivetrain_encoder') as HTMLElement);
+  private falconRightFrontPosition: HTMLElement =
+      (document.getElementById('falcon_right_front') as HTMLElement);
+  private falconRightBackPosition: HTMLElement =
+      (document.getElementById('falcon_right_back') as HTMLElement);
+  private falconLeftFrontPosition: HTMLElement =
+      (document.getElementById('falcon_left_front') as HTMLElement);
+  private falconLeftBackPosition: HTMLElement =
+      (document.getElementById('falcon_left_back') as HTMLElement);
+
   constructor(private readonly connection: Connection) {
     (document.getElementById('field') as HTMLElement).appendChild(this.canvas);
 
     this.fieldImage.src = '2024.png';
+
+    this.connection.addConfigHandler(() => {
+
+      this.connection.addHandler(
+        '/drivetrain', 'frc971.control_loops.drivetrain.Status', (data) => {
+          this.handleDrivetrainStatus(data);
+        });
+      this.connection.addHandler(
+        '/drivetrain', 'frc971.control_loops.drivetrain.Position', (data) => {
+          this.handleDrivetrainPosition(data);
+        });
+      this.connection.addHandler(
+        '/drivetrain', 'frc971.control_loops.drivetrain.CANPosition', (data) => {
+          this.handleDrivetrainCANPosition(data);
+        });
+      this.connection.addHandler(
+        '/localizer', 'frc971.controls.LocalizerOutput', (data) => {
+          this.handleLocalizerOutput(data);
+        });
+      this.connection.addHandler(
+        '/superstructure', "y2024.control_loops.superstructure.Status",
+        (data) => {
+          this.handleSuperstructureStatus(data)
+          });
+      });
+  }
+
+  private handleDrivetrainStatus(data: Uint8Array): void {
+    const fbBuffer = new ByteBuffer(data);
+    this.drivetrainStatus = DrivetrainStatus.getRootAsStatus(fbBuffer);
+  }
+
+  private handleDrivetrainPosition(data: Uint8Array): void {
+    const fbBuffer = new ByteBuffer(data);
+    this.drivetrainPosition = DrivetrainPosition.getRootAsPosition(fbBuffer);
+  }
+
+  private handleDrivetrainCANPosition(data: Uint8Array): void {
+    const fbBuffer = new ByteBuffer(data);
+    this.drivetrainCANPosition = DrivetrainCANPosition.getRootAsCANPosition(fbBuffer);
+  }
+
+  private handleLocalizerOutput(data: Uint8Array): void {
+    const fbBuffer = new ByteBuffer(data);
+    this.localizerOutput = LocalizerOutput.getRootAsLocalizerOutput(fbBuffer);
+  }
+
+  private handleSuperstructureStatus(data: Uint8Array): void {
+	  const fbBuffer = new ByteBuffer(data);
+	  this.superstructureStatus = SuperstructureStatus.getRootAsStatus(fbBuffer);
   }
 
   drawField(): void {
@@ -38,10 +199,345 @@
     ctx.restore();
   }
 
+  drawCamera(x: number, y: number, theta: number, color: string = 'blue'):
+  void {
+    const ctx = this.canvas.getContext('2d');
+    ctx.save();
+    ctx.translate(x, y);
+    ctx.rotate(theta);
+    ctx.strokeStyle = color;
+    ctx.beginPath();
+    ctx.moveTo(0.5, 0.5);
+    ctx.lineTo(0, 0);
+    ctx.lineTo(0.5, -0.5);
+    ctx.stroke();
+    ctx.beginPath();
+    ctx.arc(0, 0, 0.25, -Math.PI / 4, Math.PI / 4);
+    ctx.stroke();
+    ctx.restore();
+  }
+
+  drawRobot(
+    x: number, y: number, theta: number, color: string = 'blue',
+    dashed: boolean = false): void {
+  const ctx = this.canvas.getContext('2d');
+  ctx.save();
+  ctx.translate(x, y);
+  ctx.rotate(theta);
+  ctx.strokeStyle = color;
+  ctx.lineWidth = ROBOT_WIDTH / 10.0;
+  if (dashed) {
+    ctx.setLineDash([0.05, 0.05]);
+  } else {
+    // Empty array = solid line.
+    ctx.setLineDash([]);
+  }
+  ctx.rect(-ROBOT_LENGTH / 2, -ROBOT_WIDTH / 2, ROBOT_LENGTH, ROBOT_WIDTH);
+  ctx.stroke();
+
+  // Draw line indicating which direction is forwards on the robot.
+  ctx.beginPath();
+  ctx.moveTo(0, 0);
+  ctx.lineTo(ROBOT_LENGTH / 2.0, 0);
+  ctx.stroke();
+
+  ctx.restore();
+}
+
+  setZeroing(div: HTMLElement): void {
+    div.innerHTML = 'zeroing';
+    div.classList.remove('faulted');
+    div.classList.add('zeroing');
+    div.classList.remove('near');
+  }
+
+  setEstopped(div: HTMLElement): void {
+    div.innerHTML = 'estopped';
+    div.classList.add('faulted');
+    div.classList.remove('zeroing');
+    div.classList.remove('near');
+  }
+
+  setTargetValue(
+      div: HTMLElement, target: number, val: number, tolerance: number): void {
+    div.innerHTML = val.toFixed(4);
+    div.classList.remove('faulted');
+    div.classList.remove('zeroing');
+    if (Math.abs(target - val) < tolerance) {
+      div.classList.add('near');
+    } else {
+      div.classList.remove('near');
+    }
+  }
+
+  setValue(div: HTMLElement, val: number): void {
+    div.innerHTML = val.toFixed(4);
+    div.classList.remove('faulted');
+    div.classList.remove('zeroing');
+    div.classList.remove('near');
+  }
+
+  setBoolean(div: HTMLElement, triggered: boolean): void {
+    div.innerHTML = ((triggered) ? "TRUE" : "FALSE")
+    if (triggered) {
+      div.classList.remove('false');
+      div.classList.add('true');
+    } else {
+      div.classList.remove('true');
+      div.classList.add('false');
+    }
+  }
+
   draw(): void {
     this.reset();
     this.drawField();
 
+    if (this.superstructureStatus) {
+      this.superstructureState.innerHTML =
+        SuperstructureState[this.superstructureStatus.state()];
+
+      this.intakeRollerState.innerHTML =
+        IntakeRollerStatus[this.superstructureStatus.intakeRoller()];
+      this.transferRollerState.innerHTML =
+        TransferRollerStatus[this.superstructureStatus.transferRoller()];
+      this.extendState.innerHTML =
+        ExtendStatus[this.superstructureStatus.extendStatus()];
+      this.extendRollerState.innerHTML =
+        ExtendRollerStatus[this.superstructureStatus.extendRoller()];
+      this.catapultState.innerHTML =
+        CatapultState[this.superstructureStatus.shooter().catapultState()];
+      this.uncompletedNoteGoal.innerHTML =
+        NoteStatus[this.superstructureStatus.uncompletedNoteGoal()];
+
+      this.setBoolean(this.extend_beambreak, this.superstructureStatus.extendBeambreak());
+
+      this.setBoolean(this.catapult_beambreak, this.superstructureStatus.catapultBeambreak());
+
+      this.setBoolean(this.extend_ready_for_transfer, this.superstructureStatus.extendReadyForTransfer());
+
+      this.setBoolean(this.extend_at_retracted, this.superstructureStatus.extendAtRetracted());
+
+      this.setBoolean(this.turret_ready_for_load, this.superstructureStatus.turretReadyForLoad());
+
+      this.setBoolean(this.altitude_ready_for_load, this.superstructureStatus.altitudeReadyForLoad());
+
+      this.setBoolean(this.extend_ready_for_catapult_transfer, this.superstructureStatus.extendReadyForCatapultTransfer());
+
+      if (this.superstructureStatus.shooter() &&
+          this.superstructureStatus.shooter().aimer()) {
+        this.turret_position.innerHTML = this.superstructureStatus.shooter()
+                                             .aimer()
+                                             .turretPosition()
+                                             .toString();
+        this.turret_velocity.innerHTML = this.superstructureStatus.shooter()
+                                             .aimer()
+                                             .turretVelocity()
+                                             .toString();
+        this.target_distance.innerHTML = this.superstructureStatus.shooter()
+                                             .aimer()
+                                             .targetDistance()
+                                             .toString();
+        this.shot_distance.innerHTML = this.superstructureStatus.shooter()
+                                           .aimer()
+                                           .shotDistance()
+                                           .toString();
+      }
+
+      if (!this.superstructureStatus.intakePivot() ||
+          !this.superstructureStatus.intakePivot().zeroed()) {
+        this.setZeroing(this.intakePivot);
+      } else if (this.superstructureStatus.intakePivot().estopped()) {
+        this.setEstopped(this.intakePivot);
+      } else {
+        this.setTargetValue(
+            this.intakePivot,
+            this.superstructureStatus.intakePivot().unprofiledGoalPosition(),
+            this.superstructureStatus.intakePivot().estimatorState().position(),
+            1e-3);
+      }
+
+      this.intakePivotAbs.innerHTML = this.superstructureStatus.intakePivot().estimatorState().absolutePosition().toString();
+
+      if (!this.superstructureStatus.climber() ||
+          !this.superstructureStatus.climber().zeroed()) {
+        this.setZeroing(this.climber);
+      } else if (this.superstructureStatus.climber().estopped()) {
+        this.setEstopped(this.climber);
+      } else {
+        this.setTargetValue(
+            this.climber,
+            this.superstructureStatus.climber().unprofiledGoalPosition(),
+            this.superstructureStatus.climber().estimatorState().position(),
+            1e-3);
+      }
+
+      this.climberAbs.innerHTML = this.superstructureStatus.climber().estimatorState().absolutePosition().toString();
+      this.climberPot.innerHTML = this.superstructureStatus.climber().estimatorState().potPosition().toString();
+
+      if (!this.superstructureStatus.extend() ||
+          !this.superstructureStatus.extend().zeroed()) {
+        this.setZeroing(this.extend);
+      } else if (this.superstructureStatus.extend().estopped()) {
+        this.setEstopped(this.extend);
+      } else {
+        this.setTargetValue(
+            this.extend,
+            this.superstructureStatus.extend().unprofiledGoalPosition(),
+            this.superstructureStatus.extend().estimatorState().position(),
+            1e-3);
+      }
+
+      this.extendAbs.innerHTML = this.superstructureStatus.extend().estimatorState().absolutePosition().toString();
+      this.extendPot.innerHTML = this.superstructureStatus.extend().estimatorState().potPosition().toString();
+
+      if (!this.superstructureStatus.shooter().turret() ||
+          !this.superstructureStatus.shooter().turret().zeroed()) {
+        this.setZeroing(this.turret);
+      } else if (this.superstructureStatus.shooter().turret().estopped()) {
+        this.setEstopped(this.turret);
+      } else {
+        this.setTargetValue(
+            this.turret,
+            this.superstructureStatus.shooter().turret().unprofiledGoalPosition(),
+            this.superstructureStatus.shooter().turret().estimatorState().position(),
+            1e-3);
+      }
+
+      this.turretAbs.innerHTML = this.superstructureStatus.shooter().turret().estimatorState().absolutePosition().toString();
+      this.turretPot.innerHTML = this.superstructureStatus.shooter().turret().estimatorState().potPosition().toString();
+
+      if (!this.superstructureStatus.shooter().catapult() ||
+          !this.superstructureStatus.shooter().catapult().zeroed()) {
+        this.setZeroing(this.catapult);
+      } else if (this.superstructureStatus.shooter().catapult().estopped()) {
+        this.setEstopped(this.catapult);
+      } else {
+        this.setTargetValue(
+            this.catapult,
+            this.superstructureStatus.shooter().catapult().unprofiledGoalPosition(),
+            this.superstructureStatus.shooter().catapult().estimatorState().position(),
+            1e-3);
+      }
+
+      this.catapultAbs.innerHTML = this.superstructureStatus.shooter().catapult().estimatorState().absolutePosition().toString();
+      this.catapultPot.innerHTML = this.superstructureStatus.shooter().catapult().estimatorState().potPosition().toString();
+
+      if (!this.superstructureStatus.shooter().altitude() ||
+          !this.superstructureStatus.shooter().altitude().zeroed()) {
+        this.setZeroing(this.altitude);
+      } else if (this.superstructureStatus.shooter().altitude().estopped()) {
+        this.setEstopped(this.altitude);
+      } else {
+        this.setTargetValue(
+            this.altitude,
+            this.superstructureStatus.shooter().altitude().unprofiledGoalPosition(),
+            this.superstructureStatus.shooter().altitude().estimatorState().position(),
+            1e-3);
+      }
+
+      this.altitudeAbs.innerHTML = this.superstructureStatus.shooter().altitude().estimatorState().absolutePosition().toString();
+      this.altitudePot.innerHTML = this.superstructureStatus.shooter().altitude().estimatorState().potPosition().toString();
+
+      let zeroingErrors: string = 'Intake Pivot Errors:' +
+          '<br/>';
+      for (let i = 0; i < this.superstructureStatus.intakePivot()
+                              .estimatorState()
+                              .errorsLength();
+           i++) {
+        zeroingErrors += ZeroingError[this.superstructureStatus.intakePivot()
+                                          .estimatorState()
+                                          .errors(i)] +
+            '<br/>';
+      }
+      zeroingErrors += '<br/>' +
+          'Climber Errors:' +
+          '<br/>';
+      for (let i = 0; i < this.superstructureStatus.climber().estimatorState().errorsLength();
+           i++) {
+        zeroingErrors += ZeroingError[this.superstructureStatus.climber().estimatorState().errors(i)] +
+            '<br/>';
+      }
+      zeroingErrors += '<br/>' +
+          'Extend Errors:' +
+          '<br/>';
+      for (let i = 0; i < this.superstructureStatus.extend().estimatorState().errorsLength();
+           i++) {
+        zeroingErrors += ZeroingError[this.superstructureStatus.extend().estimatorState().errors(i)] +
+            '<br/>';
+      }
+      zeroingErrors += '<br/>' +
+          'Turret Errors:' +
+          '<br/>';
+      for (let i = 0; i < this.superstructureStatus.shooter().turret().estimatorState().errorsLength();
+           i++) {
+        zeroingErrors += ZeroingError[this.superstructureStatus.shooter().turret().estimatorState().errors(i)] +
+            '<br/>';
+      }
+      zeroingErrors += '<br/>' +
+          'Catapult Errors:' +
+          '<br/>';
+      for (let i = 0; i < this.superstructureStatus.shooter().catapult().estimatorState().errorsLength();
+           i++) {
+        zeroingErrors += ZeroingError[this.superstructureStatus.shooter().catapult().estimatorState().errors(i)] +
+            '<br/>';
+      }
+      zeroingErrors += '<br/>' +
+          'Altitude Errors:' +
+          '<br/>';
+      for (let i = 0; i < this.superstructureStatus.shooter().altitude().estimatorState().errorsLength();
+           i++) {
+        zeroingErrors += ZeroingError[this.superstructureStatus.shooter().altitude().estimatorState().errors(i)] +
+            '<br/>';
+      }
+      this.zeroingFaults.innerHTML = zeroingErrors;
+    }
+
+    if (this.drivetrainPosition) {
+      this.leftDrivetrainEncoder.innerHTML =
+      this.drivetrainPosition.leftEncoder().toString();
+
+      this.rightDrivetrainEncoder.innerHTML =
+      this.drivetrainPosition.rightEncoder().toString();
+    }
+
+    if (this.drivetrainCANPosition) {
+      this.falconRightFrontPosition.innerHTML =
+      this.drivetrainCANPosition.talonfxs(0).position().toString();
+
+      this.falconRightBackPosition.innerHTML =
+      this.drivetrainCANPosition.talonfxs(1).position().toString();
+
+      this.falconLeftFrontPosition.innerHTML =
+      this.drivetrainCANPosition.talonfxs(2).position().toString();
+
+      this.falconLeftBackPosition.innerHTML =
+      this.drivetrainCANPosition.talonfxs(3).position().toString();
+    }
+
+    if (this.drivetrainStatus && this.drivetrainStatus.trajectoryLogging()) {
+      this.drawRobot(
+          this.drivetrainStatus.trajectoryLogging().x(),
+          this.drivetrainStatus.trajectoryLogging().y(),
+          this.drivetrainStatus.trajectoryLogging().theta(), '#000000FF',
+          false);
+    }
+
+    if (this.localizerOutput) {
+      if (!this.localizerOutput.zeroed()) {
+        this.setZeroing(this.x);
+        this.setZeroing(this.y);
+        this.setZeroing(this.theta);
+      } else {
+        this.setValue(this.x, this.localizerOutput.x());
+        this.setValue(this.y, this.localizerOutput.y());
+        this.setValue(this.theta, this.localizerOutput.theta());
+      }
+
+      this.drawRobot(
+          this.localizerOutput.x(), this.localizerOutput.y(),
+          this.localizerOutput.theta());
+    }
+
     window.requestAnimationFrame(() => this.draw());
   }
 
diff --git a/y2024/www/plotter.html b/y2024/www/plotter.html
index 629ceaa..86f5aa8 100644
--- a/y2024/www/plotter.html
+++ b/y2024/www/plotter.html
@@ -1,6 +1,7 @@
 <html>
   <head>
     <script src="plot_index_bundle.min.js" defer></script>
+    <link rel="stylesheet" href="styles.css">
   </head>
   <body>
   </body>
diff --git a/y2024/www/styles.css b/y2024/www/styles.css
index c2c44d2..39b7519 100644
--- a/y2024/www/styles.css
+++ b/y2024/www/styles.css
@@ -15,7 +15,6 @@
   float: right;
 }
 
-
 #legend {
   display: inline-block;
 }
@@ -25,13 +24,20 @@
   border-collapse: collapse;
   padding: 5px;
   margin: 10px;
+  table-layout: fixed;
+  width: 100%;
+  overflow: hidden;
 }
 
 th, td {
-  text-align: right;
+  text-align: left;
   width: 70px;
 }
 
+table:first-child {
+  text-align: center;
+}
+
 td:first-child {
   width: 150px;
 }
@@ -72,3 +78,73 @@
   padding: 5px;
   text-align: right;
 }
+
+.channel {
+  display: flex;
+  border-bottom: 1px solid;
+  font-size: 24px;
+}
+
+.aos_plot {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  box-sizing: border-box;
+}
+
+.aos_plot_text {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  pointer-events: none;
+}
+
+.aos_legend {
+  position: absolute;
+  z-index: 1;
+  pointer-events: none;
+}
+
+.aos_legend_line {
+  background: white;
+  padding: 2px;
+  border-radius: 2px;
+  margin-top: 3px;
+  margin-bottom: 3px;
+  font-size: 12;
+}
+
+.aos_legend_line>div {
+  display: inline-block;
+  vertical-align: middle;
+  margin-left: 5px;
+}
+.aos_legend_line>canvas {
+  vertical-align: middle;
+  pointer-events: all;
+}
+
+.aos_legend_line_hidden {
+  filter: contrast(0.75);
+}
+
+.aos_cpp_plot {
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  align-items: flex-start;
+}
+
+.aos_cpp_plot>div {
+  flex: 1;
+  width: 100%;
+}
+
+.true {
+  background-color: LightGreen;
+}
+
+.false {
+  background-color: red;
+}
diff --git a/y2024/y2024.json b/y2024/y2024.json
index c901ac6..b55aebe 100644
--- a/y2024/y2024.json
+++ b/y2024/y2024.json
@@ -15,6 +15,5 @@
     "y2024_roborio.json",
     "y2024_imu.json",
     "y2024_orin1.json",
-    "y2024_orin2.json"
   ]
 }
diff --git a/y2024/y2024_imu.json b/y2024/y2024_imu.json
index 5dd4710..155edb4 100644
--- a/y2024/y2024_imu.json
+++ b/y2024/y2024_imu.json
@@ -7,8 +7,7 @@
       "frequency": 100,
       "logger": "LOCAL_AND_REMOTE_LOGGER",
       "logger_nodes": [
-        "orin1",
-        "orin2"
+        "orin1"
       ],
       "destination_nodes": [
         {
@@ -19,16 +18,7 @@
           "timestamp_logger_nodes": [
             "imu"
           ]
-        },
-        {
-          "name": "orin2",
-          "priority": 5,
-          "time_to_live": 50000000,
-          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
-          "timestamp_logger_nodes": [
-            "imu"
-          ]
-        },
+        }
       ]
     },
     {
@@ -41,21 +31,12 @@
       "max_size": 200
     },
     {
-      "name": "/imu/aos/remote_timestamps/orin2/imu/aos/aos-JoystickState",
-      "type": "aos.message_bridge.RemoteMessage",
-      "source_node": "imu",
-      "logger": "NOT_LOGGED",
-      "frequency": 300,
-      "num_senders": 2,
-      "max_size": 200
-    },
-    {
       "name": "/imu/aos",
       "type": "aos.timing.Report",
       "source_node": "imu",
       "frequency": 50,
       "num_senders": 20,
-      "max_size": 4096
+      "max_size": 6184
     },
     {
       "name": "/imu/aos",
@@ -70,7 +51,7 @@
       "source_node": "imu",
       "frequency": 50,
       "num_senders": 20,
-      "max_size": 2048
+      "max_size": 4096
     },
     {
       "name": "/imu/aos",
@@ -150,6 +131,36 @@
         }
       ]
     },
+        {
+      "name": "/localizer",
+      "type": "frc971.controls.LocalizerOutput",
+      "source_node": "imu",
+      "frequency": 52,
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_nodes": [
+        "roborio"
+      ],
+      "destination_nodes": [
+        {
+          "name": "roborio",
+          "priority": 5,
+          "time_to_live": 5000000,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": [
+            "imu"
+          ]
+        }
+      ]
+    },
+    {
+      "name": "/imu/aos/remote_timestamps/roborio/localizer/frc971-controls-LocalizerOutput",
+      "type": "aos.message_bridge.RemoteMessage",
+      "source_node": "imu",
+      "logger": "NOT_LOGGED",
+      "frequency": 52,
+      "num_senders": 2,
+      "max_size": 200
+    },
     {
       "name": "/roborio/aos/remote_timestamps/imu/roborio/aos/aos-starter-StarterRpc",
       "type": "aos.message_bridge.RemoteMessage",
@@ -160,6 +171,106 @@
       "max_size": 200
     },
     {
+      "name": "/imu/camera0",
+      "type": "frc971.vision.CameraImage",
+      "source_node": "imu",
+      "channel_storage_duration": 1000000000,
+      "frequency": 65,
+      "max_size": 4752384,
+      "num_readers": 6,
+      "read_method": "PIN",
+      "num_senders": 18
+    },
+    {
+      "name": "/imu/camera1",
+      "type": "frc971.vision.CameraImage",
+      "source_node": "imu",
+      "channel_storage_duration": 1000000000,
+      "frequency": 65,
+      "max_size": 4752384,
+      "num_readers": 6,
+      "read_method": "PIN",
+      "num_senders": 18
+    },
+    {
+      "name": "/imu/camera0",
+      "type": "foxglove.CompressedImage",
+      "source_node": "imu",
+      "logger": "NOT_LOGGED",
+      "channel_storage_duration": 1000000000,
+      "frequency": 65,
+      "max_size": 622384
+    },
+    {
+      "name": "/imu/camera1",
+      "type": "foxglove.CompressedImage",
+      "source_node": "imu",
+      "logger": "NOT_LOGGED",
+      "channel_storage_duration": 1000000000,
+      "frequency": 65,
+      "max_size": 622384
+    },
+    {
+      "name": "/imu/camera0",
+      "type": "foxglove.ImageAnnotations",
+      "source_node": "imu",
+      "frequency": 65,
+      "max_size": 50000
+    },
+    {
+      "name": "/imu/camera1",
+      "type": "foxglove.ImageAnnotations",
+      "source_node": "imu",
+      "frequency": 65,
+      "max_size": 50000
+    },
+    {
+      "name": "/imu/camera0",
+      "type": "y2024.localizer.Visualization",
+      "source_node": "imu",
+      "frequency": 65,
+      "max_size": 50000
+    },
+    {
+      "name": "/imu/camera1",
+      "type": "y2024.localizer.Visualization",
+      "source_node": "imu",
+      "frequency": 65,
+      "max_size": 50000
+    },
+    {
+      "name": "/imu/camera0",
+      "type": "frc971.vision.TargetMap",
+      "source_node": "imu",
+      "frequency": 65,
+      "num_senders": 2,
+      "max_size": 1024
+    },
+    {
+      "name": "/imu/camera1",
+      "type": "frc971.vision.TargetMap",
+      "source_node": "imu",
+      "frequency": 65,
+      "num_senders": 2,
+      "max_size": 1024
+    },
+    {
+      "name": "/imu",
+      "type": "frc971.imu.DualImu",
+      "source_node": "imu",
+      "frequency": 1100,
+      "num_senders": 1,
+      "max_size": 496
+    },
+    {
+      "name": "/imu",
+      "type": "frc971.imu.CanTranslatorStatus",
+      "source_node": "imu",
+      "frequency": 1000,
+      "num_senders": 1,
+      "max_size": 200
+    },
+    {
       "name": "/can/cana",
       "type": "frc971.can_logger.CanFrame",
       "source_node": "imu",
@@ -192,6 +303,22 @@
       "num_senders": 2
     },
     {
+      "name": "/imu",
+      "type": "frc971.imu.DualImuBlenderStatus",
+      "source_node": "imu",
+      "frequency": 1100,
+      "num_senders": 1,
+      "max_size": 200
+    },
+    {
+      "name": "/localizer",
+      "type": "y2024.localizer.Status",
+      "source_node": "imu",
+      "frequency": 1600,
+      "max_size": 1600,
+      "num_senders": 2
+    },
+    {
       "name": "/imu/constants",
       "type": "y2024.Constants",
       "source_node": "imu",
@@ -203,22 +330,10 @@
   "applications": [
     {
       "name": "message_bridge_client",
-      "nodes": [
-        "imu"
-      ]
-    },
-    {
-      "name": "localizer",
-      "executable_name": "localizer_main",
-      "user": "pi",
-      "nodes": [
-        "imu"
-      ]
-    },
-    {
-      "name": "imu",
-      "executable_name": "imu_main",
-      "user": "pi",
+      "args": [
+        "--rt_priority=16",
+        "--sinit_max_init_timeout=5000"
+      ],
       "nodes": [
         "imu"
       ]
@@ -235,31 +350,54 @@
       "name": "message_bridge_server",
       "executable_name": "message_bridge_server",
       "user": "pi",
+      "args": [
+        "--rt_priority=16"
+      ],
       "nodes": [
         "imu"
       ]
     },
     {
-      "name": "localizer_logger",
-      "executable_name": "logger_main",
-      "args": [
-        "--logging_folder",
-        "",
-        "--snappy_compress",
-        "--rotate_every", "30.0"
-      ],
+      "name": "localizer",
+      "executable_name": "localizer_main",
       "user": "pi",
       "nodes": [
         "imu"
       ]
     },
     {
-      "name": "can_logger",
+      "name": "localizer_logger",
+      "executable_name": "localizer_logger",
+      "user": "pi",
+      "nodes": [
+        "imu"
+      ]
+    },
+    {
+      "name": "imu_can_logger",
       "executable_name": "can_logger",
       "nodes": [
         "imu"
       ]
     },
+    // TODO(max): Update the channel value with whatever channel the IMU is on.
+    {
+      "name": "can_translator",
+      "executable_name": "can_translator",
+      "args": [
+          "--channel=/can/canb"
+      ],
+      "nodes": [
+        "imu"
+      ]
+    },
+    {
+      "name": "dual_imu_blender",
+      "executable_name": "dual_imu_blender",
+      "nodes": [
+        "imu"
+      ]
+    },
     {
       "name": "web_proxy",
       "executable_name": "web_proxy_main",
@@ -286,6 +424,105 @@
       "nodes": [
         "imu"
       ]
+    },
+    {
+      "name": "image_logger",
+      "executable_name": "image_logger",
+      "args": [
+        "--rotate_every",
+        "30.0",
+        "--direct",
+        "--flush_size=4194304"
+      ],
+      "user": "pi",
+      "nodes": [
+        "imu"
+      ]
+    },
+    {
+      "name": "foxglove_websocket",
+      "user": "pi",
+      "nodes": [
+        "imu"
+      ]
+    },
+    {
+      "name": "foxglove_image_converter0",
+      "executable_name": "foxglove_image_converter",
+      "user": "pi",
+      "args": [
+          "--channel", "/camera0"
+      ],
+      "nodes": [
+        "imu"
+      ]
+    },
+    {
+      "name": "foxglove_image_converter1",
+      "executable_name": "foxglove_image_converter",
+      "user": "pi",
+      "args": [
+          "--channel", "/camera1"
+      ],
+      "nodes": [
+        "imu"
+      ]
+    },
+    {
+      "name": "constants_sender",
+      "autorestart": false,
+      "user": "pi",
+      "nodes": [
+        "imu"
+      ]
+    },
+    {
+      "name": "argus_camera0",
+      "executable_name": "argus_camera",
+      "args": [
+          "--enable_ftrace",
+          "--camera=0",
+          "--channel=/camera0"
+      ],
+      "user": "pi",
+      "nodes": [
+        "imu"
+      ]
+    },
+    {
+      "name": "argus_camera1",
+      "executable_name": "argus_camera",
+      "args": [
+          "--enable_ftrace",
+          "--camera=1",
+          "--channel=/camera1"
+      ],
+      "user": "pi",
+      "nodes": [
+        "imu"
+      ]
+    },
+    {
+      "name": "apriltag_detector0",
+      "executable_name": "apriltag_detector",
+      "args": [
+          "--channel=/camera0"
+      ],
+      "user": "pi",
+      "nodes": [
+        "imu"
+      ]
+    },
+    {
+      "name": "apriltag_detector1",
+      "executable_name": "apriltag_detector",
+      "args": [
+          "--channel=/camera1"
+      ],
+      "user": "pi",
+      "nodes": [
+        "imu"
+      ]
     }
   ],
   "maps": [
@@ -306,17 +543,26 @@
       "rename": {
         "name": "/imu/aos"
       }
+    },
+    {
+      "match": {
+        "name": "/camera*",
+        "source_node": "imu"
+      },
+      "rename": {
+        "name": "/imu/camera"
+      }
     }
   ],
   "nodes": [
     {
       "name": "imu",
-      "hostname": "orin3",
+      "hostname": "orin2",
       "hostnames": [
-        "orin-971-3",
-        "orin-7971-3",
-        "orin-8971-3",
-        "orin-9971-3"
+        "orin-971-2",
+        "orin-7971-2",
+        "orin-8971-2",
+        "orin-9971-2"
       ],
       "port": 9971
     },
@@ -325,9 +571,6 @@
     },
     {
       "name": "orin1"
-    },
-    {
-      "name": "orin2"
-    },
+    }
   ]
 }
diff --git a/y2024/y2024_orin_template.json b/y2024/y2024_orin1.json
similarity index 70%
rename from y2024/y2024_orin_template.json
rename to y2024/y2024_orin1.json
index 2bb2f46..a9f75d5 100644
--- a/y2024/y2024_orin_template.json
+++ b/y2024/y2024_orin1.json
@@ -1,61 +1,61 @@
 {
   "channels": [
     {
-      "name": "/orin{{ NUM }}/aos",
+      "name": "/orin1/aos",
       "type": "aos.timing.Report",
-      "source_node": "orin{{ NUM }}",
+      "source_node": "orin1",
       "frequency": 50,
       "num_senders": 20,
       "max_size": 8192
     },
     {
-      "name": "/orin{{ NUM }}/aos",
+      "name": "/orin1/aos",
       "type": "aos.logging.LogMessageFbs",
-      "source_node": "orin{{ NUM }}",
+      "source_node": "orin1",
       "frequency": 200,
       "num_senders": 20
     },
     {
-      "name": "/orin{{ NUM }}/aos",
+      "name": "/orin1/aos",
       "type": "aos.starter.Status",
-      "source_node": "orin{{ NUM }}",
+      "source_node": "orin1",
       "frequency": 50,
       "num_senders": 20,
       "max_size": 2000
     },
     {
-      "name": "/orin{{ NUM }}/aos",
+      "name": "/orin1/aos",
       "type": "aos.starter.StarterRpc",
-      "source_node": "orin{{ NUM }}",
+      "source_node": "orin1",
       "frequency": 10,
       "num_senders": 2
     },
     {
-      "name": "/orin{{ NUM }}/aos",
+      "name": "/orin1/aos",
       "type": "aos.message_bridge.ServerStatistics",
-      "source_node": "orin{{ NUM }}",
+      "source_node": "orin1",
       "max_size": 2048,
       "frequency": 10,
       "num_senders": 2
     },
     {
-      "name": "/orin{{ NUM }}/aos",
+      "name": "/orin1/aos",
       "type": "aos.message_bridge.ClientStatistics",
-      "source_node": "orin{{ NUM }}",
+      "source_node": "orin1",
       "frequency": 20,
       "num_senders": 2
     },
     {
-      "name": "/orin{{ NUM }}/aos",
+      "name": "/orin1/aos",
       "type": "aos.logging.DynamicLogCommand",
-      "source_node": "orin{{ NUM }}",
+      "source_node": "orin1",
       "frequency": 10,
       "num_senders": 2
     },
     {
-      "name": "/orin{{ NUM }}/aos",
+      "name": "/orin1/aos",
       "type": "aos.message_bridge.Timestamp",
-      "source_node": "orin{{ NUM }}",
+      "source_node": "orin1",
       "frequency": 15,
       "num_senders": 2,
       "logger": "LOCAL_AND_REMOTE_LOGGER",
@@ -70,16 +70,16 @@
           "time_to_live": 5000000,
           "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
           "timestamp_logger_nodes": [
-            "orin{{ NUM }}"
+            "orin1"
           ]
         }
       ]
     },
     {
-      "name": "/orin{{ NUM }}/aos/remote_timestamps/imu/orin{{ NUM }}/aos/aos-message_bridge-Timestamp",
+      "name": "/orin1/aos/remote_timestamps/imu/orin1/aos/aos-message_bridge-Timestamp",
       "type": "aos.message_bridge.RemoteMessage",
       "frequency": 20,
-      "source_node": "orin{{ NUM }}",
+      "source_node": "orin1",
       "max_size": 208
     },
     {
@@ -88,11 +88,11 @@
       "source_node": "imu",
       "logger": "LOCAL_AND_REMOTE_LOGGER",
       "logger_nodes": [
-        "orin{{ NUM }}"
+        "orin1"
       ],
       "destination_nodes": [
         {
-          "name": "orin{{ NUM }}",
+          "name": "orin1",
           "priority": 1,
           "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
           "timestamp_logger_nodes": [
@@ -103,16 +103,16 @@
       ]
     },
     {
-      "name": "/imu/aos/remote_timestamps/orin{{ NUM }}/imu/aos/aos-message_bridge-Timestamp",
+      "name": "/imu/aos/remote_timestamps/orin1/imu/aos/aos-message_bridge-Timestamp",
       "type": "aos.message_bridge.RemoteMessage",
       "frequency": 20,
       "source_node": "imu",
       "max_size": 208
     },
     {
-      "name": "/orin{{ NUM }}/camera1",
+      "name": "/orin1/camera0",
       "type": "frc971.vision.CameraImage",
-      "source_node": "orin{{ NUM }}",
+      "source_node": "orin1",
       "channel_storage_duration": 1000000000,
       "frequency": 65,
       "max_size": 4752384,
@@ -121,9 +121,9 @@
       "num_senders": 18
     },
     {
-      "name": "/orin{{ NUM }}/camera2",
+      "name": "/orin1/camera1",
       "type": "frc971.vision.CameraImage",
-      "source_node": "orin{{ NUM }}",
+      "source_node": "orin1",
       "channel_storage_duration": 1000000000,
       "frequency": 65,
       "max_size": 4752384,
@@ -132,39 +132,53 @@
       "num_senders": 18
     },
     {
-      "name": "/orin{{ NUM }}/camera1",
+      "name": "/orin1/camera0",
       "type": "foxglove.CompressedImage",
-      "source_node": "orin{{ NUM }}",
+      "source_node": "orin1",
       "channel_storage_duration": 1000000000,
       "frequency": 65,
       "max_size": 622384
     },
     {
-      "name": "/orin{{ NUM }}/camera2",
+      "name": "/orin1/camera1",
       "type": "foxglove.CompressedImage",
-      "source_node": "orin{{ NUM }}",
+      "source_node": "orin1",
       "channel_storage_duration": 1000000000,
       "frequency": 65,
       "max_size": 622384
     },
     {
-      "name": "/orin{{ NUM }}/camera1",
+      "name": "/orin1/camera0",
       "type": "foxglove.ImageAnnotations",
-      "source_node": "orin{{ NUM }}",
+      "source_node": "orin1",
       "frequency": 65,
       "max_size": 50000
     },
     {
-      "name": "/orin{{ NUM }}/camera2",
+      "name": "/orin1/camera1",
       "type": "foxglove.ImageAnnotations",
-      "source_node": "orin{{ NUM }}",
+      "source_node": "orin1",
       "frequency": 65,
       "max_size": 50000
     },
     {
-      "name": "/orin{{ NUM }}/camera1",
+      "name": "/orin1/camera0",
+      "type": "y2024.localizer.Visualization",
+      "source_node": "imu",
+      "frequency": 65,
+      "max_size": 50000
+    },
+    {
+      "name": "/orin1/camera1",
+      "type": "y2024.localizer.Visualization",
+      "source_node": "imu",
+      "frequency": 65,
+      "max_size": 50000
+    },
+    {
+      "name": "/orin1/camera0",
       "type": "frc971.vision.TargetMap",
-      "source_node": "orin{{ NUM }}",
+      "source_node": "orin1",
       "frequency": 65,
       "num_senders": 2,
       "max_size": 1024,
@@ -178,16 +192,16 @@
           "priority": 4,
           "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
           "timestamp_logger_nodes": [
-            "orin{{ NUM }}"
+            "orin1"
           ],
           "time_to_live": 5000000
         }
       ]
     },
     {
-      "name": "/orin{{ NUM }}/camera2",
+      "name": "/orin1/camera1",
       "type": "frc971.vision.TargetMap",
-      "source_node": "orin{{ NUM }}",
+      "source_node": "orin1",
       "frequency": 65,
       "num_senders": 2,
       "max_size": 1024,
@@ -201,30 +215,30 @@
           "priority": 4,
           "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
           "timestamp_logger_nodes": [
-            "orin{{ NUM }}"
+            "orin1"
           ],
           "time_to_live": 5000000
         }
       ]
     },
     {
-      "name": "/orin{{ NUM }}/aos/remote_timestamps/imu/orin{{ NUM }}/camera1/frc971-vision-TargetMap",
+      "name": "/orin1/aos/remote_timestamps/imu/orin1/camera0/frc971-vision-TargetMap",
       "type": "aos.message_bridge.RemoteMessage",
       "frequency": 80,
-      "source_node": "orin{{ NUM }}",
+      "source_node": "orin1",
       "max_size": 208
     },
     {
-      "name": "/orin{{ NUM }}/aos/remote_timestamps/imu/orin{{ NUM }}/camera2/frc971-vision-TargetMap",
+      "name": "/orin1/aos/remote_timestamps/imu/orin1/camera1/frc971-vision-TargetMap",
       "type": "aos.message_bridge.RemoteMessage",
       "frequency": 80,
-      "source_node": "orin{{ NUM }}",
+      "source_node": "orin1",
       "max_size": 208
     },
     {
-      "name": "/orin{{ NUM }}/constants",
+      "name": "/orin1/constants",
       "type": "y2024.Constants",
-      "source_node": "orin{{ NUM }}",
+      "source_node": "orin1",
       "frequency": 1,
       "num_senders": 2,
       "max_size": 65536
@@ -240,7 +254,7 @@
       ],
       "user": "pi",
       "nodes": [
-        "orin{{ NUM }}"
+        "orin1"
       ]
     },
     {
@@ -249,15 +263,18 @@
       "user": "root",
       "args": ["--user=pi"],
       "nodes": [
-          "orin{{ NUM }}"
+          "orin1"
       ]
     },
     {
       "name": "message_bridge_server",
       "executable_name": "message_bridge_server",
+       "args": [
+         "--rt_priority=16"
+       ],
       "user": "pi",
       "nodes": [
-        "orin{{ NUM }}"
+        "orin1"
       ]
     },
     {
@@ -269,15 +286,13 @@
         "--max_ice_port=5810"
       ],
       "nodes": [
-        "orin{{ NUM }}"
+        "orin1"
       ]
     },
     {
       "name": "image_logger",
       "executable_name": "image_logger",
       "args": [
-        "--logging_folder",
-        "",
         "--rotate_every",
         "30.0",
         "--direct",
@@ -285,14 +300,25 @@
       ],
       "user": "pi",
       "nodes": [
-        "orin{{ NUM }}"
+        "orin1"
       ]
     },
     {
       "name": "foxglove_websocket",
       "user": "pi",
       "nodes": [
-        "orin{{ NUM }}"
+        "orin1"
+      ]
+    },
+    {
+      "name": "foxglove_image_converter0",
+      "executable_name": "foxglove_image_converter",
+      "user": "pi",
+      "args": [
+          "--channel", "/camera0"
+      ],
+      "nodes": [
+        "orin1"
       ]
     },
     {
@@ -303,18 +329,7 @@
           "--channel", "/camera1"
       ],
       "nodes": [
-        "orin{{ NUM }}"
-      ]
-    },
-    {
-      "name": "foxglove_image_converter2",
-      "executable_name": "foxglove_image_converter",
-      "user": "pi",
-      "args": [
-          "--channel", "/camera2"
-      ],
-      "nodes": [
-        "orin{{ NUM }}"
+        "orin1"
       ]
     },
     {
@@ -322,7 +337,20 @@
       "autorestart": false,
       "user": "pi",
       "nodes": [
-        "orin{{ NUM }}"
+        "orin1"
+      ]
+    },
+    {
+      "name": "argus_camera0",
+      "executable_name": "argus_camera",
+      "args": [
+          "--enable_ftrace",
+          "--camera=0",
+          "--channel=/camera0",
+      ],
+      "user": "pi",
+      "nodes": [
+        "orin1"
       ]
     },
     {
@@ -330,25 +358,23 @@
       "executable_name": "argus_camera",
       "args": [
           "--enable_ftrace",
-          "--camera=0",
+          "--camera=1",
           "--channel=/camera1",
       ],
       "user": "pi",
       "nodes": [
-        "orin{{ NUM }}"
+        "orin1"
       ]
     },
     {
-      "name": "argus_camera2",
-      "executable_name": "argus_camera",
+      "name": "apriltag_detector0",
+      "executable_name": "apriltag_detector",
       "args": [
-          "--enable_ftrace",
-          "--camera=1",
-          "--channel=/camera2",
+          "--channel=/camera0",
       ],
       "user": "pi",
       "nodes": [
-        "orin{{ NUM }}"
+        "orin1"
       ]
     },
     {
@@ -359,18 +385,7 @@
       ],
       "user": "pi",
       "nodes": [
-        "orin{{ NUM }}"
-      ]
-    },
-    {
-      "name": "apriltag_detector2",
-      "executable_name": "apriltag_detector",
-      "args": [
-          "--channel=/camera2",
-      ],
-      "user": "pi",
-      "nodes": [
-        "orin{{ NUM }}"
+        "orin1"
       ]
     }
   ],
@@ -378,40 +393,40 @@
     {
       "match": {
         "name": "/aos*",
-        "source_node": "orin{{ NUM }}"
+        "source_node": "orin1"
       },
       "rename": {
-        "name": "/orin{{ NUM }}/aos"
+        "name": "/orin1/aos"
       }
     },
     {
       "match": {
         "name": "/constants*",
-        "source_node": "orin{{ NUM }}"
+        "source_node": "orin1"
       },
       "rename": {
-        "name": "/orin{{ NUM }}/constants"
+        "name": "/orin1/constants"
       }
     },
     {
       "match": {
         "name": "/camera*",
-        "source_node": "orin{{ NUM }}"
+        "source_node": "orin1"
       },
       "rename": {
-        "name": "/orin{{ NUM }}/camera"
+        "name": "/orin1/camera"
       }
     }
   ],
   "nodes": [
     {
-      "name": "orin{{ NUM }}",
-      "hostname": "orin{{ NUM }}",
+      "name": "orin1",
+      "hostname": "orin1",
       "hostnames": [
-        "orin-971-{{ NUM }}",
-        "orin-7971-{{ NUM }}",
-        "orin-8971-{{ NUM }}",
-        "orin-9971-{{ NUM }}"
+        "orin-971-1",
+        "orin-7971-1",
+        "orin-8971-1",
+        "orin-9971-1"
       ],
       "port": 9971
     },
diff --git a/y2024/y2024_roborio.json b/y2024/y2024_roborio.json
index 93caacc..5ba882a 100644
--- a/y2024/y2024_roborio.json
+++ b/y2024/y2024_roborio.json
@@ -19,6 +19,13 @@
     },
     {
       "name": "/roborio/aos",
+      "type": "frc971.PDPValues",
+      "source_node": "roborio",
+      "frequency": 55,
+      "max_size": 368
+    },
+    {
+      "name": "/roborio/aos",
       "type": "aos.RobotState",
       "source_node": "roborio",
       "frequency": 250
@@ -28,7 +35,7 @@
       "type": "aos.timing.Report",
       "source_node": "roborio",
       "frequency": 50,
-      "num_senders": 20,
+      "num_senders": 30,
       "max_size": 8192
     },
     {
@@ -120,6 +127,7 @@
       "type": "y2024.control_loops.superstructure.Status",
       "source_node": "roborio",
       "frequency": 400,
+      "max_size": 2048,
       "num_senders": 2
     },
     {
@@ -139,6 +147,22 @@
       "max_size": 448
     },
     {
+      "name": "/superstructure/canivore",
+      "type": "y2024.control_loops.superstructure.CANPosition",
+      "source_node": "roborio",
+      "frequency": 220,
+      "num_senders": 2,
+      "max_size": 1024
+    },
+    {
+      "name": "/superstructure/rio",
+      "type": "y2024.control_loops.superstructure.CANPosition",
+      "source_node": "roborio",
+      "frequency": 220,
+      "num_senders": 2,
+      "max_size": 1024
+    },
+    {
       "name": "/can",
       "type": "frc971.can_logger.CanFrame",
       "source_node": "roborio",
@@ -152,7 +176,7 @@
       "source_node": "roborio",
       "frequency": 220,
       "num_senders": 2,
-      "max_size": 400
+      "max_size": 424
     },
     {
       "name": "/drivetrain",
@@ -197,12 +221,12 @@
       "type": "frc971.control_loops.drivetrain.Position",
       "source_node": "roborio",
       "frequency": 400,
-      "max_size": 112,
+      "max_size": 128,
       "num_senders": 2
     },
     {
       "name": "/drivetrain",
-      "type": "frc971.control_loops.drivetrain.Output",
+      "type": "frc971.control_loops.drivetrain.RioLocalizerInputs",
       "source_node": "roborio",
       "frequency": 400,
       "max_size": 80,
@@ -221,6 +245,14 @@
     },
     {
       "name": "/drivetrain",
+      "type": "frc971.control_loops.drivetrain.Output",
+      "source_node": "roborio",
+      "frequency": 400,
+      "max_size": 80,
+      "num_senders": 2
+    },
+    {
+      "name": "/drivetrain",
       "type": "frc971.control_loops.drivetrain.Status",
       "source_node": "roborio",
       "frequency": 400,
@@ -406,8 +438,12 @@
       ]
     },
     {
-      "name": "can_logger",
+      "name": "roborio_can_logger",
       "executable_name": "can_logger",
+      "autostart": false,
+      "args": [
+        "--poll"
+      ],
       "nodes": [
         "roborio"
       ]