From d6587f3c9d2185503bd7d5a5721a7a16d8a49548 Mon Sep 17 00:00:00 2001 From: moo Date: Sat, 9 May 2026 11:55:37 -0700 Subject: [PATCH 1/1] first commit, works ig --- .gitignore | 1 + Cargo.lock | 532 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 9 + src/main.rs | 511 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1053 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f4408e7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,532 @@ +# 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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8efed6b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "subsystem-generator" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.102" +clap = { version = "4.6.1", features = ["derive"] } +inquire = "0.7" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ce59644 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,511 @@ +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, +} + +fn main() -> Result<(), Box> { + 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> { + 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::::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::::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(); + }} +}} +" + ) +} -- 2.39.5