--- /dev/null
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "clap"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "crossterm"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
+dependencies = [
+ "bitflags 1.3.2",
+ "crossterm_winapi",
+ "libc",
+ "mio",
+ "parking_lot",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "fuzzy-matcher"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
+dependencies = [
+ "thread_local",
+]
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "inquire"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a"
+dependencies = [
+ "bitflags 2.11.1",
+ "crossterm",
+ "dyn-clone",
+ "fuzzy-matcher",
+ "fxhash",
+ "newline-converter",
+ "once_cell",
+ "unicode-segmentation",
+ "unicode-width",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "mio"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "newline-converter"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags 2.11.1",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "signal-hook"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-mio"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
+dependencies = [
+ "libc",
+ "mio",
+ "signal-hook",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subsystem-generator"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "inquire",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+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",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
--- /dev/null
+use clap::Parser;
+use inquire::{Confirm, CustomType, Select, Text};
+use std::{
+ collections::HashSet,
+ fs, io,
+ path::{Path, PathBuf},
+};
+
+#[derive(clap::Parser, Debug)]
+#[command(version, about, long_about = None)]
+struct Args {
+ /// Overwrite existing files if they already exist
+ #[arg(long, short)]
+ force: bool,
+}
+
+#[derive(Debug)]
+struct MotorConfig {
+ field_base: String,
+ constant_base: String,
+ can_id: i32,
+ inverted: bool,
+}
+
+#[derive(Debug)]
+struct GeneratorConfig {
+ subsystem: String,
+ project_root: PathBuf,
+ subsystems_path: PathBuf,
+ package_prefix: String,
+ neutral_mode: String,
+ motors: Vec<MotorConfig>,
+}
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let args = Args::parse();
+ let mut config = prompt_config()?;
+ normalize_motor_identifiers(&mut config.motors);
+
+ let package = format!("{}.{}", config.package_prefix, config.subsystem);
+ let subsystem_dir = config
+ .project_root
+ .join(&config.subsystems_path)
+ .join(&config.subsystem);
+
+ fs::create_dir_all(&subsystem_dir)?;
+
+ let io_interface = format!("{}IO", config.subsystem);
+ let io_impl = format!("{}IOTalonFX", config.subsystem);
+ let io_inputs = format!("{}IOInputs", config.subsystem);
+ let constants_class = format!("{}Constants", config.subsystem);
+
+ let files = vec![
+ (
+ subsystem_dir.join(format!("{constants_class}.java")),
+ render_constants(&package, &constants_class, &config.motors),
+ ),
+ (
+ subsystem_dir.join(format!("{io_interface}.java")),
+ render_io_interface(
+ &package,
+ &io_interface,
+ &io_inputs,
+ &constants_class,
+ &config.motors,
+ ),
+ ),
+ (
+ subsystem_dir.join(format!("{io_impl}.java")),
+ render_io_impl(
+ &package,
+ &io_impl,
+ &io_interface,
+ &io_inputs,
+ &constants_class,
+ &config.neutral_mode,
+ &config.motors,
+ ),
+ ),
+ (
+ subsystem_dir.join(format!("{}.java", config.subsystem)),
+ render_subsystem(&package, &config.subsystem, &io_interface, &config.motors),
+ ),
+ ];
+
+ for (path, contents) in files {
+ write_file(&path, &contents, args.force)?;
+ println!("Created {}", path.display());
+ }
+
+ Ok(())
+}
+
+fn prompt_config() -> Result<GeneratorConfig, Box<dyn std::error::Error>> {
+ let project_root = Text::new("WPILib project root")
+ .with_default(".")
+ .prompt()?;
+ let subsystems_path = Text::new("Subsystems path (relative to project root)")
+ .with_default("src/main/java/frc/robot/subsystems")
+ .prompt()?;
+ let package_prefix = Text::new("Java package")
+ .with_default("frc.robot.subsystems")
+ .prompt()?;
+ let subsystem_name = Text::new("Subsystem name")
+ .with_help_message("Used for class name and package segment")
+ .prompt()?;
+ let subsystem = to_pascal_case(&subsystem_name);
+
+ let motor_count = CustomType::<usize>::new("Number of TalonFX motors")
+ .with_default(1)
+ .with_error_message("Enter an int greater than zero")
+ .prompt()?;
+ if motor_count == 0 {
+ return Err("motor count must be at least 1".into());
+ }
+
+ let neutral_mode = Select::new("Neutral mode for generated motors", vec!["Brake", "Coast"])
+ .prompt()?
+ .to_string();
+
+ let mut motors = Vec::with_capacity(motor_count);
+ for i in 0..motor_count {
+ let default_name = format!("motor{}", i + 1);
+ let name = Text::new(&format!("Motor {} name", i + 1))
+ .with_default(&default_name)
+ .with_help_message("Examples: left, right, roller, feeder")
+ .prompt()?;
+ let can_id = CustomType::<i32>::new(&format!("Motor {} CAN ID", i + 1))
+ .with_error_message("CAN ID must be an integer from 0 to 62")
+ .with_validator(|value: &i32| {
+ if (0..=62).contains(value) {
+ Ok(inquire::validator::Validation::Valid)
+ } else {
+ Ok(inquire::validator::Validation::Invalid(
+ "CAN ID must be between 0 and 62".into(),
+ ))
+ }
+ })
+ .prompt()?;
+ let inverted = Confirm::new(&format!("Is '{}' inverted?", name))
+ .with_default(false)
+ .prompt()?;
+
+ motors.push(MotorConfig {
+ field_base: to_lower_camel_case(&name),
+ constant_base: to_upper_snake_case(&name),
+ can_id,
+ inverted,
+ });
+ }
+
+ Ok(GeneratorConfig {
+ subsystem,
+ project_root: PathBuf::from(project_root),
+ subsystems_path: PathBuf::from(subsystems_path),
+ package_prefix,
+ neutral_mode,
+ motors,
+ })
+}
+
+fn normalize_motor_identifiers(motors: &mut [MotorConfig]) {
+ let mut seen_fields = HashSet::new();
+ let mut seen_constants = HashSet::new();
+
+ for (index, motor) in motors.iter_mut().enumerate() {
+ let base_field = if motor.field_base.is_empty() {
+ format!("motor{}", index + 1)
+ } else {
+ motor.field_base.clone()
+ };
+
+ let mut unique_field = base_field.clone();
+ let mut n = 2;
+ while !seen_fields.insert(unique_field.clone()) {
+ unique_field = format!("{base_field}{n}");
+ n += 1;
+ }
+ motor.field_base = unique_field;
+
+ let base_constant = if motor.constant_base.is_empty() {
+ format!("MOTOR_{}", index + 1)
+ } else {
+ motor.constant_base.clone()
+ };
+
+ let mut unique_constant = base_constant.clone();
+ let mut m = 2;
+ while !seen_constants.insert(unique_constant.clone()) {
+ unique_constant = format!("{base_constant}_{m}");
+ m += 1;
+ }
+ motor.constant_base = unique_constant;
+ }
+}
+
+fn write_file(path: &Path, contents: &str, force: bool) -> io::Result<()> {
+ if path.exists() && !force {
+ return Err(io::Error::new(
+ io::ErrorKind::AlreadyExists,
+ format!(
+ "{} already exists. Re-run with --force to overwrite.",
+ path.display()
+ ),
+ ));
+ }
+
+ fs::write(path, contents)
+}
+
+fn to_pascal_case(name: &str) -> String {
+ let mut out = String::new();
+ for part in name.split(|c: char| !c.is_ascii_alphanumeric()) {
+ if part.is_empty() {
+ continue;
+ }
+ let mut chars = part.chars();
+ if let Some(first) = chars.next() {
+ out.push(first.to_ascii_uppercase());
+ for c in chars {
+ out.push(c.to_ascii_lowercase());
+ }
+ }
+ }
+
+ if out.is_empty() {
+ "Subsystem".to_string()
+ } else if out.chars().next().is_some_and(|c| c.is_ascii_digit()) {
+ format!("Subsystem{out}")
+ } else {
+ out
+ }
+}
+
+fn to_lower_camel_case(name: &str) -> String {
+ let pascal = to_pascal_case(name);
+ if pascal.is_empty() {
+ return "motor".to_string();
+ }
+ let mut chars = pascal.chars();
+ let first = chars.next().unwrap_or('m').to_ascii_lowercase();
+ let mut out = String::new();
+ out.push(first);
+ out.extend(chars);
+ if out.chars().next().is_some_and(|c| c.is_ascii_digit()) {
+ format!("motor{out}")
+ } else {
+ out
+ }
+}
+
+fn to_upper_snake_case(name: &str) -> String {
+ let mut out = String::new();
+ let mut last_was_sep = false;
+ for c in name.chars() {
+ if c.is_ascii_alphanumeric() {
+ out.push(c.to_ascii_uppercase());
+ last_was_sep = false;
+ } else if !last_was_sep && !out.is_empty() {
+ out.push('_');
+ last_was_sep = true;
+ }
+ }
+
+ let out = out.trim_matches('_').to_string();
+ if out.is_empty() {
+ "MOTOR".to_string()
+ } else if out.chars().next().is_some_and(|c| c.is_ascii_digit()) {
+ format!("MOTOR_{out}")
+ } else {
+ out
+ }
+}
+
+fn upper_first(value: &str) -> String {
+ let mut chars = value.chars();
+ match chars.next() {
+ Some(first) => {
+ let mut out = String::new();
+ out.push(first.to_ascii_uppercase());
+ out.extend(chars);
+ out
+ }
+ None => String::new(),
+ }
+}
+
+fn render_constants(package: &str, constants_class: &str, motors: &[MotorConfig]) -> String {
+ let mut motor_constants = String::new();
+ for motor in motors {
+ motor_constants.push_str(&format!(
+ " public static final int {}_MOTOR_ID = {};\n",
+ motor.constant_base, motor.can_id
+ ));
+ }
+
+ format!(
+ "package {package};
+
+public class {constants_class} {{
+
+{motor_constants}}}
+"
+ )
+}
+
+fn render_io_interface(
+ package: &str,
+ io_interface: &str,
+ io_inputs: &str,
+ _constants_class: &str,
+ motors: &[MotorConfig],
+) -> String {
+ let mut input_fields = String::new();
+ for motor in motors {
+ let f = &motor.field_base;
+ input_fields.push_str(&format!(" public double {f}PositionRot = 0.0;\n"));
+ input_fields.push_str(&format!(" public double {f}VelocityRps = 0.0;\n"));
+ input_fields.push_str(&format!(" public double {f}StatorCurrentAmps = 0.0;\n"));
+ input_fields.push_str(&format!(" public double {f}SupplyCurrentAmps = 0.0;\n"));
+ input_fields.push_str(&format!(" public double {f}AppliedVolts = 0.0;\n"));
+ }
+
+ let mut speed_methods = String::new();
+ let mut stop_calls = String::new();
+ for motor in motors {
+ let method_suffix = upper_first(&motor.field_base);
+ speed_methods.push_str(&format!(
+ " public void set{method_suffix}SpeedRaw(double speed);\n\n"
+ ));
+ speed_methods.push_str(&format!(
+ " public void set{method_suffix}Control(ControlRequest request);\n\n"
+ ));
+ stop_calls.push_str(&format!(" set{method_suffix}SpeedRaw(0.0);\n"));
+ }
+
+ format!(
+ "package {package};
+
+import org.littletonrobotics.junction.AutoLog;
+import com.ctre.phoenix6.controls.ControlRequest;
+
+public interface {io_interface} {{
+ @AutoLog
+ public static class {io_inputs} {{
+{input_fields} }}
+
+ public void updateInputs({io_inputs} inputs);
+
+{speed_methods} public default void stop() {{
+{stop_calls} }}
+
+ public default void close() {{}}
+}}
+"
+ )
+}
+
+fn render_io_impl(
+ package: &str,
+ io_impl: &str,
+ io_interface: &str,
+ io_inputs: &str,
+ constants_class: &str,
+ neutral_mode: &str,
+ motors: &[MotorConfig],
+) -> String {
+ let mut motor_fields = String::new();
+ for motor in motors {
+ motor_fields.push_str(&format!(
+ " private final TalonFX {}Motor = new TalonFX({constants_class}.{}_MOTOR_ID, Constants.CANIVORE_SUB);\n",
+ motor.field_base, motor.constant_base
+ ));
+ }
+
+ let mut config_calls = String::new();
+ for motor in motors {
+ let inverted = if motor.inverted {
+ "InvertedValue.Clockwise_Positive"
+ } else {
+ "InvertedValue.CounterClockwise_Positive"
+ };
+ let neutral = if neutral_mode == "Brake" {
+ "NeutralModeValue.Brake"
+ } else {
+ "NeutralModeValue.Coast"
+ };
+ config_calls.push_str(&format!(
+ " {}Motor.getConfigurator().apply(config);\n",
+ motor.field_base
+ ));
+ config_calls.push_str(&format!(
+ " {}Motor.getConfigurator().apply(new MotorOutputConfigs().withInverted({inverted}).withNeutralMode({neutral}));\n",
+ motor.field_base
+ ));
+ }
+
+ let mut input_assignments = String::new();
+ for motor in motors {
+ let f = &motor.field_base;
+ input_assignments.push_str(&format!(
+ " inputs.{f}PositionRot = {f}Motor.getPosition().getValueAsDouble();\n"
+ ));
+ input_assignments.push_str(&format!(
+ " inputs.{f}VelocityRps = {f}Motor.getVelocity().getValueAsDouble();\n"
+ ));
+ input_assignments.push_str(&format!(
+ " inputs.{f}StatorCurrentAmps = {f}Motor.getStatorCurrent().getValueAsDouble();\n"
+ ));
+ input_assignments.push_str(&format!(
+ " inputs.{f}SupplyCurrentAmps = {f}Motor.getSupplyCurrent().getValueAsDouble();\n"
+ ));
+ input_assignments.push_str(&format!(
+ " inputs.{f}AppliedVolts = {f}Motor.getMotorVoltage().getValueAsDouble();\n"
+ ));
+ }
+
+ let mut raw_speed_methods = String::new();
+ for motor in motors {
+ let method_suffix = upper_first(&motor.field_base);
+ raw_speed_methods.push_str(&format!(
+ " @Override\n public void set{method_suffix}SpeedRaw(double speed) {{\n {}Motor.set(speed);\n }}\n\n",
+ motor.field_base
+ ));
+ raw_speed_methods.push_str(&format!(
+ " @Override\n public void set{method_suffix}Control(ControlRequest request) {{\n {}Motor.setControl(request);\n }}\n\n",
+ motor.field_base
+ ));
+ }
+
+ let mut close_calls = String::new();
+ for motor in motors {
+ close_calls.push_str(&format!(" {}Motor.close();\n", motor.field_base));
+ }
+
+ format!(
+ "package {package};
+
+import com.ctre.phoenix6.configs.MotorOutputConfigs;
+import com.ctre.phoenix6.configs.TalonFXConfiguration;
+import com.ctre.phoenix6.controls.ControlRequest;
+import com.ctre.phoenix6.hardware.TalonFX;
+import com.ctre.phoenix6.signals.InvertedValue;
+import com.ctre.phoenix6.signals.NeutralModeValue;
+import frc.robot.constants.Constants;
+
+public class {io_impl} implements {io_interface} {{
+{motor_fields}
+ public {io_impl}() {{
+ TalonFXConfiguration config = new TalonFXConfiguration();
+ // TODO: tune PID, current limits, and motion magic limits
+{config_calls} }}
+
+ @Override
+ public void updateInputs({io_inputs} inputs) {{
+{input_assignments} }}
+
+{raw_speed_methods} @Override
+ public void close() {{
+{close_calls} }}
+}}
+"
+ )
+}
+
+fn render_subsystem(
+ package: &str,
+ subsystem: &str,
+ io_interface: &str,
+ motors: &[MotorConfig],
+) -> String {
+ let io_inputs_auto = format!("{subsystem}IOInputsAutoLogged");
+ let mut methods = String::new();
+ for motor in motors {
+ let method_suffix = upper_first(&motor.field_base);
+ methods.push_str(&format!(
+ " public void set{method_suffix}SpeedRaw(double speed) {{\n io.set{method_suffix}SpeedRaw(speed);\n }}\n\n"
+ ));
+ }
+ format!(
+ "package {package};
+
+import org.littletonrobotics.junction.Logger;
+
+import edu.wpi.first.wpilibj2.command.SubsystemBase;
+
+public class {subsystem} extends SubsystemBase {{
+ private final {io_interface} io;
+ private final {io_inputs_auto} inputs = new {io_inputs_auto}();
+
+ public {subsystem}({io_interface} io) {{
+ this.io = io;
+ }}
+
+ @Override
+ public void periodic() {{
+ io.updateInputs(inputs);
+ Logger.processInputs(\"{subsystem}\", inputs);
+ }}
+
+{methods} public void stop() {{
+ io.stop();
+ }}
+
+ public void close() {{
+ io.close();
+ }}
+}}
+"
+ )
+}