Étape 4 : Création et suivi d’une trajectoire
Avec notre sous-système de déplacement écrit, il est maintenant temps de générer une trajectoire et d’écrire une commande autonome pour la suivre.
Selon le projet structure standard d’un projet orienté commande, nous le ferons dans la méthode getAutonomousCommand
de la classe RobotContainer
. La méthode complète du projet d’exemple RamseteCommand (Java, C++) peut être vue ci-dessous. Le reste de l’article décomposera plus en détail les différentes parties de la méthode.
74 /**
75 * Use this to pass the autonomous command to the main {@link Robot} class.
76 *
77 * @return the command to run in autonomous
78 */
79 public Command getAutonomousCommand() {
80 // Create a voltage constraint to ensure we don't accelerate too fast
81 var autoVoltageConstraint =
82 new DifferentialDriveVoltageConstraint(
83 new SimpleMotorFeedforward(
84 DriveConstants.ksVolts,
85 DriveConstants.kvVoltSecondsPerMeter,
86 DriveConstants.kaVoltSecondsSquaredPerMeter),
87 DriveConstants.kDriveKinematics,
88 10);
89
90 // Create config for trajectory
91 TrajectoryConfig config =
92 new TrajectoryConfig(
93 AutoConstants.kMaxSpeedMetersPerSecond,
94 AutoConstants.kMaxAccelerationMetersPerSecondSquared)
95 // Add kinematics to ensure max speed is actually obeyed
96 .setKinematics(DriveConstants.kDriveKinematics)
97 // Apply the voltage constraint
98 .addConstraint(autoVoltageConstraint);
99
100 // An example trajectory to follow. All units in meters.
101 Trajectory exampleTrajectory =
102 TrajectoryGenerator.generateTrajectory(
103 // Start at the origin facing the +X direction
104 new Pose2d(0, 0, new Rotation2d(0)),
105 // Pass through these two interior waypoints, making an 's' curve path
106 List.of(new Translation2d(1, 1), new Translation2d(2, -1)),
107 // End 3 meters straight ahead of where we started, facing forward
108 new Pose2d(3, 0, new Rotation2d(0)),
109 // Pass config
110 config);
111
112 RamseteCommand ramseteCommand =
113 new RamseteCommand(
114 exampleTrajectory,
115 m_robotDrive::getPose,
116 new RamseteController(AutoConstants.kRamseteB, AutoConstants.kRamseteZeta),
117 new SimpleMotorFeedforward(
118 DriveConstants.ksVolts,
119 DriveConstants.kvVoltSecondsPerMeter,
120 DriveConstants.kaVoltSecondsSquaredPerMeter),
121 DriveConstants.kDriveKinematics,
122 m_robotDrive::getWheelSpeeds,
123 new PIDController(DriveConstants.kPDriveVel, 0, 0),
124 new PIDController(DriveConstants.kPDriveVel, 0, 0),
125 // RamseteCommand passes volts to the callback
126 m_robotDrive::tankDriveVolts,
127 m_robotDrive);
128
129 // Reset odometry to the starting pose of the trajectory.
130 m_robotDrive.resetOdometry(exampleTrajectory.getInitialPose());
131
132 // Run path following command, then stop at the end.
133 return ramseteCommand.andThen(() -> m_robotDrive.tankDriveVolts(0, 0));
134 }
135}
45frc2::CommandPtr RobotContainer::GetAutonomousCommand() {
46 // Create a voltage constraint to ensure we don't accelerate too fast
47 frc::DifferentialDriveVoltageConstraint autoVoltageConstraint{
48 frc::SimpleMotorFeedforward<units::meters>{
49 DriveConstants::ks, DriveConstants::kv, DriveConstants::ka},
50 DriveConstants::kDriveKinematics, 10_V};
51
52 // Set up config for trajectory
53 frc::TrajectoryConfig config{AutoConstants::kMaxSpeed,
54 AutoConstants::kMaxAcceleration};
55 // Add kinematics to ensure max speed is actually obeyed
56 config.SetKinematics(DriveConstants::kDriveKinematics);
57 // Apply the voltage constraint
58 config.AddConstraint(autoVoltageConstraint);
59
60 // An example trajectory to follow. All units in meters.
61 auto exampleTrajectory = frc::TrajectoryGenerator::GenerateTrajectory(
62 // Start at the origin facing the +X direction
63 frc::Pose2d{0_m, 0_m, 0_deg},
64 // Pass through these two interior waypoints, making an 's' curve path
65 {frc::Translation2d{1_m, 1_m}, frc::Translation2d{2_m, -1_m}},
66 // End 3 meters straight ahead of where we started, facing forward
67 frc::Pose2d{3_m, 0_m, 0_deg},
68 // Pass the config
69 config);
70
71 frc2::CommandPtr ramseteCommand{frc2::RamseteCommand(
72 exampleTrajectory, [this] { return m_drive.GetPose(); },
73 frc::RamseteController{AutoConstants::kRamseteB,
74 AutoConstants::kRamseteZeta},
75 frc::SimpleMotorFeedforward<units::meters>{
76 DriveConstants::ks, DriveConstants::kv, DriveConstants::ka},
77 DriveConstants::kDriveKinematics,
78 [this] { return m_drive.GetWheelSpeeds(); },
79 frc2::PIDController{DriveConstants::kPDriveVel, 0, 0},
80 frc2::PIDController{DriveConstants::kPDriveVel, 0, 0},
81 [this](auto left, auto right) { m_drive.TankDriveVolts(left, right); },
82 {&m_drive})};
83
84 // Reset odometry to the starting pose of the trajectory.
85 m_drive.ResetOdometry(exampleTrajectory.InitialPose());
86
87 return std::move(ramseteCommand)
88 .BeforeStarting(
89 frc2::cmd::RunOnce([this] { m_drive.TankDriveVolts(0_V, 0_V); }, {}));
90}
Configuration des contraintes de la trajectoire
Tout d’abord, nous devons définir certains paramètres de configuration pour la trajectoire qui garantiront que la trajectoire ainsi générée peut effectivement être suivie.
Création d’une contrainte de tension
La première pièce de configuration dont nous aurons besoin est une contrainte de tension. Cela permettra de s’assurer que la trajectoire générée ne contraigne jamais le robot d’aller plus vite qu’il n’est capable d’atteindre avec l’alimentation de tension disponible:
80 // Create a voltage constraint to ensure we don't accelerate too fast
81 var autoVoltageConstraint =
82 new DifferentialDriveVoltageConstraint(
83 new SimpleMotorFeedforward(
84 DriveConstants.ksVolts,
85 DriveConstants.kvVoltSecondsPerMeter,
86 DriveConstants.kaVoltSecondsSquaredPerMeter),
87 DriveConstants.kDriveKinematics,
88 10);
46 // Create a voltage constraint to ensure we don't accelerate too fast
47 frc::DifferentialDriveVoltageConstraint autoVoltageConstraint{
48 frc::SimpleMotorFeedforward<units::meters>{
49 DriveConstants::ks, DriveConstants::kv, DriveConstants::ka},
50 DriveConstants::kDriveKinematics, 10_V};
Notez que nous avons fixé la tension maximale à 10V, plutôt qu’à la tension nominale de la batterie de 12V. Cela nous laisse une certaine « marge » pour faire face à un éventuel « creux de tension » pendant le suivi de la trajectoire.
Création de la configuration
Maintenant que nous avons notre contrainte de tension, nous pouvons créer notre instance TrajectoryConfig
, qui englobe toutes nos contraintes de trajectoire :
90 // Create config for trajectory
91 TrajectoryConfig config =
92 new TrajectoryConfig(
93 AutoConstants.kMaxSpeedMetersPerSecond,
94 AutoConstants.kMaxAccelerationMetersPerSecondSquared)
95 // Add kinematics to ensure max speed is actually obeyed
96 .setKinematics(DriveConstants.kDriveKinematics)
97 // Apply the voltage constraint
98 .addConstraint(autoVoltageConstraint);
52 // Set up config for trajectory
53 frc::TrajectoryConfig config{AutoConstants::kMaxSpeed,
54 AutoConstants::kMaxAcceleration};
55 // Add kinematics to ensure max speed is actually obeyed
56 config.SetKinematics(DriveConstants::kDriveKinematics);
57 // Apply the voltage constraint
58 config.AddConstraint(autoVoltageConstraint);
Génération de la trajectoire
Avec notre configuration de trajectoire en main, nous sommes maintenant prêts à générer notre trajectoire. Pour cet exemple, nous allons générer une trajectoire « clamped cubic » - cela signifie que nous allons spécifier au complet les poses de robots aux extrémités, et spécifier des positions uniquement pour les points de passage intérieurs (également connu sous le nom « points noeuds »). Comme ailleurs, toutes les distances sont en mètres.
100 // An example trajectory to follow. All units in meters.
101 Trajectory exampleTrajectory =
102 TrajectoryGenerator.generateTrajectory(
103 // Start at the origin facing the +X direction
104 new Pose2d(0, 0, new Rotation2d(0)),
105 // Pass through these two interior waypoints, making an 's' curve path
106 List.of(new Translation2d(1, 1), new Translation2d(2, -1)),
107 // End 3 meters straight ahead of where we started, facing forward
108 new Pose2d(3, 0, new Rotation2d(0)),
109 // Pass config
110 config);
60 // An example trajectory to follow. All units in meters.
61 auto exampleTrajectory = frc::TrajectoryGenerator::GenerateTrajectory(
62 // Start at the origin facing the +X direction
63 frc::Pose2d{0_m, 0_m, 0_deg},
64 // Pass through these two interior waypoints, making an 's' curve path
65 {frc::Translation2d{1_m, 1_m}, frc::Translation2d{2_m, -1_m}},
66 // End 3 meters straight ahead of where we started, facing forward
67 frc::Pose2d{3_m, 0_m, 0_deg},
68 // Pass the config
69 config);
Note
Au lieu de générer la trajectoire sur le roboRIO comme indiqué ci-dessus, on peut également importer un PathWeaver JSON.
Création de la commande Ramsete ou RamseteCommand
Nous allons d’abord réinitialiser la pose de notre robot à la pose de départ de la trajectoire. Cela garantit que l’emplacement du robot sur le système de coordonnées et la position de départ de la trajectoire sont identiques.
128 // Reset odometry to the starting pose of the trajectory.
129 m_robotDrive.resetOdometry(exampleTrajectory.getInitialPose());
84 // Reset odometry to the starting pose of the trajectory.
85 m_drive.ResetOdometry(exampleTrajectory.InitialPose());
It is very important that the initial robot pose match the first pose in the trajectory. For the purposes of our example, the robot will be reliably starting at a position of (0,0)
with a heading of 0
. In actual use, however, it is probably not desirable to base your coordinate system on the robot position, and so the starting position for both the robot and the trajectory should be set to some other value. If you wish to use a trajectory that has been defined in robot-centric coordinates in such a situation, you can transform it to be relative to the robot’s current pose using the transformBy
method (Java, C++). For more information about transforming trajectories, see La transformation des trajectoires.
Now that we have a trajectory, we can create a command that, when executed, will follow that trajectory. To do this, we use the RamseteCommand
class (Java, C++)
112 RamseteCommand ramseteCommand =
113 new RamseteCommand(
114 exampleTrajectory,
115 m_robotDrive::getPose,
116 new RamseteController(AutoConstants.kRamseteB, AutoConstants.kRamseteZeta),
117 new SimpleMotorFeedforward(
118 DriveConstants.ksVolts,
119 DriveConstants.kvVoltSecondsPerMeter,
120 DriveConstants.kaVoltSecondsSquaredPerMeter),
121 DriveConstants.kDriveKinematics,
122 m_robotDrive::getWheelSpeeds,
123 new PIDController(DriveConstants.kPDriveVel, 0, 0),
124 new PIDController(DriveConstants.kPDriveVel, 0, 0),
125 // RamseteCommand passes volts to the callback
126 m_robotDrive::tankDriveVolts,
127 m_robotDrive);
71 frc2::CommandPtr ramseteCommand{frc2::RamseteCommand(
72 exampleTrajectory, [this] { return m_drive.GetPose(); },
73 frc::RamseteController{AutoConstants::kRamseteB,
74 AutoConstants::kRamseteZeta},
75 frc::SimpleMotorFeedforward<units::meters>{
76 DriveConstants::ks, DriveConstants::kv, DriveConstants::ka},
77 DriveConstants::kDriveKinematics,
78 [this] { return m_drive.GetWheelSpeeds(); },
79 frc2::PIDController{DriveConstants::kPDriveVel, 0, 0},
80 frc2::PIDController{DriveConstants::kPDriveVel, 0, 0},
81 [this](auto left, auto right) { m_drive.TankDriveVolts(left, right); },
82 {&m_drive})};
Cette déclaration est assez substantielle, nous allons donc la scruter paramètre par paramètre:
La trajectoire : C’est la trajectoire à suivre; en conséquence, nous passons à la commande la trajectoire que nous venons de construire dans nos étapes antérieures.
Le fournisseur de pose : il s’agit d’une référence de méthode (ou lambda) à la méthode du sous-système drive qui renvoie la pose. Le contrôleur RAMSETE a besoin de la mesure de pose actuelle pour déterminer les sorties de roue requises.
The RAMSETE controller: This is the
RamseteController
object (Java, C++) that will perform the path-following computation that translates the current measured pose and trajectory state into a chassis speed setpoint.The drive feedforward: This is a
SimpleMotorFeedforward
object (Java, C++) that will automatically perform the correct feedforward calculation with the feedforward gains (kS
,kV
, andkA
) that we obtained from the drive identification tool.The drive kinematics: This is the
DifferentialDriveKinematics
object (Java, C++) that we constructed earlier in our constants file, and will be used to convert chassis speeds to wheel speeds.Le fournisseur de vitesse de roue : il s’agit d’une référence de méthode (ou lambda) à la méthode du sous-système drive qui renvoie les vitesses de roue
The left-side PIDController: This is the
PIDController
object (Java, C++) that will track the left-side wheel speed setpoint, using the P gain that we obtained from the drive identification tool.The right-side PIDController: This is the
PIDController
object (Java, C++) that will track the right-side wheel speed setpoint, using the P gain that we obtained from the drive identification tool.Le consommateur de sortie : il s’agit d’une référence de méthode (ou lambda) à la méthode du sous-système drive qui transmet les sorties de tension aux moteurs d’entraînement.
Le sous-système d’entraînement du robot : Il s’agit du sous-système de déplacement lui-même, inclus ici pour s’assurer que la commande ne fonctionne pas sur la base pilotable en même temps que toute autre commande qui utilise cette même base pilotable.
Enfin, notez que nous ajoutons une commande finale « stop » dans l’ordre après la commande de suivi de la trajectoire, pour s’assurer que le robot s’immobilise à la fin de la trajectoire.
Vidéo
Si tout s’est bien passé, la routine autonome de votre robot devrait ressembler à ceci :