diff --git a/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/sysid/SysIdRoutine.java b/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/sysid/SysIdRoutine.java new file mode 100644 index 00000000000..45f2ccf17d2 --- /dev/null +++ b/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/sysid/SysIdRoutine.java @@ -0,0 +1,235 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package edu.wpi.first.wpilibj2.command.sysid; + +import static edu.wpi.first.units.MutableMeasure.mutable; +import static edu.wpi.first.units.Units.Second; +import static edu.wpi.first.units.Units.Seconds; +import static edu.wpi.first.units.Units.Volts; +import static java.util.Map.entry; + +import edu.wpi.first.units.Measure; +import edu.wpi.first.units.MutableMeasure; +import edu.wpi.first.units.Velocity; +import edu.wpi.first.units.Voltage; +import edu.wpi.first.wpilibj.Timer; +import edu.wpi.first.wpilibj.sysid.MotorLog; +import edu.wpi.first.wpilibj.sysid.SysIdRoutineLog; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Subsystem; +import java.util.Map; +import java.util.function.Consumer; + +/** + * A SysId characterization routine for a single mechanism. Mechanisms may have multiple motors. + * + *
A single subsystem may have multiple mechanisms, but mechanisms should not share test
+ * routines. Each complete test of a mechanism should have its own SysIdRoutine instance, since the
+ * log name of the recorded data is determined by the mechanism name.
+ */
+public class SysIdRoutine extends SysIdRoutineLog {
+ private final Config m_config;
+ private final Mechanism m_mechanism;
+ private final MutableMeasure The command will call the `drive` and `log` callbacks supplied at routine construction once
+ * per iteration. Upon command end or interruption, the `drive` callback is called with a value of
+ * 0 volts.
+ *
+ * @param direction The direction in which to run the test.
+ * @return A command to run the test.
+ */
+ public Command quasistatic(Direction direction) {
+ Timer timer = new Timer();
+ double outputSign = direction == Direction.kForward ? 1.0 : -1.0;
+ State state =
+ Map.ofEntries(
+ entry(Direction.kForward, State.kQuasistaticForward),
+ entry(Direction.kReverse, State.kQuasistaticReverse))
+ .get(direction);
+
+ return m_mechanism
+ .m_subsystem
+ .runOnce(timer::start)
+ .andThen(
+ m_mechanism.m_subsystem.run(
+ () -> {
+ m_outputVolts.mut_replace(
+ outputSign * timer.get() * m_config.m_rampRate.in(Volts.per(Second)), Volts);
+ m_mechanism.m_drive.accept(m_outputVolts);
+ m_mechanism.m_log.accept(this);
+ m_recordState.accept(state);
+ }))
+ .finallyDo(
+ () -> {
+ m_mechanism.m_drive.accept(Volts.of(0));
+ m_recordState.accept(State.kNone);
+ timer.stop();
+ })
+ .withName("sysid-" + state.toString() + "-" + m_mechanism.m_name);
+ }
+
+ /**
+ * Returns a command to run a dynamic test in the specified direction.
+ *
+ * The command will call the `drive` and `log` callbacks supplied at routine construction once
+ * per iteration. Upon command end or interruption, the `drive` callback is called with a value of
+ * 0 volts.
+ *
+ * @param direction The direction in which to run the test.
+ * @return A command to run the test.
+ */
+ public Command dynamic(Direction direction) {
+ State state =
+ Map.ofEntries(
+ entry(Direction.kForward, State.kDynamicForward),
+ entry(Direction.kReverse, State.kDynamicReverse))
+ .get(direction);
+
+ return m_mechanism
+ .m_subsystem
+ .runOnce(() -> m_outputVolts.mut_replace(m_config.m_stepVoltage))
+ .andThen(
+ m_mechanism.m_subsystem.run(
+ () -> {
+ m_mechanism.m_drive.accept(m_outputVolts);
+ m_mechanism.m_log.accept(this);
+ m_recordState.accept(state);
+ }))
+ .finallyDo(
+ () -> {
+ m_mechanism.m_drive.accept(Volts.of(0));
+ m_recordState.accept(State.kNone);
+ })
+ .withName("sysid-" + state.toString() + "-" + m_mechanism.m_name);
+ }
+}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/MotorController.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/MotorController.java
index 4cd50da11d8..ca61cae00bf 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/MotorController.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/MotorController.java
@@ -30,6 +30,16 @@ default void setVoltage(double outputVolts) {
set(outputVolts / RobotController.getBatteryVoltage());
}
+ /**
+ * Returns the currently-applied voltage at the motor.
+ *
+ * @return The voltage applied to the motor, in volts. Equal to the current battery voltage times
+ * the duty cycle output.
+ */
+ default double getVoltage() {
+ return get() * RobotController.getBatteryVoltage();
+ }
+
/**
* Common interface for getting the current set speed of a motor controller.
*
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/sysid/MotorLog.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/sysid/MotorLog.java
new file mode 100644
index 00000000000..9993ee1727e
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/sysid/MotorLog.java
@@ -0,0 +1,90 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj.sysid;
+
+import static edu.wpi.first.units.Units.Meters;
+import static edu.wpi.first.units.Units.MetersPerSecond;
+import static edu.wpi.first.units.Units.Rotations;
+import static edu.wpi.first.units.Units.RotationsPerSecond;
+import static edu.wpi.first.units.Units.Volts;
+
+import edu.wpi.first.units.Angle;
+import edu.wpi.first.units.Distance;
+import edu.wpi.first.units.Measure;
+import edu.wpi.first.units.Velocity;
+import edu.wpi.first.units.Voltage;
+
+/** Handle for recording SysId motor data to WPILog. */
+@FunctionalInterface
+public interface MotorLog {
+ /**
+ * Records a frame of motor data during a SysId routine. The data are recorded as a struct under
+ * the string topic specified in motorName.
+ *
+ * @param voltage Voltage applied to the motor in volts.
+ * @param distance Total displacement of the motor, in units of distanceUnit.
+ * @param velocity Rate of change of displacement of the motor, in units of distanceUnit per
+ * second.
+ * @param motorName Name of the motor, used as the topic name for the WPILog entry for this data
+ * frame.
+ * @param distanceUnit String name of the unit used to measure distance, e.g. "meters".
+ */
+ void recordFrame(
+ double voltage, double distance, double velocity, String motorName, String distanceUnit);
+
+ /**
+ * Records a frame of motor data during a SysId routine. The data are recorded as a struct under
+ * the string topic specified in motorName. Assumes a linear distance dimension, and records all
+ * data to WPILog in meters.
+ *
+ * For best performance, MutableMeasure should be used by calling code to avoid the need to
+ * allocate during each call.
+ *
+ * @param voltage Voltage applied to the motor.
+ * @param distance Total linear displacement of the motor.
+ * @param velocity Rate of change of linear displacement of the motor.
+ * @param motorName Name of the motor, used as the topic name for the WPILog entry for this data
+ * frame.
+ */
+ default void recordFrameLinear(
+ Measure For best performance, MutableMeasure should be used by calling code to avoid the need to
+ * allocate during each call.
+ *
+ * @param voltage Voltage applied to the motor.
+ * @param angle Total angular displacement of the motor.
+ * @param velocity Rate of change of angular displacement of the motor.
+ * @param motorName Name of the motor, used as the topic name for the WPILog entry for this data
+ * frame.
+ */
+ default void recordFrameAngular(
+ Measure It is advised to statically import this class (or one of its inner classes) wherever the
+ * constants are needed, to reduce verbosity.
+ */
+public final class Constants {
+ public static final class DriveConstants {
+ public static final int kLeftMotor1Port = 0;
+ public static final int kLeftMotor2Port = 1;
+ public static final int kRightMotor1Port = 2;
+ public static final int kRightMotor2Port = 3;
+
+ public static final int[] kLeftEncoderPorts = {0, 1};
+ public static final int[] kRightEncoderPorts = {2, 3};
+ public static final boolean kLeftEncoderReversed = false;
+ public static final boolean kRightEncoderReversed = true;
+
+ public static final int kEncoderCPR = 1024;
+ public static final double kWheelDiameterMeters = Units.inchesToMeters(6);
+ public static final double kEncoderDistancePerPulse =
+ // Assumes the encoders are directly mounted on the wheel shafts
+ (kWheelDiameterMeters * Math.PI) / (double) kEncoderCPR;
+ }
+
+ public static final class ShooterConstants {
+ public static final int[] kEncoderPorts = {4, 5};
+ public static final boolean kEncoderReversed = false;
+ public static final int kEncoderCPR = 1024;
+ public static final double kEncoderDistancePerPulse =
+ // Distance units will be rotations
+ 1.0 / (double) kEncoderCPR;
+
+ public static final int kShooterMotorPort = 4;
+ public static final int kFeederMotorPort = 5;
+
+ public static final double kShooterFreeRPS = 5300;
+ public static final double kShooterTargetRPS = 4000;
+ public static final double kShooterToleranceRPS = 50;
+
+ // These are not real PID gains, and will have to be tuned for your specific robot.
+ public static final double kP = 1;
+
+ // On a real robot the feedforward constants should be empirically determined; these are
+ // reasonable guesses.
+ public static final double kSVolts = 0.05;
+ public static final double kVVoltSecondsPerRotation =
+ // Should have value 12V at free speed...
+ 12.0 / kShooterFreeRPS;
+
+ public static final double kFeederSpeed = 0.5;
+ }
+
+ public static final class IntakeConstants {
+ public static final int kMotorPort = 6;
+ public static final int[] kSolenoidPorts = {2, 3};
+ }
+
+ public static final class StorageConstants {
+ public static final int kMotorPort = 7;
+ public static final int kBallSensorPort = 6;
+ }
+
+ public static final class AutoConstants {
+ public static final double kTimeoutSeconds = 3;
+ public static final double kDriveDistanceMeters = 2;
+ public static final double kDriveSpeed = 0.5;
+ }
+
+ public static final class OIConstants {
+ public static final int kDriverControllerPort = 0;
+ }
+}
diff --git a/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/sysid/Main.java b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/sysid/Main.java
new file mode 100644
index 00000000000..1fdd4b7374c
--- /dev/null
+++ b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/sysid/Main.java
@@ -0,0 +1,25 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj.examples.sysid;
+
+import edu.wpi.first.wpilibj.RobotBase;
+
+/**
+ * Do NOT add any static variables to this class, or any initialization at all. Unless you know what
+ * you are doing, do not modify this file except to change the parameter class to the startRobot
+ * call.
+ */
+public final class Main {
+ private Main() {}
+
+ /**
+ * Main initialization function. Do not perform any initialization here.
+ *
+ * If you change your main robot class, change the parameter type.
+ */
+ public static void main(String... args) {
+ RobotBase.startRobot(Robot::new);
+ }
+}
diff --git a/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/sysid/Robot.java b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/sysid/Robot.java
new file mode 100644
index 00000000000..0b0f1428913
--- /dev/null
+++ b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/sysid/Robot.java
@@ -0,0 +1,92 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj.examples.sysid;
+
+import edu.wpi.first.wpilibj.TimedRobot;
+import edu.wpi.first.wpilibj2.command.Command;
+import edu.wpi.first.wpilibj2.command.CommandScheduler;
+
+/**
+ * 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 {
+ private Command m_autonomousCommand;
+
+ private final SysIdRoutineBot m_robot = new SysIdRoutineBot();
+
+ /**
+ * This function is run when the robot is first started up and should be used for any
+ * initialization code.
+ */
+ @Override
+ public void robotInit() {
+ // Configure default commands and condition bindings on robot startup
+ m_robot.configureBindings();
+ }
+
+ /**
+ * This function is called every robot packet, no matter the mode. Use this for items like
+ * diagnostics that you want ran during disabled, autonomous, teleoperated and test.
+ *
+ * This runs after the mode specific periodic functions, but before LiveWindow and
+ * SmartDashboard integrated updating.
+ */
+ @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.
+ CommandScheduler.getInstance().run();
+ }
+
+ /** This function is called once each time the robot enters Disabled mode. */
+ @Override
+ public void disabledInit() {}
+
+ @Override
+ public void disabledPeriodic() {}
+
+ @Override
+ public void autonomousInit() {
+ m_autonomousCommand = m_robot.getAutonomousCommand();
+
+ if (m_autonomousCommand != null) {
+ m_autonomousCommand.schedule();
+ }
+ }
+
+ /** This function is called periodically during autonomous. */
+ @Override
+ public void autonomousPeriodic() {}
+
+ @Override
+ public void teleopInit() {
+ // This makes sure that the autonomous stops running when
+ // teleop starts running. If you want the autonomous to
+ // continue until interrupted by another command, remove
+ // this line or comment it out.
+ if (m_autonomousCommand != null) {
+ m_autonomousCommand.cancel();
+ }
+ }
+
+ /** This function is called periodically during operator control. */
+ @Override
+ public void teleopPeriodic() {}
+
+ @Override
+ public void testInit() {
+ // Cancels all running commands at the start of test mode.
+ CommandScheduler.getInstance().cancelAll();
+ }
+
+ /** This function is called periodically during test mode. */
+ @Override
+ public void testPeriodic() {}
+}
diff --git a/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/sysid/SysIdRoutineBot.java b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/sysid/SysIdRoutineBot.java
new file mode 100644
index 00000000000..6837ffc8f21
--- /dev/null
+++ b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/sysid/SysIdRoutineBot.java
@@ -0,0 +1,58 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj.examples.sysid;
+
+import static edu.wpi.first.wpilibj.examples.sysid.Constants.OIConstants;
+
+import edu.wpi.first.wpilibj.examples.sysid.subsystems.Drive;
+import edu.wpi.first.wpilibj2.command.Command;
+import edu.wpi.first.wpilibj2.command.button.CommandXboxController;
+import edu.wpi.first.wpilibj2.command.button.Trigger;
+import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine;
+
+/**
+ * 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 button mappings) should be declared here.
+ */
+public class SysIdRoutineBot {
+ // The robot's subsystems
+ private final Drive m_drive = new Drive();
+
+ // The driver's controller
+ CommandXboxController m_driverController =
+ new CommandXboxController(OIConstants.kDriverControllerPort);
+
+ /**
+ * Use this method to define bindings between conditions and commands. These are useful for
+ * automating robot behaviors based on button and sensor input.
+ *
+ * Should be called during {@link Robot#robotInit()}.
+ *
+ * Event binding methods are available on the {@link Trigger} class.
+ */
+ public void configureBindings() {
+ // Control the drive with split-stick arcade controls
+ m_drive.setDefaultCommand(
+ m_drive.arcadeDriveCommand(
+ () -> -m_driverController.getLeftY(), () -> -m_driverController.getRightX()));
+
+ m_driverController.a().whileTrue(m_drive.sysIdQuasistatic(SysIdRoutine.Direction.kForward));
+ m_driverController.b().whileTrue(m_drive.sysIdQuasistatic(SysIdRoutine.Direction.kReverse));
+ m_driverController.x().whileTrue(m_drive.sysIdDynamic(SysIdRoutine.Direction.kForward));
+ m_driverController.y().whileTrue(m_drive.sysIdDynamic(SysIdRoutine.Direction.kReverse));
+ }
+
+ /**
+ * Use this to define the command that runs during autonomous.
+ *
+ * Scheduled during {@link Robot#autonomousInit()}.
+ */
+ public Command getAutonomousCommand() {
+ // Do nothing
+ return m_drive.run(() -> {});
+ }
+}
diff --git a/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/sysid/subsystems/Drive.java b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/sysid/subsystems/Drive.java
new file mode 100644
index 00000000000..f387a133fc6
--- /dev/null
+++ b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/sysid/subsystems/Drive.java
@@ -0,0 +1,128 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj.examples.sysid.subsystems;
+
+import static edu.wpi.first.units.MutableMeasure.mutable;
+import static edu.wpi.first.units.Units.Meters;
+import static edu.wpi.first.units.Units.MetersPerSecond;
+import static edu.wpi.first.units.Units.Volts;
+
+import edu.wpi.first.units.Distance;
+import edu.wpi.first.units.Measure;
+import edu.wpi.first.units.MutableMeasure;
+import edu.wpi.first.units.Velocity;
+import edu.wpi.first.units.Voltage;
+import edu.wpi.first.wpilibj.Encoder;
+import edu.wpi.first.wpilibj.drive.DifferentialDrive;
+import edu.wpi.first.wpilibj.examples.rapidreactcommandbot.Constants.DriveConstants;
+import edu.wpi.first.wpilibj.motorcontrol.MotorControllerGroup;
+import edu.wpi.first.wpilibj.motorcontrol.PWMSparkMax;
+import edu.wpi.first.wpilibj.sysid.MotorLog;
+import edu.wpi.first.wpilibj2.command.Command;
+import edu.wpi.first.wpilibj2.command.SubsystemBase;
+import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine;
+import java.util.function.DoubleSupplier;
+
+public class Drive extends SubsystemBase {
+ // The motors on the left side of the drive.
+ private final MotorControllerGroup m_leftMotors =
+ new MotorControllerGroup(
+ new PWMSparkMax(DriveConstants.kLeftMotor1Port),
+ new PWMSparkMax(DriveConstants.kLeftMotor2Port));
+
+ // The motors on the right side of the drive.
+ private final MotorControllerGroup m_rightMotors =
+ new MotorControllerGroup(
+ new PWMSparkMax(DriveConstants.kRightMotor1Port),
+ new PWMSparkMax(DriveConstants.kRightMotor2Port));
+
+ // The robot's drive
+ private final DifferentialDrive m_drive = new DifferentialDrive(m_leftMotors, m_rightMotors);
+
+ // The left-side drive encoder
+ private final Encoder m_leftEncoder =
+ new Encoder(
+ DriveConstants.kLeftEncoderPorts[0],
+ DriveConstants.kLeftEncoderPorts[1],
+ DriveConstants.kLeftEncoderReversed);
+
+ // The right-side drive encoder
+ private final Encoder m_rightEncoder =
+ new Encoder(
+ DriveConstants.kRightEncoderPorts[0],
+ DriveConstants.kRightEncoderPorts[1],
+ DriveConstants.kRightEncoderReversed);
+
+ // Mutable holder for unit-safe voltage values, persisted to avoid reallocation.
+ private final MutableMeasure