diff --git a/.gitignore b/.gitignore index 34cbaac..134d1a6 100644 --- a/.gitignore +++ b/.gitignore @@ -171,6 +171,7 @@ out/ # Simulation GUI and other tools window save file networktables.json simgui.json +simgui-ds.json *-window.json # Simulation data log directory diff --git a/.wpilib/wpilib_preferences.json b/.wpilib/wpilib_preferences.json index bbfeccd..44dd67e 100644 --- a/.wpilib/wpilib_preferences.json +++ b/.wpilib/wpilib_preferences.json @@ -1,6 +1,6 @@ { "enableCppIntellisense": false, "currentLanguage": "java", - "projectYear": "2027_alpha5", + "projectYear": "2027_alpha7", "teamNumber": 1138 } \ No newline at end of file diff --git a/build.gradle b/build.gradle index b4f32f5..1972bb7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ plugins { id "java" - id "org.wpilib.GradleRIO" version "2027.0.0-alpha-6" - id "com.gradleup.shadow" version "9.3.0" + id "application" + id "org.wpilib.GradleRIO" version "2027.0.0-alpha-7" } java { @@ -18,8 +18,6 @@ deploy { systemcore(getTargetTypeClass('SystemCore')) { // Team number is loaded either from the .wpilib/wpilib_preferences.json // or from command line. If not found an exception will be thrown. - // You can use getTeamOrDefault(team) instead of getTeamNumber if you - // want to store a team number in this file. team = project.wpilib.getTeamNumber() // Use the default systemcore host name. This must be called after setting team // as happens on the line above @@ -31,6 +29,9 @@ deploy { // getTargetTypeClass is a shortcut to get the class type using a string wpilibJava(getArtifactTypeClass('WPILibJavaArtifact')) { + // Set to true to use debug including JNI, which will drastically impact + // performance. + debugJni = false } // Static files artifact @@ -47,9 +48,8 @@ deploy { def deployArtifact = deploy.targets.systemcore.artifacts.wpilibJava -// Set to true to use debug for all targets including JNI, which will drastically impact -// performance. -wpi.java.debugJni = false +// Set to true to use debug for simulation including JNI +wpi.java.runSimWithDebugJni = false // Set this to true to enable desktop support. def includeDesktopSupport = false @@ -77,6 +77,12 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // AdvantageKit's @AutoLog annotation processor. Version is read from the vendordep so the + // two can never drift apart. + def akitJson = new groovy.json.JsonSlurper() + .parseText(new File(projectDir, "vendordeps/AdvantageKit.json").text) + annotationProcessor "org.littletonrobotics.akit:akit-autolog:$akitJson.version" } test { @@ -88,23 +94,18 @@ test { wpi.sim.addGui().defaultEnabled = true wpi.sim.addDriverstation() -// Setting up my Jar File. In this case, adding all libraries into the main jar ('fat/shaded jar') -// in order to make them all available at runtime and merging service files to make JSON work. -// Also adding the manifest so WPILib knows where to look for our Robot Class. -shadowJar { - mergeServiceFiles() +application.mainClass = ROBOT_MAIN_CLASS + +deployArtifact.configureApplication(application) +wpi.java.configureApplication(application) +wpi.java.configureTestTasks(test) + +jar { from('src') { into 'backup/src' } from('vendordeps') { into 'backup/vendordeps' } from('build.gradle') { into 'backup' } - manifest org.wpilib.gradlerio.GradleRIOPlugin.javaManifest(ROBOT_MAIN_CLASS) - duplicatesStrategy = DuplicatesStrategy.INCLUDE } -// Configure jar and deploy tasks -deployArtifact.jarTask = shadowJar -wpi.java.configureExecutableTasks(shadowJar) -wpi.java.configureTestTasks(test) - // Configure string concat to always inline compile tasks.withType(JavaCompile) { options.compilerArgs.add '-XDstringConcat=inline' diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/settings.gradle b/settings.gradle index e2626a8..842a352 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,19 +2,28 @@ import org.gradle.internal.os.OperatingSystem pluginManagement { repositories { - String wpilibYear = '2027_alpha5' + String wpilibYear = '2027_alpha7' File wpilibHome - if (OperatingSystem.current().isWindows()) { + def os = OperatingSystem.current() + if (os.isWindows()) { String publicFolder = System.getenv('PUBLIC') if (publicFolder == null) { publicFolder = "C:\\Users\\Public" } - def homeRoot = new File(publicFolder, "wpilib") - wpilibHome = new File(homeRoot, wpilibYear) + wpilibHome = new File(new File(publicFolder, "wpilib"), wpilibYear) + } else if (os.isLinux()) { + String xdgDataHome = System.getenv('XDG_DATA_HOME') + if (xdgDataHome == null || xdgDataHome.trim().isEmpty() || !new File(xdgDataHome).isAbsolute()) { + xdgDataHome = new File(System.getProperty("user.home"), ".local/share").getPath() + } + wpilibHome = new File(new File(xdgDataHome, "wpilib"), wpilibYear) } else { - def userFolder = System.getProperty("user.home") - def homeRoot = new File(userFolder, "wpilib") - wpilibHome = new File(homeRoot, wpilibYear) + def userFolder = new File(System.getProperty("user.home")) + // The macOS installer has used both ~/.wpilib and ~/wpilib across alphas. + wpilibHome = new File(new File(userFolder, ".wpilib"), wpilibYear) + if (!wpilibHome.exists()) { + wpilibHome = new File(new File(userFolder, "wpilib"), wpilibYear) + } } def wpilibHomeMaven = new File(wpilibHome, 'maven') maven { diff --git a/src/main/java/first/Main.java b/src/main/java/first/Main.java index 7d287e7..b3a3127 100644 --- a/src/main/java/first/Main.java +++ b/src/main/java/first/Main.java @@ -20,6 +20,6 @@ private Main() {} *
If you change your main robot class, change the parameter type. */ public static void main(String... args) { - RobotBase.startRobot(first.robot.Robot.class); + RobotBase.startRobot(first.robot.Robot::new); } } diff --git a/src/main/java/first/robot/Constants.java b/src/main/java/first/robot/Constants.java index 446e758..af188da 100644 --- a/src/main/java/first/robot/Constants.java +++ b/src/main/java/first/robot/Constants.java @@ -4,16 +4,93 @@ package first.robot; +import org.wpilib.framework.RobotBase; +import org.wpilib.util.Alert; +import org.wpilib.util.Alert.Level; + /** - * The Constants class provides a convenient place for teams to hold robot-wide numerical or boolean - * constants. This class should not be used for any other purpose. All constants should be declared - * globally (i.e. public static). Do not put anything functional in this class. - * - *
It is advised to statically import this class (or one of its inner classes) wherever the - * constants are needed, to reduce verbosity. + * This class defines the runtime mode used by AdvantageKit. The mode is always "real" when running + * on SystemCore. Change the value of {@link #simMode} to switch between "sim" (physics sim) and + * "replay" (log replay from a file). */ public final class Constants { - public static class OperatorConstants { - public static final int kDriverControllerPort = 0; + /** + * Robot loop period. This is handed to {@code LoggedRobot} in {@link Robot}, so the value used + * for velocity discretization and Phoenix status frame rates always matches the real loop rate. + */ + public static final double loopPeriodSecs = 0.02; + + /** Which physical robot the code is running on. Selects hardware IDs. */ + private static RobotType robotType = RobotType.DEVBOT; + + /** Enables tuning dashboard inputs. Must be false when merging. */ + public static final boolean tuningMode = false; + + /** Mode used when not running on real hardware. Set to REPLAY to replay a log instead. */ + public static final Mode simMode = Mode.SIM; + + @SuppressWarnings("resource") + public static RobotType getRobot() { + if (!disableHAL && RobotBase.isReal() && robotType == RobotType.SIMBOT) { + new Alert( + "invalidRobotType", + "Invalid robot selected, using competition robot as default.", + Level.MEDIUM) + .set(true); + robotType = RobotType.DEVBOT; + } + return robotType; + } + + /** + * Returns the current runtime mode. Real hardware is always {@link Mode#REAL}; off-robot this + * follows {@link #simMode} so that the physics simulation actually runs by default. + */ + public static Mode getMode() { + return RobotBase.isReal() ? Mode.REAL : simMode; + } + + public enum Mode { + /** Running on a real robot. */ + REAL, + + /** Running a physics simulator. */ + SIM, + + /** Replaying from a log file. */ + REPLAY } + + public enum RobotType { + DEVBOT, + SIMBOT + } + + public static boolean disableHAL = false; + + public static void disableHAL() { + disableHAL = true; + } + + /** Checks whether the correct robot is selected when deploying. */ + public static class CheckDeploy { + public static void main(String... args) { + if (robotType == RobotType.SIMBOT) { + System.err.println("Cannot deploy, invalid robot selected: " + robotType); + System.exit(1); + } + } + } + + /** Checks that the default robot is selected and tuning mode is disabled. */ + public static class CheckPullRequest { + public static void main(String... args) { + if (robotType != RobotType.DEVBOT || tuningMode) { + System.err.println("Do not merge, non-default constants are configured."); + System.exit(1); + } + } + } + + private Constants() {} } diff --git a/src/main/java/first/robot/Robot.java b/src/main/java/first/robot/Robot.java index 703d454..422560d 100644 --- a/src/main/java/first/robot/Robot.java +++ b/src/main/java/first/robot/Robot.java @@ -3,51 +3,96 @@ // the WPILib BSD license file in the root directory of this project. package first.robot; - import org.wpilib.command2.Command; import org.wpilib.command2.CommandScheduler; -import org.wpilib.framework.TimedRobot; +import org.littletonrobotics.junction.LogFileUtil; +import org.littletonrobotics.junction.LoggedRobot; +import org.littletonrobotics.junction.Logger; +import org.littletonrobotics.junction.networktables.NT4Publisher; +import org.littletonrobotics.junction.wpilog.WPILOGReader; +import org.littletonrobotics.junction.wpilog.WPILOGWriter; /** - * The methods in this class are called automatically corresponding to each mode, as described in - * the TimedRobot documentation. If you change the name of this class or the package after creating - * this project, you must also update the Main.java file in the project. + * The VM is configured to automatically run this class, and to call the functions corresponding to + * each mode, as described in the TimedRobot documentation. If you change the name of this class or + * the package after creating this project, you must also update the build.gradle file in the + * project. */ -public class Robot extends TimedRobot { +public class Robot extends LoggedRobot { private Command autonomousCommand; + private RobotContainer robotContainer; - private final RobotContainer robotContainer; - - /** - * This function is run when the robot is first started up and should be used for any - * initialization code. - */ public Robot() { - // Instantiate our RobotContainer. This will perform all our button bindings, and put our - // autonomous chooser on the dashboard. + // Run the loop at the period the rest of the code assumes (see Constants.loopPeriodSecs). + super(Constants.loopPeriodSecs); + + // Record build/runtime metadata so logs can be traced back to a configuration + Logger.recordMetadata("RobotType", Constants.getRobot().toString()); + Logger.recordMetadata("RuntimeMode", Constants.getMode().toString()); + Logger.recordMetadata("TuningMode", Boolean.toString(Constants.tuningMode)); + + // Set up data receivers & replay source + switch (Constants.getMode()) { + case REAL: + // Running on a real robot, log to a USB stick ("/U/logs") + Logger.addDataReceiver(new WPILOGWriter()); + // Logger.addDataReceiver(new WPILOGXZWriter()); + Logger.addDataReceiver(new NT4Publisher()); + break; + + case SIM: + // Running a physics simulator, log to NT + Logger.addDataReceiver(new NT4Publisher()); + break; + + case REPLAY: + // Replaying a log, set up replay source + setUseTiming(false); // Run as fast as possible + String logPath = LogFileUtil.findReplayLog(); + Logger.setReplaySource(new WPILOGReader(logPath)); + Logger.addDataReceiver(new WPILOGWriter(LogFileUtil.addPathSuffix(logPath, "_sim"))); + // String inPath = LogFileUtil.findReplayLog(); + // String outPath = LogFileUtil.addPathSuffix(inPath, "_sim"); + // Logger.setReplaySource(inPath.endsWith(".wpilogxz") ? new WPILOGXZReader(inPath) : + // new WPILOGReader(inPath)); + // if (outPath.endsWith(".wpilogxz")) { + // outPath = outPath.substring(0, outPath.length() - 2); + // } + // Logger.addDataReceiver(new WPILOGWriter(outPath)); + break; + } + + // Start AdvantageKit logger + Logger.start(); + + // Instantiate our RobotContainer. This will perform all our button bindings, + // and put our autonomous chooser on the dashboard. robotContainer = new RobotContainer(); } - /** - * This function is called every 20 ms, no matter the mode. Use this for items like diagnostics - * that you want ran during disabled, autonomous, teleoperated and utility. - * - *
This runs after the mode specific periodic functions, but before LiveWindow and
- * SmartDashboard integrated updating.
- */
+ /** This function is called periodically during all modes. */
@Override
public void robotPeriodic() {
- // Runs the Scheduler. This is responsible for polling buttons, adding newly-scheduled
- // commands, running already-scheduled commands, removing finished or interrupted commands,
- // and running subsystem periodic() methods. This must be called from the robot's periodic
- // block in order for anything in the Command-based framework to work.
+ // Optionally switch the thread to high priority to improve loop
+ // timing (see the template project documentation for details)
+ // Threads.setCurrentThreadPriority(true, 99);
+
+ // Runs the Scheduler. This is responsible for polling buttons, adding
+ // newly-scheduled commands, running already-scheduled commands, removing
+ // finished or interrupted commands, and running subsystem periodic() methods.
+ // This must be called from the robot's periodic block in order for anything in
+ // the Command-based framework to work.
CommandScheduler.getInstance().run();
+
+ // Return to non-RT thread priority (do not modify the first argument)
+ // Threads.setCurrentThreadPriority(false, 10);
}
- /** This function is called once each time the robot enters Disabled mode. */
+ /** This function is called once when the robot is disabled. */
@Override
public void disabledInit() {}
+ /** This function is called periodically when disabled. */
@Override
public void disabledPeriodic() {}
@@ -56,7 +101,7 @@ public void disabledPeriodic() {}
public void autonomousInit() {
autonomousCommand = robotContainer.getAutonomousCommand();
- // schedule the autonomous command (example)
+ // schedule the autonomous command (Command.schedule() was removed in 2027)
if (autonomousCommand != null) {
CommandScheduler.getInstance().schedule(autonomousCommand);
}
@@ -66,6 +111,7 @@ public void autonomousInit() {
@Override
public void autonomousPeriodic() {}
+ /** This function is called once when teleop is enabled. */
@Override
public void teleopInit() {
// This makes sure that the autonomous stops running when
@@ -81,15 +127,14 @@ public void teleopInit() {
@Override
public void teleopPeriodic() {}
- @Override
- public void utilityInit() {
- // Cancels all running commands at the start of utility mode.
+ /** This function is called once when test mode is enabled. */
+ public void testInit() {
+ // Cancels all running commands at the start of test mode.
CommandScheduler.getInstance().cancelAll();
}
- /** This function is called periodically during utility mode. */
- @Override
- public void utilityPeriodic() {}
+ /** This function is called periodically during test mode. */
+ public void testPeriodic() {}
/** This function is called once when the robot is first started up. */
@Override
@@ -98,4 +143,4 @@ public void simulationInit() {}
/** This function is called periodically whilst in simulation. */
@Override
public void simulationPeriodic() {}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/first/robot/RobotContainer.java b/src/main/java/first/robot/RobotContainer.java
index ca7e3d6..e242915 100644
--- a/src/main/java/first/robot/RobotContainer.java
+++ b/src/main/java/first/robot/RobotContainer.java
@@ -4,47 +4,120 @@
package first.robot;
+import first.robot.commands.DriveCommands;
+import first.robot.subsystems.drive.Drive;
+import first.robot.subsystems.drive.DriveConstants;
+import first.robot.subsystems.drive.GyroIO;
+import first.robot.subsystems.drive.GyroIOPigeon2;
+import first.robot.subsystems.drive.ModuleIO;
+import first.robot.subsystems.drive.ModuleIOSim;
+import first.robot.subsystems.drive.ModuleIOTalonFX;
+import org.littletonrobotics.junction.networktables.LoggedNetworkChooser;
import org.wpilib.command2.Command;
+import org.wpilib.command2.Commands;
import org.wpilib.command2.button.CommandGamepad;
-import org.wpilib.command2.button.Trigger;
-import first.robot.Constants.OperatorConstants;
-import first.robot.commands.Autos;
-import first.robot.commands.ExampleCommand;
-import first.robot.subsystems.ExampleSubsystem;
+import org.wpilib.math.geometry.Pose2d;
+import org.wpilib.math.geometry.Rotation2d;
/**
* This class is where the bulk of the robot should be declared. Since Command-based is a
* "declarative" paradigm, very little robot logic should actually be handled in the {@link Robot}
* periodic methods (other than the scheduler calls). Instead, the structure of the robot (including
- * subsystems, commands, and trigger mappings) should be declared here.
+ * subsystems, commands, and button mappings) should be declared here.
*/
public class RobotContainer {
- // The robot's subsystems and commands are defined here...
- private final ExampleSubsystem exampleSubsystem = new ExampleSubsystem();
+ // Subsystems
+ private final Drive drive;
- private final CommandGamepad driverController =
- new CommandGamepad(OperatorConstants.kDriverControllerPort);
+ // Controller. CommandGamepad uses controller-agnostic names: faceDown/faceRight/faceLeft/faceUp
+ // are A/B/X/Y on an Xbox pad.
+ private final CommandGamepad controller = new CommandGamepad(0);
+
+ // Dashboard inputs
+ private final LoggedNetworkChooser This command should only be used in voltage control mode.
+ */
+ public static Command feedforwardCharacterization(Drive drive) {
+ List {@code encoderOffset} is written into the CANcoder's {@code MagnetSensor.MagnetOffset}, so
+ * after calibration a module pointing straight forward must read 0. To recalibrate: point every
+ * wheel forward (bevel gears all facing the same way), set the offsets below to zero, deploy,
+ * then read {@code /Drive/ModuleN/turnAbsolutePosition} and negate each value here.
+ */
+ public static final ModuleConfig[] moduleConfigs = {
+ // FL
+ ModuleConfig.builder()
+ .driveMotorId(12)
+ .turnMotorId(9)
+ .encoderId(2)
+ .encoderOffset(Rotation2d.fromRadians(0.9022009671847623))
+ .turnInverted(true)
+ .encoderInverted(false)
+ .build(),
+ // FR
+ ModuleConfig.builder()
+ .driveMotorId(2)
+ .turnMotorId(10)
+ .encoderId(3)
+ .encoderOffset(Rotation2d.fromRadians(1.6663099495963458))
+ .turnInverted(true)
+ .encoderInverted(false)
+ .build(),
+ // BL
+ ModuleConfig.builder()
+ .driveMotorId(15)
+ .turnMotorId(11)
+ .encoderId(4)
+ .encoderOffset(Rotation2d.fromRadians(-0.09896592242077659))
+ .turnInverted(true)
+ .encoderInverted(false)
+ .build(),
+ // BR
+ ModuleConfig.builder()
+ .driveMotorId(3)
+ .turnMotorId(8)
+ .encoderId(5)
+ .encoderOffset(Rotation2d.fromRadians(-3.051832863487227))
+ .turnInverted(true)
+ .encoderInverted(false)
+ .build()
+ };
+
+ public static class PigeonConstants {
+ public static final int id = Constants.getRobot() == RobotType.DEVBOT ? 3 : 30;
+ }
+
+ /**
+ * Configuration for a single swerve module.
+ *
+ * @param driveMotorId CAN id of the drive TalonFX
+ * @param turnMotorId CAN id of the turn TalonFX
+ * @param encoderId CAN id of the steer CANcoder
+ * @param encoderOffset Offset applied to the CANcoder so that forward reads zero
+ * @param turnInverted Whether the turn motor is inverted
+ * @param encoderInverted Whether the CANcoder counts clockwise-positive
+ */
+ public record ModuleConfig(
+ int driveMotorId,
+ int turnMotorId,
+ int encoderId,
+ Rotation2d encoderOffset,
+ boolean turnInverted,
+ boolean encoderInverted) {
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** Hand-written builder, so the project needs no annotation processor for this. */
+ public static final class Builder {
+ private int driveMotorId;
+ private int turnMotorId;
+ private int encoderId;
+ private Rotation2d encoderOffset = Rotation2d.ZERO;
+ private boolean turnInverted;
+ private boolean encoderInverted;
+
+ public Builder driveMotorId(int driveMotorId) {
+ this.driveMotorId = driveMotorId;
+ return this;
+ }
+
+ public Builder turnMotorId(int turnMotorId) {
+ this.turnMotorId = turnMotorId;
+ return this;
+ }
+
+ public Builder encoderId(int encoderId) {
+ this.encoderId = encoderId;
+ return this;
+ }
+
+ public Builder encoderOffset(Rotation2d encoderOffset) {
+ this.encoderOffset = encoderOffset;
+ return this;
+ }
+
+ public Builder turnInverted(boolean turnInverted) {
+ this.turnInverted = turnInverted;
+ return this;
+ }
+
+ public Builder encoderInverted(boolean encoderInverted) {
+ this.encoderInverted = encoderInverted;
+ return this;
+ }
+
+ public ModuleConfig build() {
+ return new ModuleConfig(
+ driveMotorId, turnMotorId, encoderId, encoderOffset, turnInverted, encoderInverted);
+ }
+ }
+ }
+
+ private DriveConstants() {}
+}
diff --git a/src/main/java/first/robot/subsystems/drive/GyroIO.java b/src/main/java/first/robot/subsystems/drive/GyroIO.java
new file mode 100644
index 0000000..be2518e
--- /dev/null
+++ b/src/main/java/first/robot/subsystems/drive/GyroIO.java
@@ -0,0 +1,22 @@
+// Copyright (c) 2025 FRC 6328
+// http://github.com/Mechanical-Advantage
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file at
+// the root directory of this project.
+
+package first.robot.subsystems.drive;
+
+import org.wpilib.math.geometry.Rotation2d;
+import org.littletonrobotics.junction.AutoLog;
+
+public interface GyroIO {
+ @AutoLog
+ public static class GyroIOInputs {
+ public boolean connected = false;
+ public Rotation2d yawPosition = new Rotation2d();
+ public double yawVelocityRadPerSec = 0.0;
+ }
+
+ public default void updateInputs(GyroIOInputs inputs) {}
+}
diff --git a/src/main/java/first/robot/subsystems/drive/GyroIOOnboardIMU.java b/src/main/java/first/robot/subsystems/drive/GyroIOOnboardIMU.java
new file mode 100644
index 0000000..a4c9c59
--- /dev/null
+++ b/src/main/java/first/robot/subsystems/drive/GyroIOOnboardIMU.java
@@ -0,0 +1,23 @@
+// Copyright (c) 2025 FRC 6328
+// http://github.com/Mechanical-Advantage
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file at
+// the root directory of this project.
+
+package first.robot.subsystems.drive;
+
+import org.wpilib.hardware.imu.OnboardIMU;
+import org.wpilib.hardware.imu.OnboardIMU.MountOrientation;
+
+
+public class GyroIOOnboardIMU implements GyroIO {
+ private final OnboardIMU imu = new OnboardIMU(MountOrientation.FLAT);
+
+ @Override
+ public void updateInputs(GyroIOInputs inputs) {
+ inputs.connected = true;
+ inputs.yawPosition = imu.getRotation2d();
+ inputs.yawVelocityRadPerSec = imu.getGyroRateZ();
+ }
+}
diff --git a/src/main/java/first/robot/subsystems/drive/GyroIOPigeon2.java b/src/main/java/first/robot/subsystems/drive/GyroIOPigeon2.java
new file mode 100644
index 0000000..9dc1215
--- /dev/null
+++ b/src/main/java/first/robot/subsystems/drive/GyroIOPigeon2.java
@@ -0,0 +1,46 @@
+// Copyright (c) 2025 FRC 6328
+// http://github.com/Mechanical-Advantage
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file at
+// the root directory of this project.
+
+package first.robot.subsystems.drive;
+
+import com.ctre.phoenix6.BaseStatusSignal;
+import com.ctre.phoenix6.CANBus;
+import com.ctre.phoenix6.StatusSignal;
+import com.ctre.phoenix6.configs.Pigeon2Configuration;
+import com.ctre.phoenix6.hardware.Pigeon2;
+import first.robot.Constants;
+import org.wpilib.math.geometry.Rotation2d;
+import org.wpilib.math.util.Units;
+import org.wpilib.units.measure.Angle;
+import org.wpilib.units.measure.AngularVelocity;
+
+/** IMU implementation for the CTRE Pigeon 2 on the CAN bus. */
+public class GyroIOPigeon2 implements GyroIO {
+ private final Pigeon2 pigeon =
+ new Pigeon2(DriveConstants.PigeonConstants.id, CANBus.systemcore(0));
+
+ private final StatusSignal The CANcoder is configured with the module's calibration offset, so its absolute position is
+ * already the true module heading. That value seeds the turn TalonFX's internal rotor position once
+ * at startup, after which the TalonFX closes the steer loop against its own (much faster) sensor.
+ */
+public class ModuleIOTalonFX implements ModuleIO {
+ private static final double driveCurrentLimitAmps = 80;
+ private static final double turnCurrentLimitAmps = 40;
+
+ /** SDS MK4i L2: 6.12:1 drive, 150/7:1 steer. */
+ public static final double driveReduction = (50.0 / 14.0) * (16.0 / 28.0) * (45.0 / 15.0);
+
+ public static final double turnReduction = (150.0 / 7.0);
+
+ private static final CANBus canBus = CANBus.systemcore(0);
+
+ // Hardware objects
+ private final TalonFX driveTalon;
+ private final TalonFX turnTalon;
+ private final CANcoder cancoder;
+
+ // Config
+ private final TalonFXConfiguration driveConfig = new TalonFXConfiguration();
+ private final TalonFXConfiguration turnConfig = new TalonFXConfiguration();
+ private final CANcoderConfiguration encoderConfig = new CANcoderConfiguration();
+
+ // Control requests
+ private final TorqueCurrentFOC torqueCurrentRequest = new TorqueCurrentFOC(0).withUpdateFreqHz(0);
+ private final PositionTorqueCurrentFOC positionTorqueCurrentRequest =
+ new PositionTorqueCurrentFOC(0.0).withUpdateFreqHz(0);
+ private final VelocityTorqueCurrentFOC velocityTorqueCurrentRequest =
+ new VelocityTorqueCurrentFOC(0.0).withUpdateFreqHz(0);
+ private final CoastOut coast = new CoastOut();
+
+ // Inputs from drive motor
+ private final StatusSignal The HAL, the AdvantageKit {@code Logger}, and {@code Alert} ids are all JVM-wide, so all test
+ * classes in this source set share one initialization and one {@link Drive} instance.
+ */
+public final class SimTestFixture {
+ private static final SwerveDriveKinematics kinematics =
+ new SwerveDriveKinematics(DriveConstants.moduleTranslations);
+
+ private static Drive drive;
+
+ private SimTestFixture() {}
+
+ /** Initializes the HAL, logger, and drive exactly once per JVM. */
+ public static synchronized Drive drive() {
+ if (drive == null) {
+ assertTrue(HAL.initialize(), "HAL failed to initialize");
+ DriverStationSim.setRobotMode(RobotMode.TELEOPERATED);
+ DriverStationSim.setEnabled(true);
+ DriverStationSim.setDsAttached(true);
+ DriverStationSim.notifyNewData();
+
+ // AdvantageKit refuses to start outside a LoggedRobot; this is its supported escape hatch
+ // for custom robot bases and tests.
+ Logger.AdvancedHooks.disableRobotBaseCheck();
+ Logger.disableConsoleCapture();
+ Logger.start();
+
+ drive =
+ new Drive(
+ new GyroIO() {},
+ new ModuleIOSim(),
+ new ModuleIOSim(),
+ new ModuleIOSim(),
+ new ModuleIOSim());
+ }
+ return drive;
+ }
+
+ /** Advances one AdvantageKit logging cycle so processInputs/recordOutput have a valid frame. */
+ public static void tick() {
+ Logger.AdvancedHooks.invokePeriodicBeforeUser();
+ Logger.AdvancedHooks.invokePeriodicAfterUser(0, 0);
+ }
+
+ /**
+ * Brings the drive to rest and zeroes its estimated pose, so each test starts from a known state
+ * with the field frame and the robot frame aligned.
+ */
+ public static void reset() {
+ Drive d = drive();
+ for (int i = 0; i < 150; i++) {
+ tick();
+ d.periodic();
+ d.stop();
+ }
+ tick();
+ d.periodic();
+ d.setPose(new Pose2d());
+ }
+
+ /** Robot-relative chassis velocity implied by the current module states. */
+ public static ChassisVelocities measured() {
+ return kinematics.toChassisVelocities(drive().getModuleVelocities());
+ }
+}
diff --git a/src/test/java/first/robot/commands/JoystickDriveTest.java b/src/test/java/first/robot/commands/JoystickDriveTest.java
new file mode 100644
index 0000000..1aaaef3
--- /dev/null
+++ b/src/test/java/first/robot/commands/JoystickDriveTest.java
@@ -0,0 +1,148 @@
+package first.robot.commands;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import first.robot.SimTestFixture;
+import first.robot.subsystems.drive.Drive;
+import first.robot.subsystems.drive.DriveConstants;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.wpilib.command2.Command;
+import org.wpilib.math.kinematics.ChassisVelocities;
+
+/**
+ * Verifies the driver-facing path: gamepad axis values -> {@code DriveCommands.joystickDrive} ->
+ * chassis motion. Stick values here are already negated the same way {@code RobotContainer} negates
+ * them, so {@code stickX = +1} means "left stick pushed fully forward".
+ *
+ * {@link SimTestFixture#reset()} zeroes the estimated heading before each test, so the
+ * field-relative command frame and the robot-relative measurement frame line up.
+ */
+class JoystickDriveTest {
+ private Drive drive;
+
+ // Mutable stick state, read by the command's suppliers.
+ private double stickX;
+ private double stickY;
+ private double stickOmega;
+
+ @BeforeEach
+ void setUp() {
+ drive = SimTestFixture.drive();
+ stickX = 0.0;
+ stickY = 0.0;
+ stickOmega = 0.0;
+ SimTestFixture.reset();
+ }
+
+ /** Runs the joystick drive command for the given number of robot loops. */
+ private ChassisVelocities run(int loops) {
+ Command command =
+ DriveCommands.joystickDrive(drive, () -> stickX, () -> stickY, () -> stickOmega);
+ command.initialize();
+ for (int i = 0; i < loops; i++) {
+ SimTestFixture.tick();
+ drive.periodic();
+ command.execute();
+ }
+ SimTestFixture.tick();
+ drive.periodic();
+ return SimTestFixture.measured();
+ }
+
+ @Test
+ void stickForwardDrivesForward() {
+ stickX = 1.0;
+ ChassisVelocities measured = run(200);
+ assertTrue(
+ measured.vx > DriveConstants.maxLinearSpeed * 0.7,
+ "full forward stick should drive near max speed, got " + measured.vx);
+ assertEquals(0.0, measured.vy, 0.3, "should not drift sideways");
+ assertEquals(0.0, measured.omega, 0.3, "should not rotate");
+ }
+
+ @Test
+ void stickBackDrivesBackward() {
+ stickX = -1.0;
+ ChassisVelocities measured = run(200);
+ assertTrue(
+ measured.vx < -DriveConstants.maxLinearSpeed * 0.7,
+ "full back stick should drive backward, got " + measured.vx);
+ assertEquals(0.0, measured.vy, 0.3, "should not drift sideways");
+ }
+
+ @Test
+ void stickLeftStrafesLeft() {
+ stickY = 1.0;
+ ChassisVelocities measured = run(200);
+ assertTrue(
+ measured.vy > DriveConstants.maxLinearSpeed * 0.7,
+ "full left stick should strafe left (+y), got " + measured.vy);
+ assertEquals(0.0, measured.vx, 0.3);
+ }
+
+ @Test
+ void stickRightStrafesRight() {
+ stickY = -1.0;
+ ChassisVelocities measured = run(200);
+ assertTrue(
+ measured.vy < -DriveConstants.maxLinearSpeed * 0.7,
+ "full right stick should strafe right (-y), got " + measured.vy);
+ assertEquals(0.0, measured.vx, 0.3);
+ }
+
+ @Test
+ void rightStickRotatesCounterClockwise() {
+ stickOmega = 1.0;
+ ChassisVelocities measured = run(200);
+ assertTrue(
+ measured.omega > drive.getMaxAngularSpeedRadPerSec() * 0.5,
+ "full rotation stick should spin CCW, got " + measured.omega);
+ }
+
+ @Test
+ void rightStickRotatesClockwise() {
+ stickOmega = -1.0;
+ ChassisVelocities measured = run(200);
+ assertTrue(
+ measured.omega < -drive.getMaxAngularSpeedRadPerSec() * 0.5,
+ "negative rotation stick should spin CW, got " + measured.omega);
+ }
+
+ /** Small stick noise must not move the robot. */
+ @Test
+ void deadbandIgnoresSmallInputs() {
+ stickX = 0.05;
+ stickY = 0.05;
+ stickOmega = 0.05;
+ ChassisVelocities measured = run(150);
+ assertEquals(0.0, measured.vx, 0.1, "inside the deadband the robot should hold still");
+ assertEquals(0.0, measured.vy, 0.1);
+ assertEquals(0.0, measured.omega, 0.1);
+ }
+
+ /** Diagonal stick input should drive along that diagonal. */
+ @Test
+ void diagonalStickDrivesDiagonally() {
+ stickX = 0.7071;
+ stickY = 0.7071;
+ ChassisVelocities measured = run(220);
+ assertTrue(measured.vx > 1.0, "expected forward motion, got " + measured.vx);
+ assertTrue(measured.vy > 1.0, "expected leftward motion, got " + measured.vy);
+ assertEquals(
+ measured.vx, measured.vy, 0.4, "a 45 degree stick should drive along the 45 degree line");
+ }
+
+ /** Half stick should be clearly slower than full stick (the squared response curve). */
+ @Test
+ void halfStickIsSlowerThanFullStick() {
+ stickX = 0.5;
+ double half = run(200).vx;
+ SimTestFixture.reset();
+ stickX = 1.0;
+ double full = run(200).vx;
+ assertTrue(half > 0.1, "half stick should still move the robot, got " + half);
+ assertTrue(half < full * 0.6, "half stick (" + half + ") should be well under full (" + full + ")");
+ }
+}
diff --git a/src/test/java/first/robot/subsystems/drive/DriveSimTest.java b/src/test/java/first/robot/subsystems/drive/DriveSimTest.java
new file mode 100644
index 0000000..b8d37d3
--- /dev/null
+++ b/src/test/java/first/robot/subsystems/drive/DriveSimTest.java
@@ -0,0 +1,142 @@
+package first.robot.subsystems.drive;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import first.robot.Constants;
+import first.robot.SimTestFixture;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.wpilib.math.geometry.Rotation2d;
+import org.wpilib.math.kinematics.ChassisVelocities;
+import org.wpilib.math.kinematics.SwerveModuleVelocity;
+
+/**
+ * End-to-end checks that the swerve stack actually drives: kinematics -> module optimization ->
+ * simulated motors -> measured chassis velocity. These drive robot-relative, so the measured
+ * velocity is directly comparable to the commanded one.
+ */
+class DriveSimTest {
+ private Drive drive;
+
+ @BeforeEach
+ void setUp() {
+ drive = SimTestFixture.drive();
+ SimTestFixture.reset();
+ }
+
+ /** Runs the drive at a fixed velocity for a while and returns the measured chassis velocity. */
+ private ChassisVelocities settleAt(ChassisVelocities target, int loops) {
+ for (int i = 0; i < loops; i++) {
+ SimTestFixture.tick();
+ drive.periodic();
+ drive.runVelocity(target);
+ }
+ SimTestFixture.tick();
+ drive.periodic();
+ return SimTestFixture.measured();
+ }
+
+ @Test
+ void drivesStraightForward() {
+ ChassisVelocities measured = settleAt(new ChassisVelocities(2.0, 0.0, 0.0), 200);
+ assertEquals(2.0, measured.vx, 0.25, "forward velocity should track the setpoint");
+ assertEquals(0.0, measured.vy, 0.25, "should not drift sideways");
+ assertEquals(0.0, measured.omega, 0.25, "should not rotate");
+ }
+
+ @Test
+ void drivesSideways() {
+ ChassisVelocities measured = settleAt(new ChassisVelocities(0.0, 1.5, 0.0), 200);
+ assertEquals(0.0, measured.vx, 0.25);
+ assertEquals(1.5, measured.vy, 0.25, "strafe velocity should track the setpoint");
+ assertEquals(0.0, measured.omega, 0.25);
+ }
+
+ @Test
+ void spinsInPlace() {
+ ChassisVelocities measured = settleAt(new ChassisVelocities(0.0, 0.0, 2.0), 200);
+ assertEquals(0.0, measured.vx, 0.25);
+ assertEquals(0.0, measured.vy, 0.25);
+ assertEquals(2.0, measured.omega, 0.3, "angular velocity should track the setpoint");
+ }
+
+ @Test
+ void drivesDiagonallyWhileRotating() {
+ ChassisVelocities target = new ChassisVelocities(1.5, 1.0, 1.0);
+ ChassisVelocities measured = settleAt(target, 250);
+ assertEquals(target.vx, measured.vx, 0.3);
+ assertEquals(target.vy, measured.vy, 0.3);
+ assertEquals(target.omega, measured.omega, 0.3);
+ }
+
+ /**
+ * A request beyond what the modules can do must be scaled down as a whole, not clipped per
+ * module. This is what the discarded {@code desaturateWheelVelocities} result used to break.
+ */
+ @Test
+ void desaturatesOverspeedRequests() {
+ settleAt(new ChassisVelocities(DriveConstants.maxLinearSpeed * 3.0, 0.0, 0.0), 200);
+ for (SwerveModuleVelocity state : drive.getModuleVelocities()) {
+ assertTrue(
+ Math.abs(state.velocity) <= DriveConstants.maxLinearSpeed * 1.1,
+ "module speed "
+ + state.velocity
+ + " exceeded the max of "
+ + DriveConstants.maxLinearSpeed);
+ }
+ }
+
+ /**
+ * Reversing direction must flip the wheel rather than steer the module 180 degrees. This is what
+ * the discarded {@code optimize()} result used to break.
+ */
+ @Test
+ void reversingFlipsWheelInsteadOfSteering() {
+ settleAt(new ChassisVelocities(2.0, 0.0, 0.0), 200);
+ ChassisVelocities measured = settleAt(new ChassisVelocities(-2.0, 0.0, 0.0), 200);
+
+ assertEquals(-2.0, measured.vx, 0.25, "should track the reversed setpoint");
+ for (SwerveModuleVelocity state : drive.getModuleVelocities()) {
+ // Steering stayed near the forward/backward axis; the wheel spins backwards instead.
+ double angleFromForward = Math.abs(state.angle.getDegrees());
+ assertTrue(
+ angleFromForward < 15.0 || angleFromForward > 165.0,
+ "module should not be steered sideways, was " + state.angle.getDegrees() + " deg");
+ assertTrue(
+ Math.signum(state.velocity) * Math.cos(state.angle.getRadians()) < 0.0,
+ "module should be driving backwards");
+ }
+ }
+
+ /** The period used for discretization and sim integration must match the real robot loop. */
+ @Test
+ void loopPeriodMatchesRobotPeriod() {
+ assertEquals(
+ org.littletonrobotics.junction.LoggedRobot.defaultPeriodSecs,
+ Constants.loopPeriodSecs,
+ 1e-9,
+ "Constants.loopPeriodSecs must match the period handed to LoggedRobot");
+ }
+
+ /** stopWithX must park the modules in an X and hold still. */
+ @Test
+ void stopWithXHoldsStill() {
+ settleAt(new ChassisVelocities(2.0, 0.0, 0.0), 100);
+ drive.stopWithX();
+ for (int i = 0; i < 100; i++) {
+ SimTestFixture.tick();
+ drive.periodic();
+ drive.stop();
+ }
+ SimTestFixture.tick();
+ drive.periodic();
+
+ for (SwerveModuleVelocity state : drive.getModuleVelocities()) {
+ assertEquals(0.0, state.velocity, 0.15, "modules should be stopped");
+ }
+ // Module headings should form the X pattern (45 degrees off axis).
+ Rotation2d flHeading = drive.getModuleVelocities()[0].angle;
+ assertEquals(45.0, Math.abs(flHeading.getDegrees()), 12.0, "front-left should sit at 45 deg");
+ }
+}
diff --git a/vendordeps/AdvantageKit.json b/vendordeps/AdvantageKit.json
new file mode 100644
index 0000000..1cb9533
--- /dev/null
+++ b/vendordeps/AdvantageKit.json
@@ -0,0 +1,35 @@
+{
+ "fileName": "AdvantageKit.json",
+ "name": "AdvantageKit",
+ "version": "27.0.0-alpha-5",
+ "uuid": "d820cc26-74e3-11ec-90d6-0242ac120003",
+ "wpilibYear": "2027_alpha7",
+ "mavenUrls": [
+ "https://frcmaven.wpi.edu/artifactory/littletonrobotics-mvn-release/"
+ ],
+ "jsonUrl": "https://github.com/Mechanical-Advantage/AdvantageKit/releases/latest/download/AdvantageKit.json",
+ "javaDependencies": [
+ {
+ "groupId": "org.littletonrobotics.akit",
+ "artifactId": "akit-java",
+ "version": "27.0.0-alpha-5"
+ }
+ ],
+ "jniDependencies": [
+ {
+ "groupId": "org.littletonrobotics.akit",
+ "artifactId": "akit-wpilibio",
+ "version": "27.0.0-alpha-5",
+ "skipInvalidPlatforms": false,
+ "isJar": false,
+ "validPlatforms": [
+ "linuxsystemcore",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal",
+ "windowsx86-64"
+ ]
+ }
+ ],
+ "cppDependencies": []
+}
\ No newline at end of file
diff --git a/vendordeps/CommandsV2.json b/vendordeps/CommandsV2.json
index 8358b09..ae2b96b 100644
--- a/vendordeps/CommandsV2.json
+++ b/vendordeps/CommandsV2.json
@@ -3,7 +3,7 @@
"name": "Commands V2",
"version": "1.0.0",
"uuid": "111e20f7-815e-48f8-9dd6-e675ce75b266",
- "wpilibYear": "2027_alpha5",
+ "wpilibYear": "2027_alpha7",
"mavenUrls": [],
"jsonUrl": "",
"conflictsWith": [
@@ -33,11 +33,9 @@
"skipInvalidPlatforms": true,
"binaryPlatforms": [
"linuxsystemcore",
- "linuxathena",
- "linuxarm32",
"linuxarm64",
"windowsx86-64",
- "windowsx86",
+ "windowsarm64",
"linuxx86-64",
"osxuniversal"
]
diff --git a/vendordeps/PathplannerLibSystemCoreAlpha.json b/vendordeps/PathplannerLibSystemCoreAlpha.json
new file mode 100644
index 0000000..5c3c6bb
--- /dev/null
+++ b/vendordeps/PathplannerLibSystemCoreAlpha.json
@@ -0,0 +1,37 @@
+{
+ "fileName": "PathplannerLibSystemCoreAlpha.json",
+ "name": "PathplannerLib",
+ "version": "2027.0.0-alpha-3",
+ "uuid": "1b42324f-17c6-4875-8e77-1c312bc8c786",
+ "wpilibYear": "2027_alpha7",
+ "mavenUrls": [
+ "https://3015rangerrobotics.github.io/pathplannerlib/repo"
+ ],
+ "jsonUrl": "https://3015rangerrobotics.github.io/pathplannerlib/PathplannerLibSystemCoreAlpha.json",
+ "javaDependencies": [
+ {
+ "groupId": "com.pathplanner.lib",
+ "artifactId": "PathplannerLib-java",
+ "version": "2027.0.0-alpha-3"
+ }
+ ],
+ "jniDependencies": [],
+ "cppDependencies": [
+ {
+ "groupId": "com.pathplanner.lib",
+ "artifactId": "PathplannerLib-cpp",
+ "version": "2027.0.0-alpha-3",
+ "libName": "PathplannerLib",
+ "headerClassifier": "headers",
+ "sharedLibrary": false,
+ "skipInvalidPlatforms": true,
+ "binaryPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "osxuniversal",
+ "linuxsystemcore",
+ "linuxarm64"
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/vendordeps/Phoenix6-26.50.0-alpha-1.json b/vendordeps/Phoenix6-26.50.0-alpha-1.json
new file mode 100644
index 0000000..5b7f1dc
--- /dev/null
+++ b/vendordeps/Phoenix6-26.50.0-alpha-1.json
@@ -0,0 +1,449 @@
+{
+ "fileName": "Phoenix6-26.50.0-alpha-1.json",
+ "name": "CTRE-Phoenix (v6)",
+ "version": "26.50.0-alpha-1",
+ "wpilibYear": "2027_alpha7",
+ "uuid": "e995de00-2c64-4df5-8831-c1441420ff19",
+ "mavenUrls": [
+ "https://maven.ctr-electronics.com/release/"
+ ],
+ "jsonUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix6/latest/Phoenix6-frc2027-latest.json",
+ "conflictsWith": [
+ {
+ "uuid": "e7900d8d-826f-4dca-a1ff-182f658e98af",
+ "errorMessage": "Users cannot have both the replay and regular Phoenix 6 vendordeps in their robot program.",
+ "offlineFileName": "Phoenix6-replay-frc2027-latest.json"
+ }
+ ],
+ "javaDependencies": [
+ {
+ "groupId": "com.ctre.phoenix6",
+ "artifactId": "wpiapi-java",
+ "version": "26.50.0-alpha-1"
+ }
+ ],
+ "jniDependencies": [
+ {
+ "groupId": "com.ctre.phoenix6",
+ "artifactId": "api-cpp",
+ "version": "26.50.0-alpha-1",
+ "isJar": false,
+ "skipInvalidPlatforms": true,
+ "validPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "linuxsystemcore"
+ ],
+ "simMode": "hwsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6",
+ "artifactId": "tools",
+ "version": "26.50.0-alpha-1",
+ "isJar": false,
+ "skipInvalidPlatforms": true,
+ "validPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "linuxsystemcore"
+ ],
+ "simMode": "hwsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "api-cpp-sim",
+ "version": "26.50.0-alpha-1",
+ "isJar": false,
+ "skipInvalidPlatforms": true,
+ "validPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "tools-sim",
+ "version": "26.50.0-alpha-1",
+ "isJar": false,
+ "skipInvalidPlatforms": true,
+ "validPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "simTalonSRX",
+ "version": "26.50.0-alpha-1",
+ "isJar": false,
+ "skipInvalidPlatforms": true,
+ "validPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "simVictorSPX",
+ "version": "26.50.0-alpha-1",
+ "isJar": false,
+ "skipInvalidPlatforms": true,
+ "validPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "simPigeonIMU",
+ "version": "26.50.0-alpha-1",
+ "isJar": false,
+ "skipInvalidPlatforms": true,
+ "validPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "simProTalonFX",
+ "version": "26.50.0-alpha-1",
+ "isJar": false,
+ "skipInvalidPlatforms": true,
+ "validPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "simProTalonFXS",
+ "version": "26.50.0-alpha-1",
+ "isJar": false,
+ "skipInvalidPlatforms": true,
+ "validPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "simProCANcoder",
+ "version": "26.50.0-alpha-1",
+ "isJar": false,
+ "skipInvalidPlatforms": true,
+ "validPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "simProPigeon2",
+ "version": "26.50.0-alpha-1",
+ "isJar": false,
+ "skipInvalidPlatforms": true,
+ "validPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "simProCANrange",
+ "version": "26.50.0-alpha-1",
+ "isJar": false,
+ "skipInvalidPlatforms": true,
+ "validPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "simProCANdi",
+ "version": "26.50.0-alpha-1",
+ "isJar": false,
+ "skipInvalidPlatforms": true,
+ "validPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "simProCANdle",
+ "version": "26.50.0-alpha-1",
+ "isJar": false,
+ "skipInvalidPlatforms": true,
+ "validPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ }
+ ],
+ "cppDependencies": [
+ {
+ "groupId": "com.ctre.phoenix6",
+ "artifactId": "wpiapi-cpp",
+ "version": "26.50.0-alpha-1",
+ "libName": "CTRE_Phoenix6_WPI",
+ "headerClassifier": "headers",
+ "sharedLibrary": true,
+ "skipInvalidPlatforms": true,
+ "binaryPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "linuxsystemcore"
+ ],
+ "simMode": "hwsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6",
+ "artifactId": "tools",
+ "version": "26.50.0-alpha-1",
+ "libName": "CTRE_PhoenixTools",
+ "headerClassifier": "headers",
+ "sharedLibrary": true,
+ "skipInvalidPlatforms": true,
+ "binaryPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "linuxsystemcore"
+ ],
+ "simMode": "hwsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "wpiapi-cpp-sim",
+ "version": "26.50.0-alpha-1",
+ "libName": "CTRE_Phoenix6_WPISim",
+ "headerClassifier": "headers",
+ "sharedLibrary": true,
+ "skipInvalidPlatforms": true,
+ "binaryPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "tools-sim",
+ "version": "26.50.0-alpha-1",
+ "libName": "CTRE_PhoenixTools_Sim",
+ "headerClassifier": "headers",
+ "sharedLibrary": true,
+ "skipInvalidPlatforms": true,
+ "binaryPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "simTalonSRX",
+ "version": "26.50.0-alpha-1",
+ "libName": "CTRE_SimTalonSRX",
+ "headerClassifier": "headers",
+ "sharedLibrary": true,
+ "skipInvalidPlatforms": true,
+ "binaryPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "simVictorSPX",
+ "version": "26.50.0-alpha-1",
+ "libName": "CTRE_SimVictorSPX",
+ "headerClassifier": "headers",
+ "sharedLibrary": true,
+ "skipInvalidPlatforms": true,
+ "binaryPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "simPigeonIMU",
+ "version": "26.50.0-alpha-1",
+ "libName": "CTRE_SimPigeonIMU",
+ "headerClassifier": "headers",
+ "sharedLibrary": true,
+ "skipInvalidPlatforms": true,
+ "binaryPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "simProTalonFX",
+ "version": "26.50.0-alpha-1",
+ "libName": "CTRE_SimProTalonFX",
+ "headerClassifier": "headers",
+ "sharedLibrary": true,
+ "skipInvalidPlatforms": true,
+ "binaryPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "simProTalonFXS",
+ "version": "26.50.0-alpha-1",
+ "libName": "CTRE_SimProTalonFXS",
+ "headerClassifier": "headers",
+ "sharedLibrary": true,
+ "skipInvalidPlatforms": true,
+ "binaryPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "simProCANcoder",
+ "version": "26.50.0-alpha-1",
+ "libName": "CTRE_SimProCANcoder",
+ "headerClassifier": "headers",
+ "sharedLibrary": true,
+ "skipInvalidPlatforms": true,
+ "binaryPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "simProPigeon2",
+ "version": "26.50.0-alpha-1",
+ "libName": "CTRE_SimProPigeon2",
+ "headerClassifier": "headers",
+ "sharedLibrary": true,
+ "skipInvalidPlatforms": true,
+ "binaryPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "simProCANrange",
+ "version": "26.50.0-alpha-1",
+ "libName": "CTRE_SimProCANrange",
+ "headerClassifier": "headers",
+ "sharedLibrary": true,
+ "skipInvalidPlatforms": true,
+ "binaryPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "simProCANdi",
+ "version": "26.50.0-alpha-1",
+ "libName": "CTRE_SimProCANdi",
+ "headerClassifier": "headers",
+ "sharedLibrary": true,
+ "skipInvalidPlatforms": true,
+ "binaryPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ },
+ {
+ "groupId": "com.ctre.phoenix6.sim",
+ "artifactId": "simProCANdle",
+ "version": "26.50.0-alpha-1",
+ "libName": "CTRE_SimProCANdle",
+ "headerClassifier": "headers",
+ "sharedLibrary": true,
+ "skipInvalidPlatforms": true,
+ "binaryPlatforms": [
+ "windowsx86-64",
+ "linuxx86-64",
+ "linuxarm64",
+ "osxuniversal"
+ ],
+ "simMode": "swsim"
+ }
+ ]
+}
\ No newline at end of file