Motion Profiling in Command-based
Nota
For a description of the WPILib motion profiling features used by these command-based wrappers, see Trapezoidal Motion Profiles in WPILib.
Nota
The TrapezoidProfile
class, used on its own, is most useful when composed with external controllers, such as a «smart» motor controller with a built-in PID functionality. For combining trapezoidal motion profiling with WPILib’s PIDController
, see Combining Motion Profiling and PID in Command-Based.
When controlling a mechanism, is often desirable to move it smoothly between two positions, rather than to abruptly change its setpoint. This is called «motion-profiling,» and is supported in WPILib through the TrapezoidProfile
class (Java, C++).
Nota
In C++, the TrapezoidProfile
class is templated on the unit type used for distance measurements, which may be angular or linear. The passed-in values must have units consistent with the distance units, or a compile-time error will be thrown. For more information on C++ units, see The C++ Units Library.
The following examples are taken from the DriveDistanceOffboard example project (Java, C++):
5package edu.wpi.first.wpilibj.examples.drivedistanceoffboard.subsystems;
6
7import static edu.wpi.first.units.Units.MetersPerSecond;
8import static edu.wpi.first.units.Units.Volts;
9
10import edu.wpi.first.math.controller.SimpleMotorFeedforward;
11import edu.wpi.first.math.trajectory.TrapezoidProfile;
12import edu.wpi.first.math.trajectory.TrapezoidProfile.State;
13import edu.wpi.first.util.sendable.SendableRegistry;
14import edu.wpi.first.wpilibj.RobotController;
15import edu.wpi.first.wpilibj.Timer;
16import edu.wpi.first.wpilibj.drive.DifferentialDrive;
17import edu.wpi.first.wpilibj.examples.drivedistanceoffboard.Constants.DriveConstants;
18import edu.wpi.first.wpilibj.examples.drivedistanceoffboard.ExampleSmartMotorController;
19import edu.wpi.first.wpilibj2.command.Command;
20import edu.wpi.first.wpilibj2.command.SubsystemBase;
21
22public class DriveSubsystem extends SubsystemBase {
23 // The motors on the left side of the drive.
24 private final ExampleSmartMotorController m_leftLeader =
25 new ExampleSmartMotorController(DriveConstants.kLeftMotor1Port);
26
27 private final ExampleSmartMotorController m_leftFollower =
28 new ExampleSmartMotorController(DriveConstants.kLeftMotor2Port);
29
30 // The motors on the right side of the drive.
31 private final ExampleSmartMotorController m_rightLeader =
32 new ExampleSmartMotorController(DriveConstants.kRightMotor1Port);
33
34 private final ExampleSmartMotorController m_rightFollower =
35 new ExampleSmartMotorController(DriveConstants.kRightMotor2Port);
36
37 // The feedforward controller.
38 private final SimpleMotorFeedforward m_feedforward =
39 new SimpleMotorFeedforward(
40 DriveConstants.ksVolts,
41 DriveConstants.kvVoltSecondsPerMeter,
42 DriveConstants.kaVoltSecondsSquaredPerMeter);
43
44 // The robot's drive
45 private final DifferentialDrive m_drive =
46 new DifferentialDrive(m_leftLeader::set, m_rightLeader::set);
47
48 // The trapezoid profile
49 private final TrapezoidProfile m_profile =
50 new TrapezoidProfile(
51 new TrapezoidProfile.Constraints(
52 DriveConstants.kMaxSpeedMetersPerSecond,
53 DriveConstants.kMaxAccelerationMetersPerSecondSquared));
54
55 // The timer
56 private final Timer m_timer = new Timer();
57
58 /** Creates a new DriveSubsystem. */
59 public DriveSubsystem() {
60 SendableRegistry.addChild(m_drive, m_leftLeader);
61 SendableRegistry.addChild(m_drive, m_rightLeader);
62
63 // We need to invert one side of the drivetrain so that positive voltages
64 // result in both sides moving forward. Depending on how your robot's
65 // gearbox is constructed, you might have to invert the left side instead.
66 m_rightLeader.setInverted(true);
67
68 m_leftFollower.follow(m_leftLeader);
69 m_rightFollower.follow(m_rightLeader);
70
71 m_leftLeader.setPID(DriveConstants.kp, 0, 0);
72 m_rightLeader.setPID(DriveConstants.kp, 0, 0);
73 }
74
75 /**
76 * Drives the robot using arcade controls.
77 *
78 * @param fwd the commanded forward movement
79 * @param rot the commanded rotation
80 */
81 public void arcadeDrive(double fwd, double rot) {
82 m_drive.arcadeDrive(fwd, rot);
83 }
84
85 /**
86 * Attempts to follow the given drive states using offboard PID.
87 *
88 * @param currentLeft The current left wheel state.
89 * @param currentRight The current right wheel state.
90 * @param nextLeft The next left wheel state.
91 * @param nextRight The next right wheel state.
92 */
93 public void setDriveStates(
94 TrapezoidProfile.State currentLeft,
95 TrapezoidProfile.State currentRight,
96 TrapezoidProfile.State nextLeft,
97 TrapezoidProfile.State nextRight) {
98 // Feedforward is divided by battery voltage to normalize it to [-1, 1]
99 m_leftLeader.setSetpoint(
100 ExampleSmartMotorController.PIDMode.kPosition,
101 currentLeft.position,
102 m_feedforward
103 .calculate(
104 MetersPerSecond.of(currentLeft.velocity), MetersPerSecond.of(nextLeft.velocity))
105 .in(Volts)
106 / RobotController.getBatteryVoltage());
107 m_rightLeader.setSetpoint(
108 ExampleSmartMotorController.PIDMode.kPosition,
109 currentRight.position,
110 m_feedforward
111 .calculate(
112 MetersPerSecond.of(currentLeft.velocity), MetersPerSecond.of(nextLeft.velocity))
113 .in(Volts)
114 / RobotController.getBatteryVoltage());
115 }
116
117 /**
118 * Returns the left encoder distance.
119 *
120 * @return the left encoder distance
121 */
122 public double getLeftEncoderDistance() {
123 return m_leftLeader.getEncoderDistance();
124 }
125
126 /**
127 * Returns the right encoder distance.
128 *
129 * @return the right encoder distance
130 */
131 public double getRightEncoderDistance() {
132 return m_rightLeader.getEncoderDistance();
133 }
134
135 /** Resets the drive encoders. */
136 public void resetEncoders() {
137 m_leftLeader.resetEncoder();
138 m_rightLeader.resetEncoder();
139 }
140
141 /**
142 * Sets the max output of the drive. Useful for scaling the drive to drive more slowly.
143 *
144 * @param maxOutput the maximum output to which the drive will be constrained
145 */
146 public void setMaxOutput(double maxOutput) {
147 m_drive.setMaxOutput(maxOutput);
148 }
149
150 /**
151 * Creates a command to drive forward a specified distance using a motion profile.
152 *
153 * @param distance The distance to drive forward.
154 * @return A command.
155 */
156 public Command profiledDriveDistance(double distance) {
157 return startRun(
158 () -> {
159 // Restart timer so profile setpoints start at the beginning
160 m_timer.restart();
161 resetEncoders();
162 },
163 () -> {
164 // Current state never changes, so we need to use a timer to get the setpoints we need
165 // to be at
166 var currentTime = m_timer.get();
167 var currentSetpoint =
168 m_profile.calculate(currentTime, new State(), new State(distance, 0));
169 var nextSetpoint =
170 m_profile.calculate(
171 currentTime + DriveConstants.kDt, new State(), new State(distance, 0));
172 setDriveStates(currentSetpoint, currentSetpoint, nextSetpoint, nextSetpoint);
173 })
174 .until(() -> m_profile.isFinished(0));
175 }
176
177 private double m_initialLeftDistance;
178 private double m_initialRightDistance;
179
180 /**
181 * Creates a command to drive forward a specified distance using a motion profile without
182 * resetting the encoders.
183 *
184 * @param distance The distance to drive forward.
185 * @return A command.
186 */
187 public Command dynamicProfiledDriveDistance(double distance) {
188 return startRun(
189 () -> {
190 // Restart timer so profile setpoints start at the beginning
191 m_timer.restart();
192 // Store distance so we know the target distance for each encoder
193 m_initialLeftDistance = getLeftEncoderDistance();
194 m_initialRightDistance = getRightEncoderDistance();
195 },
196 () -> {
197 // Current state never changes for the duration of the command, so we need to use a
198 // timer to get the setpoints we need to be at
199 var currentTime = m_timer.get();
200 var currentLeftSetpoint =
201 m_profile.calculate(
202 currentTime,
203 new State(m_initialLeftDistance, 0),
204 new State(m_initialLeftDistance + distance, 0));
205 var currentRightSetpoint =
206 m_profile.calculate(
207 currentTime,
208 new State(m_initialRightDistance, 0),
209 new State(m_initialRightDistance + distance, 0));
210 var nextLeftSetpoint =
211 m_profile.calculate(
212 currentTime + DriveConstants.kDt,
213 new State(m_initialLeftDistance, 0),
214 new State(m_initialLeftDistance + distance, 0));
215 var nextRightSetpoint =
216 m_profile.calculate(
217 currentTime + DriveConstants.kDt,
218 new State(m_initialRightDistance, 0),
219 new State(m_initialRightDistance + distance, 0));
220 setDriveStates(
221 currentLeftSetpoint, currentRightSetpoint, nextLeftSetpoint, nextRightSetpoint);
222 })
223 .until(() -> m_profile.isFinished(0));
224 }
225}
5#pragma once
6
7#include <frc/Encoder.h>
8#include <frc/Timer.h>
9#include <frc/controller/SimpleMotorFeedforward.h>
10#include <frc/drive/DifferentialDrive.h>
11#include <frc/trajectory/TrapezoidProfile.h>
12#include <frc2/command/CommandPtr.h>
13#include <frc2/command/SubsystemBase.h>
14#include <units/length.h>
15
16#include "Constants.h"
17#include "ExampleSmartMotorController.h"
18
19class DriveSubsystem : public frc2::SubsystemBase {
20 public:
21 DriveSubsystem();
22
23 /**
24 * Will be called periodically whenever the CommandScheduler runs.
25 */
26 void Periodic() override;
27
28 // Subsystem methods go here.
29
30 /**
31 * Attempts to follow the given drive states using offboard PID.
32 *
33 * @param currentLeft The current left wheel state.
34 * @param currentRight The current right wheel state.
35 * @param nextLeft The next left wheel state.
36 * @param nextRight The next right wheel state.
37 */
38 void SetDriveStates(frc::TrapezoidProfile<units::meters>::State currentLeft,
39 frc::TrapezoidProfile<units::meters>::State currentRight,
40 frc::TrapezoidProfile<units::meters>::State nextLeft,
41 frc::TrapezoidProfile<units::meters>::State nextRight);
42
43 /**
44 * Drives the robot using arcade controls.
45 *
46 * @param fwd the commanded forward movement
47 * @param rot the commanded rotation
48 */
49 void ArcadeDrive(double fwd, double rot);
50
51 /**
52 * Resets the drive encoders to currently read a position of 0.
53 */
54 void ResetEncoders();
55
56 /**
57 * Gets the distance of the left encoder.
58 *
59 * @return the average of the TWO encoder readings
60 */
61 units::meter_t GetLeftEncoderDistance();
62
63 /**
64 * Gets the distance of the right encoder.
65 *
66 * @return the average of the TWO encoder readings
67 */
68 units::meter_t GetRightEncoderDistance();
69
70 /**
71 * Sets the max output of the drive. Useful for scaling the drive to drive
72 * more slowly.
73 *
74 * @param maxOutput the maximum output to which the drive will be constrained
75 */
76 void SetMaxOutput(double maxOutput);
77
78 /**
79 * Creates a command to drive forward a specified distance using a motion
80 * profile.
81 *
82 * @param distance The distance to drive forward.
83 * @return A command.
84 */
85 [[nodiscard]]
86 frc2::CommandPtr ProfiledDriveDistance(units::meter_t distance);
87
88 /**
89 * Creates a command to drive forward a specified distance using a motion
90 * profile without resetting the encoders.
91 *
92 * @param distance The distance to drive forward.
93 * @return A command.
94 */
95 [[nodiscard]]
96 frc2::CommandPtr DynamicProfiledDriveDistance(units::meter_t distance);
97
98 private:
99 frc::TrapezoidProfile<units::meters> m_profile{
100 {DriveConstants::kMaxSpeed, DriveConstants::kMaxAcceleration}};
101 frc::Timer m_timer;
102 units::meter_t m_initialLeftDistance;
103 units::meter_t m_initialRightDistance;
104 // Components (e.g. motor controllers and sensors) should generally be
105 // declared private and exposed only through public methods.
106
107 // The motor controllers
108 ExampleSmartMotorController m_leftLeader;
109 ExampleSmartMotorController m_leftFollower;
110 ExampleSmartMotorController m_rightLeader;
111 ExampleSmartMotorController m_rightFollower;
112
113 // A feedforward component for the drive
114 frc::SimpleMotorFeedforward<units::meters> m_feedforward;
115
116 // The robot's drive
117 frc::DifferentialDrive m_drive{
118 [&](double output) { m_leftLeader.Set(output); },
119 [&](double output) { m_rightLeader.Set(output); }};
120};
5#include "subsystems/DriveSubsystem.h"
6
7#include <frc/RobotController.h>
8
9using namespace DriveConstants;
10
11DriveSubsystem::DriveSubsystem()
12 : m_leftLeader{kLeftMotor1Port},
13 m_leftFollower{kLeftMotor2Port},
14 m_rightLeader{kRightMotor1Port},
15 m_rightFollower{kRightMotor2Port},
16 m_feedforward{ks, kv, ka} {
17 wpi::SendableRegistry::AddChild(&m_drive, &m_leftLeader);
18 wpi::SendableRegistry::AddChild(&m_drive, &m_rightLeader);
19
20 // We need to invert one side of the drivetrain so that positive voltages
21 // result in both sides moving forward. Depending on how your robot's
22 // gearbox is constructed, you might have to invert the left side instead.
23 m_rightLeader.SetInverted(true);
24
25 m_leftFollower.Follow(m_leftLeader);
26 m_rightFollower.Follow(m_rightLeader);
27
28 m_leftLeader.SetPID(kp, 0, 0);
29 m_rightLeader.SetPID(kp, 0, 0);
30}
31
32void DriveSubsystem::Periodic() {
33 // Implementation of subsystem periodic method goes here.
34}
35
36void DriveSubsystem::SetDriveStates(
37 frc::TrapezoidProfile<units::meters>::State currentLeft,
38 frc::TrapezoidProfile<units::meters>::State currentRight,
39 frc::TrapezoidProfile<units::meters>::State nextLeft,
40 frc::TrapezoidProfile<units::meters>::State nextRight) {
41 // Feedforward is divided by battery voltage to normalize it to [-1, 1]
42 m_leftLeader.SetSetpoint(
43 ExampleSmartMotorController::PIDMode::kPosition,
44 currentLeft.position.value(),
45 m_feedforward.Calculate(currentLeft.velocity, nextLeft.velocity) /
46 frc::RobotController::GetBatteryVoltage());
47 m_rightLeader.SetSetpoint(
48 ExampleSmartMotorController::PIDMode::kPosition,
49 currentRight.position.value(),
50 m_feedforward.Calculate(currentRight.velocity, nextRight.velocity) /
51 frc::RobotController::GetBatteryVoltage());
52}
53
54void DriveSubsystem::ArcadeDrive(double fwd, double rot) {
55 m_drive.ArcadeDrive(fwd, rot);
56}
57
58void DriveSubsystem::ResetEncoders() {
59 m_leftLeader.ResetEncoder();
60 m_rightLeader.ResetEncoder();
61}
62
63units::meter_t DriveSubsystem::GetLeftEncoderDistance() {
64 return units::meter_t{m_leftLeader.GetEncoderDistance()};
65}
66
67units::meter_t DriveSubsystem::GetRightEncoderDistance() {
68 return units::meter_t{m_rightLeader.GetEncoderDistance()};
69}
70
71void DriveSubsystem::SetMaxOutput(double maxOutput) {
72 m_drive.SetMaxOutput(maxOutput);
73}
74
75frc2::CommandPtr DriveSubsystem::ProfiledDriveDistance(
76 units::meter_t distance) {
77 return StartRun(
78 [&] {
79 // Restart timer so profile setpoints start at the beginning
80 m_timer.Restart();
81 ResetEncoders();
82 },
83 [&] {
84 // Current state never changes, so we need to use a timer to get
85 // the setpoints we need to be at
86 auto currentTime = m_timer.Get();
87 auto currentSetpoint =
88 m_profile.Calculate(currentTime, {}, {distance, 0_mps});
89 auto nextSetpoint = m_profile.Calculate(currentTime + kDt, {},
90 {distance, 0_mps});
91 SetDriveStates(currentSetpoint, currentSetpoint, nextSetpoint,
92 nextSetpoint);
93 })
94 .Until([&] { return m_profile.IsFinished(0_s); });
95}
96
97frc2::CommandPtr DriveSubsystem::DynamicProfiledDriveDistance(
98 units::meter_t distance) {
99 return StartRun(
100 [&] {
101 // Restart timer so profile setpoints start at the beginning
102 m_timer.Restart();
103 // Store distance so we know the target distance for each encoder
104 m_initialLeftDistance = GetLeftEncoderDistance();
105 m_initialRightDistance = GetRightEncoderDistance();
106 },
107 [&] {
108 // Current state never changes for the duration of the command,
109 // so we need to use a timer to get the setpoints we need to be
110 // at
111 auto currentTime = m_timer.Get();
112
113 auto currentLeftSetpoint = m_profile.Calculate(
114 currentTime, {m_initialLeftDistance, 0_mps},
115 {m_initialLeftDistance + distance, 0_mps});
116 auto currentRightSetpoint = m_profile.Calculate(
117 currentTime, {m_initialRightDistance, 0_mps},
118 {m_initialRightDistance + distance, 0_mps});
119
120 auto nextLeftSetpoint = m_profile.Calculate(
121 currentTime + kDt, {m_initialLeftDistance, 0_mps},
122 {m_initialLeftDistance + distance, 0_mps});
123 auto nextRightSetpoint = m_profile.Calculate(
124 currentTime + kDt, {m_initialRightDistance, 0_mps},
125 {m_initialRightDistance + distance, 0_mps});
126 SetDriveStates(currentLeftSetpoint, currentRightSetpoint,
127 nextLeftSetpoint, nextRightSetpoint);
128 })
129 .Until([&] { return m_profile.IsFinished(0_s); });
130}
There are two commands in this example. They function very similarly, with the main difference being that one resets encoders, and the other doesn’t, which allows encoder data to be preserved.
The subsystem contains a TrapezoidProfile
with a Timer
. The timer is used along with a kDt
constant of 0.02 seconds to calculate the current and next states from the TrapezoidProfile
. The current state is fed to the «smart» motor controller for PID control, while the current and next state are used to calculate feedforward outputs. Both commands end when isFinished(0)
returns true, which means that the profile has reached the goal state.