mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-20 00:51:42 +00:00
[wpilib] Add physics simulation support with state-space (#2615)
This includes physics simulation support for arms/elevator models, as well as differential drivetrains. Swerve might be added at a later date. Co-authored-by: Claudius Tewari <cttewari@gmail.com> Co-authored-by: Prateek Machiraju <prateek.machiraju@gmail.com> Co-authored-by: Tyler Veness <calcmogul@gmail.com>
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.examples.armsimulation;
|
||||
|
||||
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.
|
||||
*
|
||||
* <p>If you change your main robot class, change the parameter type.
|
||||
*/
|
||||
public static void main(String... args) {
|
||||
RobotBase.startRobot(Robot::new);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.examples.armsimulation;
|
||||
|
||||
import edu.wpi.first.wpilibj.Encoder;
|
||||
import edu.wpi.first.wpilibj.Joystick;
|
||||
import edu.wpi.first.wpilibj.PWMVictorSPX;
|
||||
import edu.wpi.first.wpilibj.RobotController;
|
||||
import edu.wpi.first.wpilibj.TimedRobot;
|
||||
import edu.wpi.first.wpilibj.controller.PIDController;
|
||||
import edu.wpi.first.wpilibj.simulation.EncoderSim;
|
||||
import edu.wpi.first.wpilibj.simulation.RoboRioSim;
|
||||
import edu.wpi.first.wpilibj.simulation.BatterySim;
|
||||
import edu.wpi.first.wpilibj.simulation.SingleJointedArmSim;
|
||||
import edu.wpi.first.wpilibj.system.plant.DCMotor;
|
||||
import edu.wpi.first.wpilibj.util.Units;
|
||||
|
||||
/**
|
||||
* This is a sample program to demonstrate the use of elevator simulation with existing code.
|
||||
*/
|
||||
public class Robot extends TimedRobot {
|
||||
private static final int kMotorPort = 0;
|
||||
private static final int kEncoderAChannel = 0;
|
||||
private static final int kEncoderBChannel = 1;
|
||||
private static final int kJoystickPort = 0;
|
||||
|
||||
// The P gain for the PID controller that drives this arm.
|
||||
private static final double kArmKp = 5.0;
|
||||
|
||||
// distance per pulse = (angle per revolution) / (pulses per revolution)
|
||||
// = (2 * PI rads) / (4096 pulses)
|
||||
private static final double kArmEncoderDistPerPulse = 2.0 * Math.PI / 4096;
|
||||
|
||||
// The arm gearbox represents a gerbox containing two Vex 775pro motors.
|
||||
private final DCMotor m_armGearbox = DCMotor.getVex775Pro(2);
|
||||
|
||||
// Standard classes for controlling our elevator
|
||||
private final PIDController m_controller = new PIDController(kArmKp, 0, 0);
|
||||
private final Encoder m_encoder = new Encoder(kEncoderAChannel, kEncoderBChannel);
|
||||
private final PWMVictorSPX m_motor = new PWMVictorSPX(kMotorPort);
|
||||
private final Joystick m_joystick = new Joystick(kJoystickPort);
|
||||
|
||||
// Simulation classes help us simulate what's going on, including gravity.
|
||||
private static final double m_armReduction = 100;
|
||||
private static final double m_armMass = 5.0; // Kilograms
|
||||
private static final double m_armLength = Units.inchesToMeters(30);
|
||||
// This arm sim represents an arm that can travel from -180 degrees (rotated straight backwards)
|
||||
// to 0 degrees (rotated straight forwards).
|
||||
private final SingleJointedArmSim m_armSim = new SingleJointedArmSim(m_armGearbox,
|
||||
m_armReduction,
|
||||
m_armMass,
|
||||
m_armLength,
|
||||
Units.degreesToRadians(-180),
|
||||
Units.degreesToRadians(0),
|
||||
Units.degreesToRadians(0.5) // Add noise with a standard deviation of 0.5 degrees
|
||||
);
|
||||
private final EncoderSim m_encoderSim = new EncoderSim(m_encoder);
|
||||
|
||||
@Override
|
||||
public void robotInit() {
|
||||
m_encoder.setDistancePerPulse(kArmEncoderDistPerPulse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void simulationPeriodic() {
|
||||
// In this method, we update our simulation of what our elevator is doing
|
||||
// First, we set our "inputs" (voltages)
|
||||
m_armSim.setInput(m_motor.get() * RobotController.getBatteryVoltage());
|
||||
|
||||
// Next, we update it. The standard loop time is 20ms.
|
||||
m_armSim.update(0.020);
|
||||
|
||||
// Finally, we set our simulated encoder's readings and simulated battery voltage
|
||||
m_encoderSim.setDistance(m_armSim.getAngleRads());
|
||||
// SimBattery estimates loaded battery voltages
|
||||
RoboRioSim.setVInVoltage(BatterySim.calculateDefaultBatteryLoadedVoltage(m_armSim.getCurrentDrawAmps()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teleopPeriodic() {
|
||||
if (m_joystick.getTrigger()) {
|
||||
// Here, we run PID control like normal, with a constant setpoint of 30in.
|
||||
var pidOutput = m_controller.calculate(m_encoder.getDistance(), Units.degreesToRadians(0));
|
||||
m_motor.setVoltage(pidOutput);
|
||||
} else {
|
||||
// Otherwise, we disable the motor.
|
||||
m_motor.set(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disabledInit() {
|
||||
// This just makes sure that our simulation code knows that the motor's off.
|
||||
m_motor.set(0.0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.examples.elevatorsimulation;
|
||||
|
||||
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.
|
||||
*
|
||||
* <p>If you change your main robot class, change the parameter type.
|
||||
*/
|
||||
public static void main(String... args) {
|
||||
RobotBase.startRobot(Robot::new);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.examples.elevatorsimulation;
|
||||
|
||||
import edu.wpi.first.wpilibj.Encoder;
|
||||
import edu.wpi.first.wpilibj.Joystick;
|
||||
import edu.wpi.first.wpilibj.PWMVictorSPX;
|
||||
import edu.wpi.first.wpilibj.RobotController;
|
||||
import edu.wpi.first.wpilibj.TimedRobot;
|
||||
import edu.wpi.first.wpilibj.controller.PIDController;
|
||||
import edu.wpi.first.wpilibj.simulation.EncoderSim;
|
||||
import edu.wpi.first.wpilibj.simulation.RoboRioSim;
|
||||
import edu.wpi.first.wpilibj.simulation.BatterySim;
|
||||
import edu.wpi.first.wpilibj.simulation.ElevatorSim;
|
||||
import edu.wpi.first.wpilibj.system.plant.DCMotor;
|
||||
import edu.wpi.first.wpilibj.util.Units;
|
||||
import edu.wpi.first.wpiutil.math.VecBuilder;
|
||||
|
||||
/**
|
||||
* This is a sample program to demonstrate the use of elevator simulation with existing code.
|
||||
*/
|
||||
public class Robot extends TimedRobot {
|
||||
private static final int kMotorPort = 0;
|
||||
private static final int kEncoderAChannel = 0;
|
||||
private static final int kEncoderBChannel = 1;
|
||||
private static final int kJoystickPort = 0;
|
||||
|
||||
private static final double kElevatorKp = 5.0;
|
||||
private static final double kElevatorGearing = 10.0;
|
||||
private static final double kElevatorDrumRadius = Units.inchesToMeters(2.0);
|
||||
private static final double kCarriageMass = 4.0; // kg
|
||||
|
||||
private static final double kMinElevatorHeight = 0.0;
|
||||
private static final double kMaxElevatorHeight = Units.inchesToMeters(50);
|
||||
|
||||
// distance per pulse = (distance per revolution) / (pulses per revolution)
|
||||
// = (Pi * D) / ppr
|
||||
private static final double kElevatorEncoderDistPerPulse = 2.0 * Math.PI * kElevatorDrumRadius / 4096;
|
||||
|
||||
private final DCMotor m_elevatorGearbox = DCMotor.getVex775Pro(4);
|
||||
|
||||
// Standard classes for controlling our elevator
|
||||
private final PIDController m_controller = new PIDController(kElevatorKp, 0, 0);
|
||||
private final Encoder m_encoder = new Encoder(kEncoderAChannel, kEncoderBChannel);
|
||||
private final PWMVictorSPX m_motor = new PWMVictorSPX(kMotorPort);
|
||||
private final Joystick m_joystick = new Joystick(kJoystickPort);
|
||||
|
||||
// Simulation classes help us simulate what's going on, including gravity.
|
||||
private final ElevatorSim m_elevatorSim = new ElevatorSim(m_elevatorGearbox,
|
||||
kCarriageMass,
|
||||
kElevatorDrumRadius,
|
||||
kElevatorGearing,
|
||||
kMinElevatorHeight,
|
||||
kMaxElevatorHeight,
|
||||
VecBuilder.fill(0.01));
|
||||
private final EncoderSim m_encoderSim = new EncoderSim(m_encoder);
|
||||
|
||||
@Override
|
||||
public void robotInit() {
|
||||
m_encoder.setDistancePerPulse(kElevatorEncoderDistPerPulse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void simulationPeriodic() {
|
||||
// In this method, we update our simulation of what our elevator is doing
|
||||
// First, we set our "inputs" (voltages)
|
||||
m_elevatorSim.setInput(m_motor.get() * RobotController.getBatteryVoltage());
|
||||
|
||||
// Next, we update it. The standard loop time is 20ms.
|
||||
m_elevatorSim.update(0.020);
|
||||
|
||||
// Finally, we set our simulated encoder's readings and simulated battery voltage
|
||||
m_encoderSim.setDistance(m_elevatorSim.getPositionMeters());
|
||||
// SimBattery estimates loaded battery voltages
|
||||
RoboRioSim.setVInVoltage(BatterySim.calculateDefaultBatteryLoadedVoltage(m_elevatorSim.getCurrentDrawAmps()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teleopPeriodic() {
|
||||
if (m_joystick.getTrigger()) {
|
||||
// Here, we run PID control like normal, with a constant setpoint of 30in.
|
||||
double pidOutput = m_controller.calculate(m_encoder.getDistance(), Units.inchesToMeters(30));
|
||||
m_motor.setVoltage(pidOutput);
|
||||
} else {
|
||||
// Otherwise, we disable the motor.
|
||||
m_motor.set(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disabledInit() {
|
||||
// This just makes sure that our simulation code knows that the motor's off.
|
||||
m_motor.set(0.0);
|
||||
}
|
||||
}
|
||||
@@ -631,5 +631,54 @@
|
||||
"gradlebase": "java",
|
||||
"mainclass": "Main",
|
||||
"commandversion": 2
|
||||
},
|
||||
{
|
||||
"name": "StateSpaceSimulation",
|
||||
"description": "An example of drivetrain simulation in combination with a RAMSETE path following controller.",
|
||||
"tags": [
|
||||
"Drivetrain",
|
||||
"State space",
|
||||
"Digital",
|
||||
"Sensors",
|
||||
"Actuators",
|
||||
"Joystick",
|
||||
"Simulation"
|
||||
],
|
||||
"foldername": "statespacedifferentialdrivesimulation",
|
||||
"gradlebase": "java",
|
||||
"mainclass": "Main",
|
||||
"commandversion": 2
|
||||
},
|
||||
{
|
||||
"name": "ElevatorSimulation",
|
||||
"description": "Demonstrates the use of physics simulation with a simple elevator.",
|
||||
"tags": [
|
||||
"Elevator",
|
||||
"State space",
|
||||
"Digital",
|
||||
"Sensors",
|
||||
"Simulation",
|
||||
"Physics"
|
||||
],
|
||||
"foldername": "elevatorsimulation",
|
||||
"gradlebase": "java",
|
||||
"mainclass": "Main",
|
||||
"commandversion": 2
|
||||
},
|
||||
{
|
||||
"name": "ArmSimulation",
|
||||
"description": "Demonstrates the use of physics simulation with a simple single-jointedarm.",
|
||||
"tags": [
|
||||
"Arm",
|
||||
"State space",
|
||||
"Digital",
|
||||
"Sensors",
|
||||
"Simulation",
|
||||
"Physics"
|
||||
],
|
||||
"foldername": "armsimulation",
|
||||
"gradlebase": "java",
|
||||
"mainclass": "Main",
|
||||
"commandversion": 2
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.examples.statespacedifferentialdrivesimulation;
|
||||
|
||||
import edu.wpi.first.wpilibj.kinematics.DifferentialDriveKinematics;
|
||||
import edu.wpi.first.wpilibj.system.LinearSystem;
|
||||
import edu.wpi.first.wpilibj.system.plant.DCMotor;
|
||||
import edu.wpi.first.wpilibj.system.plant.LinearSystemId;
|
||||
import edu.wpi.first.wpiutil.math.numbers.N2;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* <p>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 = new int[]{0, 1};
|
||||
public static final int[] kRightEncoderPorts = new int[]{2, 3};
|
||||
public static final boolean kLeftEncoderReversed = false;
|
||||
public static final boolean kRightEncoderReversed = true;
|
||||
|
||||
public static final double kTrackwidthMeters = 0.69;
|
||||
public static final DifferentialDriveKinematics kDriveKinematics =
|
||||
new DifferentialDriveKinematics(kTrackwidthMeters);
|
||||
|
||||
public static final int kEncoderCPR = 1024;
|
||||
public static final double kWheelDiameterMeters = 0.15;
|
||||
public static final double kEncoderDistancePerPulse =
|
||||
// Assumes the encoders are directly mounted on the wheel shafts
|
||||
(kWheelDiameterMeters * Math.PI) / (double) kEncoderCPR;
|
||||
|
||||
public static final boolean kGyroReversed = true;
|
||||
|
||||
// These are example values only - DO NOT USE THESE FOR YOUR OWN ROBOT!
|
||||
// These characterization values MUST be determined either experimentally or theoretically
|
||||
// for *your* robot's drive.
|
||||
// The Robot Characterization Toolsuite provides a convenient tool for obtaining these
|
||||
// values for your robot.
|
||||
public static final double ksVolts = 0.22;
|
||||
public static final double kvVoltSecondsPerMeter = 1.98;
|
||||
public static final double kaVoltSecondsSquaredPerMeter = 0.2;
|
||||
|
||||
// These are example values only - DO NOT USE THESE FOR YOUR OWN ROBOT!
|
||||
// These characterization values MUST be determined either experimentally or theoretically
|
||||
// for *your* robot's drive.
|
||||
// These two values are "angular" kV and kA
|
||||
public static final double kvVoltSecondsPerRadian = 1.5;
|
||||
public static final double kaVoltSecondsSquaredPerRadian = 0.3;
|
||||
|
||||
public static final LinearSystem<N2, N2, N2> kDrivetrainPlant =
|
||||
LinearSystemId.identifyDrivetrainSystem(kvVoltSecondsPerMeter, kaVoltSecondsSquaredPerMeter,
|
||||
kvVoltSecondsPerRadian, kaVoltSecondsSquaredPerRadian);
|
||||
|
||||
// Example values only -- use what's on your physical robot!
|
||||
public static final DCMotor kDriveGearbox = DCMotor.getCIM(2);
|
||||
public static final double kDriveGearing = 8;
|
||||
|
||||
// Example value only - as above, this must be tuned for your drive!
|
||||
public static final double kPDriveVel = 0.1;
|
||||
}
|
||||
|
||||
public static final class OIConstants {
|
||||
public static final int kDriverControllerPort = 0;
|
||||
}
|
||||
|
||||
public static final class AutoConstants {
|
||||
public static final double kMaxSpeedMetersPerSecond = 3;
|
||||
public static final double kMaxAccelerationMetersPerSecondSquared = 6;
|
||||
|
||||
// Reasonable baseline values for a RAMSETE follower in units of meters and seconds
|
||||
public static final double kRamseteB = 2;
|
||||
public static final double kRamseteZeta = 0.7;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.examples.statespacedifferentialdrivesimulation;
|
||||
|
||||
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.
|
||||
*
|
||||
* <p>If you change your main robot class, change the parameter type.
|
||||
*/
|
||||
public static void main(String... args) {
|
||||
RobotBase.startRobot(Robot::new);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.examples.statespacedifferentialdrivesimulation;
|
||||
|
||||
import edu.wpi.first.wpilibj.TimedRobot;
|
||||
import edu.wpi.first.wpilibj.simulation.RoboRioSim;
|
||||
import edu.wpi.first.wpilibj.simulation.BatterySim;
|
||||
import edu.wpi.first.wpilibj2.command.CommandScheduler;
|
||||
|
||||
/**
|
||||
* This is a sample program to demonstrate the use of state-space classes in robot simulation.
|
||||
* This robot has a flywheel, elevator, arm and differential drivetrain, and interfaces with
|
||||
* the sim GUI's {@link edu.wpi.first.wpilibj.simulation.Field2d} class.
|
||||
*/
|
||||
public class Robot extends TimedRobot {
|
||||
private RobotContainer m_robotContainer;
|
||||
|
||||
/**
|
||||
* This function is run when the robot is first started up and should be used for any
|
||||
* initialization code.
|
||||
*/
|
||||
@Override
|
||||
public void robotInit() {
|
||||
// Instantiate our RobotContainer. This will perform all our button bindings, and put our
|
||||
// autonomous chooser on the dashboard.
|
||||
m_robotContainer = new RobotContainer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void simulationPeriodic() {
|
||||
// Here we calculate the battery voltage based on drawn current.
|
||||
// As our robot draws more power from the battery its voltage drops.
|
||||
// The estimated voltage is highly dependent on the battery's internal
|
||||
// resistance.
|
||||
double drawCurrent = m_robotContainer.getRobotDrive().getDrawnCurrentAmps();
|
||||
double loadedVoltage = BatterySim.calculateDefaultBatteryLoadedVoltage(drawCurrent);
|
||||
RoboRioSim.setVInVoltage(loadedVoltage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void robotPeriodic() {
|
||||
CommandScheduler.getInstance().run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void autonomousInit() {
|
||||
m_robotContainer.getAutonomousCommand().schedule();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disabledInit() {
|
||||
CommandScheduler.getInstance().cancelAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.examples.statespacedifferentialdrivesimulation;
|
||||
|
||||
import edu.wpi.first.wpilibj.GenericHID;
|
||||
import edu.wpi.first.wpilibj.XboxController;
|
||||
import edu.wpi.first.wpilibj.controller.PIDController;
|
||||
import edu.wpi.first.wpilibj.controller.RamseteController;
|
||||
import edu.wpi.first.wpilibj.controller.SimpleMotorFeedforward;
|
||||
import edu.wpi.first.wpilibj.examples.statespacedifferentialdrivesimulation.subsystems.DriveSubsystem;
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Rotation2d;
|
||||
import edu.wpi.first.wpilibj.trajectory.Trajectory;
|
||||
import edu.wpi.first.wpilibj.trajectory.TrajectoryConfig;
|
||||
import edu.wpi.first.wpilibj.trajectory.TrajectoryGenerator;
|
||||
import edu.wpi.first.wpilibj.trajectory.constraint.DifferentialDriveVoltageConstraint;
|
||||
import edu.wpi.first.wpilibj2.command.Command;
|
||||
import edu.wpi.first.wpilibj2.command.RamseteCommand;
|
||||
import edu.wpi.first.wpilibj2.command.RunCommand;
|
||||
import edu.wpi.first.wpilibj2.command.button.JoystickButton;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static edu.wpi.first.wpilibj.XboxController.Button;
|
||||
|
||||
/**
|
||||
* 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 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 RobotContainer {
|
||||
// The robot's subsystems
|
||||
private final DriveSubsystem m_robotDrive = new DriveSubsystem();
|
||||
|
||||
// The driver's controller
|
||||
XboxController m_driverController = new XboxController(Constants.OIConstants.kDriverControllerPort);
|
||||
|
||||
/**
|
||||
* The container for the robot. Contains subsystems, OI devices, and commands.
|
||||
*/
|
||||
public RobotContainer() {
|
||||
// Configure the button bindings
|
||||
configureButtonBindings();
|
||||
|
||||
// Configure default commands
|
||||
// Set the default drive command to split-stick arcade drive
|
||||
m_robotDrive.setDefaultCommand(
|
||||
// A split-stick arcade command, with forward/backward controlled by the left
|
||||
// hand, and turning controlled by the right.
|
||||
new RunCommand(() -> m_robotDrive
|
||||
.arcadeDrive(-m_driverController.getY(GenericHID.Hand.kRight),
|
||||
m_driverController.getX(GenericHID.Hand.kLeft)), m_robotDrive));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this method to define your button->command mappings. Buttons can be created by
|
||||
* instantiating a {@link GenericHID} or one of its subclasses ({@link
|
||||
* edu.wpi.first.wpilibj.Joystick} or {@link XboxController}), and then calling passing it to a
|
||||
* {@link JoystickButton}.
|
||||
*/
|
||||
private void configureButtonBindings() {
|
||||
// Drive at half speed when the right bumper is held
|
||||
new JoystickButton(m_driverController, Button.kBumperRight.value)
|
||||
.whenPressed(() -> m_robotDrive.setMaxOutput(0.5))
|
||||
.whenReleased(() -> m_robotDrive.setMaxOutput(1));
|
||||
|
||||
}
|
||||
|
||||
public DriveSubsystem getRobotDrive() {
|
||||
return m_robotDrive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this to pass the autonomous command to the main {@link Robot} class.
|
||||
*
|
||||
* @return the command to run in autonomous
|
||||
*/
|
||||
public Command getAutonomousCommand() {
|
||||
|
||||
// Create a voltage constraint to ensure we don't accelerate too fast
|
||||
var autoVoltageConstraint =
|
||||
new DifferentialDriveVoltageConstraint(
|
||||
new SimpleMotorFeedforward(Constants.DriveConstants.ksVolts,
|
||||
Constants.DriveConstants.kvVoltSecondsPerMeter,
|
||||
Constants.DriveConstants.kaVoltSecondsSquaredPerMeter),
|
||||
Constants.DriveConstants.kDriveKinematics,
|
||||
7);
|
||||
|
||||
// Create config for trajectory
|
||||
TrajectoryConfig config =
|
||||
new TrajectoryConfig(Constants.AutoConstants.kMaxSpeedMetersPerSecond,
|
||||
Constants.AutoConstants.kMaxAccelerationMetersPerSecondSquared)
|
||||
// Add kinematics to ensure max speed is actually obeyed
|
||||
.setKinematics(Constants.DriveConstants.kDriveKinematics)
|
||||
// Apply the voltage constraint
|
||||
.addConstraint(autoVoltageConstraint);
|
||||
|
||||
// An example trajectory to follow. All units in meters.
|
||||
Trajectory exampleTrajectory = TrajectoryGenerator.generateTrajectory(
|
||||
// Start at the origin facing the +X direction
|
||||
new Pose2d(0, 0, new Rotation2d(0)),
|
||||
// Pass through these two interior waypoints, making an 's' curve path
|
||||
List.of(),
|
||||
// End 3 meters straight ahead of where we started, facing forward
|
||||
new Pose2d(6, 6, new Rotation2d(0)),
|
||||
// Pass config
|
||||
config
|
||||
);
|
||||
|
||||
RamseteCommand ramseteCommand = new RamseteCommand(
|
||||
exampleTrajectory,
|
||||
m_robotDrive::getPose,
|
||||
new RamseteController(Constants.AutoConstants.kRamseteB, Constants.AutoConstants.kRamseteZeta),
|
||||
new SimpleMotorFeedforward(Constants.DriveConstants.ksVolts,
|
||||
Constants.DriveConstants.kvVoltSecondsPerMeter,
|
||||
Constants.DriveConstants.kaVoltSecondsSquaredPerMeter),
|
||||
Constants.DriveConstants.kDriveKinematics,
|
||||
m_robotDrive::getWheelSpeeds,
|
||||
new PIDController(Constants.DriveConstants.kPDriveVel, 0, 0),
|
||||
new PIDController(Constants.DriveConstants.kPDriveVel, 0, 0),
|
||||
// RamseteCommand passes volts to the callback
|
||||
m_robotDrive::tankDriveVolts,
|
||||
m_robotDrive
|
||||
);
|
||||
|
||||
// Run path following command, then stop at the end.
|
||||
return ramseteCommand.andThen(() -> m_robotDrive.tankDriveVolts(0, 0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.examples.statespacedifferentialdrivesimulation.subsystems;
|
||||
|
||||
import edu.wpi.first.hal.SimDouble;
|
||||
import edu.wpi.first.wpilibj.ADXRS450_Gyro;
|
||||
import edu.wpi.first.wpilibj.Encoder;
|
||||
import edu.wpi.first.wpilibj.PWMVictorSPX;
|
||||
import edu.wpi.first.wpilibj.RobotBase;
|
||||
import edu.wpi.first.wpilibj.RobotController;
|
||||
import edu.wpi.first.wpilibj.SPI;
|
||||
import edu.wpi.first.wpilibj.SpeedControllerGroup;
|
||||
import edu.wpi.first.wpilibj.drive.DifferentialDrive;
|
||||
import edu.wpi.first.wpilibj.examples.statespacedifferentialdrivesimulation.Constants;
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Rotation2d;
|
||||
import edu.wpi.first.wpilibj.interfaces.Gyro;
|
||||
import edu.wpi.first.wpilibj.kinematics.DifferentialDriveOdometry;
|
||||
import edu.wpi.first.wpilibj.kinematics.DifferentialDriveWheelSpeeds;
|
||||
import edu.wpi.first.wpilibj.simulation.EncoderSim;
|
||||
import edu.wpi.first.wpilibj.simulation.SimDeviceSim;
|
||||
import edu.wpi.first.wpilibj.simulation.DifferentialDrivetrainSim;
|
||||
import edu.wpi.first.wpilibj.simulation.Field2d;
|
||||
import edu.wpi.first.wpilibj2.command.SubsystemBase;
|
||||
|
||||
public class DriveSubsystem extends SubsystemBase {
|
||||
// The motors on the left side of the drive.
|
||||
private final SpeedControllerGroup m_leftMotors =
|
||||
new SpeedControllerGroup(new PWMVictorSPX(Constants.DriveConstants.kLeftMotor1Port),
|
||||
new PWMVictorSPX(Constants.DriveConstants.kLeftMotor2Port));
|
||||
|
||||
// The motors on the right side of the drive.
|
||||
private final SpeedControllerGroup m_rightMotors =
|
||||
new SpeedControllerGroup(new PWMVictorSPX(Constants.DriveConstants.kRightMotor1Port),
|
||||
new PWMVictorSPX(Constants.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(Constants.DriveConstants.kLeftEncoderPorts[0],
|
||||
Constants.DriveConstants.kLeftEncoderPorts[1],
|
||||
Constants.DriveConstants.kLeftEncoderReversed);
|
||||
|
||||
// The right-side drive encoder
|
||||
private final Encoder m_rightEncoder =
|
||||
new Encoder(Constants.DriveConstants.kRightEncoderPorts[0],
|
||||
Constants.DriveConstants.kRightEncoderPorts[1],
|
||||
Constants.DriveConstants.kRightEncoderReversed);
|
||||
|
||||
// The gyro sensor
|
||||
private final Gyro m_gyro = new ADXRS450_Gyro();
|
||||
|
||||
// Odometry class for tracking robot pose
|
||||
private final DifferentialDriveOdometry m_odometry;
|
||||
|
||||
// These classes help us simulate our drivetrain
|
||||
public DifferentialDrivetrainSim m_drivetrainSimulator;
|
||||
private EncoderSim m_leftEncoderSim;
|
||||
private EncoderSim m_rightEncoderSim;
|
||||
// The Field2d class simulates the field in the sim GUI. Note that we can have only one
|
||||
// instance!
|
||||
private Field2d m_fieldSim;
|
||||
private SimDouble m_gyroAngleSim;
|
||||
|
||||
/**
|
||||
* Creates a new DriveSubsystem.
|
||||
*/
|
||||
public DriveSubsystem() {
|
||||
// Sets the distance per pulse for the encoders
|
||||
m_leftEncoder.setDistancePerPulse(Constants.DriveConstants.kEncoderDistancePerPulse);
|
||||
m_rightEncoder.setDistancePerPulse(Constants.DriveConstants.kEncoderDistancePerPulse);
|
||||
|
||||
resetEncoders();
|
||||
m_odometry = new DifferentialDriveOdometry(Rotation2d.fromDegrees(getHeading()));
|
||||
|
||||
if (RobotBase.isSimulation()) { // If our robot is simulated
|
||||
// This class simulates our drivetrain's motion around the field.
|
||||
m_drivetrainSimulator = new DifferentialDrivetrainSim(
|
||||
Constants.DriveConstants.kDrivetrainPlant,
|
||||
Constants.DriveConstants.kDriveGearbox,
|
||||
Constants.DriveConstants.kDriveGearing,
|
||||
Constants.DriveConstants.kTrackwidthMeters,
|
||||
Constants.DriveConstants.kWheelDiameterMeters / 2.0);
|
||||
|
||||
// The encoder and gyro angle sims let us set simulated sensor readings
|
||||
m_leftEncoderSim = new EncoderSim(m_leftEncoder);
|
||||
m_rightEncoderSim = new EncoderSim(m_rightEncoder);
|
||||
m_gyroAngleSim =
|
||||
new SimDeviceSim("ADXRS450_Gyro" + "[" + SPI.Port.kOnboardCS0.value + "]")
|
||||
.getDouble("Angle");
|
||||
|
||||
// the Field2d class lets us visualize our robot in the simulation GUI.
|
||||
m_fieldSim = new Field2d();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void periodic() {
|
||||
// Update the odometry in the periodic block
|
||||
m_odometry.update(Rotation2d.fromDegrees(getHeading()), m_leftEncoder.getDistance(),
|
||||
m_rightEncoder.getDistance());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void simulationPeriodic() {
|
||||
// To update our simulation, we set motor voltage inputs, update the simulation,
|
||||
// and write the simulated positions and velocities to our simulated encoder and gyro.
|
||||
// We negate the right side so that positive voltages make the right side
|
||||
// move forward.
|
||||
m_drivetrainSimulator.setInputs(m_leftMotors.get() * RobotController.getBatteryVoltage(),
|
||||
-m_rightMotors.get() * RobotController.getBatteryVoltage());
|
||||
m_drivetrainSimulator.update(0.020);
|
||||
|
||||
m_leftEncoderSim.setDistance(m_drivetrainSimulator.getState(DifferentialDrivetrainSim.State.kLeftPosition));
|
||||
m_leftEncoderSim.setRate(m_drivetrainSimulator.getState(DifferentialDrivetrainSim.State.kLeftVelocity));
|
||||
m_rightEncoderSim.setDistance(m_drivetrainSimulator.getState(DifferentialDrivetrainSim.State.kRightPosition));
|
||||
m_rightEncoderSim.setRate(m_drivetrainSimulator.getState(DifferentialDrivetrainSim.State.kRightVelocity));
|
||||
m_gyroAngleSim.set(-m_drivetrainSimulator.getHeading().getDegrees());
|
||||
|
||||
m_fieldSim.setRobotPose(getPose());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current being drawn by the drivetrain. This works in SIMULATION ONLY!
|
||||
* If you want it to work elsewhere, use the code in
|
||||
* {@link DifferentialDrivetrainSim#getCurrentDrawAmps()}
|
||||
*
|
||||
* @return The drawn current in Amps.
|
||||
*/
|
||||
public double getDrawnCurrentAmps() {
|
||||
return m_drivetrainSimulator.getCurrentDrawAmps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently-estimated pose of the robot.
|
||||
*
|
||||
* @return The pose.
|
||||
*/
|
||||
public Pose2d getPose() {
|
||||
return m_odometry.getPoseMeters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current wheel speeds of the robot.
|
||||
*
|
||||
* @return The current wheel speeds.
|
||||
*/
|
||||
public DifferentialDriveWheelSpeeds getWheelSpeeds() {
|
||||
return new DifferentialDriveWheelSpeeds(m_leftEncoder.getRate(), m_rightEncoder.getRate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the odometry to the specified pose.
|
||||
*
|
||||
* @param pose The pose to which to set the odometry.
|
||||
*/
|
||||
public void resetOdometry(Pose2d pose) {
|
||||
resetEncoders();
|
||||
m_odometry.resetPosition(pose, Rotation2d.fromDegrees(getHeading()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Drives the robot using arcade controls.
|
||||
*
|
||||
* @param fwd the commanded forward movement
|
||||
* @param rot the commanded rotation
|
||||
*/
|
||||
public void arcadeDrive(double fwd, double rot) {
|
||||
m_drive.arcadeDrive(fwd, rot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls the left and right sides of the drive directly with voltages.
|
||||
*
|
||||
* @param leftVolts the commanded left output
|
||||
* @param rightVolts the commanded right output
|
||||
*/
|
||||
public void tankDriveVolts(double leftVolts, double rightVolts) {
|
||||
var batteryVoltage = RobotController.getBatteryVoltage();
|
||||
if (Math.max(Math.abs(leftVolts), Math.abs(rightVolts))
|
||||
> batteryVoltage) {
|
||||
leftVolts *= batteryVoltage / 12.0;
|
||||
rightVolts *= batteryVoltage / 12.0;
|
||||
}
|
||||
m_leftMotors.setVoltage(leftVolts);
|
||||
m_rightMotors.setVoltage(-rightVolts);
|
||||
m_drive.feed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the drive encoders to currently read a position of 0.
|
||||
*/
|
||||
public void resetEncoders() {
|
||||
m_leftEncoder.reset();
|
||||
m_rightEncoder.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the average distance of the two encoders.
|
||||
*
|
||||
* @return the average of the two encoder readings
|
||||
*/
|
||||
public double getAverageEncoderDistance() {
|
||||
return (m_leftEncoder.getDistance() + m_rightEncoder.getDistance()) / 2.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the left drive encoder.
|
||||
*
|
||||
* @return the left drive encoder
|
||||
*/
|
||||
public Encoder getLeftEncoder() {
|
||||
return m_leftEncoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the right drive encoder.
|
||||
*
|
||||
* @return the right drive encoder
|
||||
*/
|
||||
public Encoder getRightEncoder() {
|
||||
return m_rightEncoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the max output of the drive. Useful for scaling the drive to drive more slowly.
|
||||
*
|
||||
* @param maxOutput the maximum output to which the drive will be constrained
|
||||
*/
|
||||
public void setMaxOutput(double maxOutput) {
|
||||
m_drive.setMaxOutput(maxOutput);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeroes the heading of the robot.
|
||||
*/
|
||||
public void zeroHeading() {
|
||||
m_gyro.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the heading of the robot.
|
||||
*
|
||||
* @return the robot's heading in degrees, from -180 to 180
|
||||
*/
|
||||
public double getHeading() {
|
||||
return Math.IEEEremainder(m_gyro.getAngle(), 360) * (Constants.DriveConstants.kGyroReversed ? -1.0 : 1.0);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,11 +50,14 @@ public class Robot extends TimedRobot {
|
||||
Units.feetToMeters(3.0), Units.feetToMeters(6.0)); // Max elevator speed and acceleration.
|
||||
private TrapezoidProfile.State m_lastProfiledReference = new TrapezoidProfile.State();
|
||||
|
||||
// The plant holds a state-space model of our elevator. This system has the following properties:
|
||||
//
|
||||
// States: [position, velocity], in meters and meters per second.
|
||||
// Inputs (what we can "put in"): [voltage], in volts.
|
||||
// Outputs (what we can measure): [position], in meters.
|
||||
/* The plant holds a state-space model of our elevator. This system has the following properties:
|
||||
|
||||
States: [position, velocity], in meters and meters per second.
|
||||
Inputs (what we can "put in"): [voltage], in volts.
|
||||
Outputs (what we can measure): [position], in meters.
|
||||
|
||||
This elevator is driven by two NEO motors.
|
||||
*/
|
||||
private final LinearSystem<N2, N1, N1> m_elevatorPlant = LinearSystemId.createElevatorSystem(
|
||||
DCMotor.getNEO(2),
|
||||
kCarriageMass,
|
||||
|
||||
Reference in New Issue
Block a user