001/* 002 * Copyright (C) Cross The Road Electronics. All rights reserved. 003 * License information can be found in CTRE_LICENSE.txt 004 * For support and suggestions contact support@ctr-electronics.com or file 005 * an issue tracker at https://github.com/CrossTheRoadElec/Phoenix-Releases 006 */ 007package com.ctre.phoenix6.mechanisms.swerve; 008 009import java.util.concurrent.locks.ReadWriteLock; 010import java.util.concurrent.locks.ReentrantReadWriteLock; 011import java.util.function.Consumer; 012 013import com.ctre.phoenix6.BaseStatusSignal; 014import com.ctre.phoenix6.StatusCode; 015import com.ctre.phoenix6.Utils; 016import com.ctre.phoenix6.hardware.Pigeon2; 017import com.ctre.phoenix6.unmanaged.Unmanaged; 018 019import static com.ctre.phoenix6.mechanisms.swerve.SwerveRequest.SwerveControlRequestParameters; 020 021import edu.wpi.first.math.Matrix; 022import edu.wpi.first.math.estimator.SwerveDrivePoseEstimator; 023import edu.wpi.first.math.filter.LinearFilter; 024import edu.wpi.first.math.filter.MedianFilter; 025import edu.wpi.first.math.geometry.Pose2d; 026import edu.wpi.first.math.geometry.Rotation2d; 027import edu.wpi.first.math.geometry.Translation2d; 028import edu.wpi.first.math.kinematics.SwerveDriveKinematics; 029import edu.wpi.first.math.kinematics.SwerveModulePosition; 030import edu.wpi.first.math.kinematics.SwerveModuleState; 031import edu.wpi.first.math.numbers.N1; 032import edu.wpi.first.math.numbers.N3; 033import edu.wpi.first.wpilibj.Threads; 034 035/** 036 * Swerve Drive class utilizing CTR Electronics' Phoenix 6 API. 037 * <p> 038 * This class handles the kinematics, configuration, and odometry of a 039 * swerve drive utilizing CTR Electronics devices. We recommend 040 * that users use the Swerve Mechanism Generator in Tuner X to create 041 * a template project that demonstrates how to use this class. 042 * <p> 043 * This class will construct the hardware devices internally, so the user 044 * only specifies the constants (IDs, PID gains, gear ratios, etc). 045 * Getters for these hardware devices are available. 046 * <p> 047 * If using the generator, the order in which modules are constructed is 048 * Front Left, Front Right, Back Left, Back Right. This means if you need 049 * the Back Left module, call {@code getModule(2);} to get the 3rd index 050 * (0-indexed) module, corresponding to the Back Left module. 051 */ 052public class SwerveDrivetrain { 053 protected final int ModuleCount; 054 protected final double UpdateFrequency; 055 protected final SwerveModule[] Modules; 056 057 protected Pigeon2 m_pigeon2; 058 protected SwerveDriveKinematics m_kinematics; 059 protected SwerveDrivePoseEstimator m_odometry; 060 protected SwerveModulePosition[] m_modulePositions; 061 protected Translation2d[] m_moduleLocations; 062 protected OdometryThread m_odometryThread; 063 protected Rotation2d m_fieldRelativeOffset; 064 065 protected SwerveRequest m_requestToApply = new SwerveRequest.Idle(); 066 protected SwerveControlRequestParameters m_requestParameters = new SwerveControlRequestParameters(); 067 068 protected ReadWriteLock m_stateLock = new ReentrantReadWriteLock(); 069 070 protected final SimSwerveDrivetrain m_simDrive; 071 protected final boolean IsOnCANFD; 072 073 /** 074 * Plain-Old-Data class holding the state of the swerve drivetrain. 075 * This encapsulates most data that is relevant for telemetry or 076 * decision-making from the Swerve Drive. 077 */ 078 public class SwerveDriveState { 079 public int SuccessfulDaqs; 080 public int FailedDaqs; 081 public Pose2d Pose; 082 public SwerveModuleState[] ModuleStates; 083 public double OdometryPeriod; 084 } 085 086 protected Consumer<SwerveDriveState> m_telemetryFunction = null; 087 protected SwerveDriveState m_cachedState = new SwerveDriveState(); 088 089 /* Perform swerve module updates in a separate thread to minimize latency */ 090 public class OdometryThread extends Thread { 091 private final int START_THREAD_PRIORITY = 1; // Testing shows 1 (minimum realtime) is sufficient for tighter odometry loops. 092 // If the odometry period is far away from the desired frequency, increasing this may help 093 094 private BaseStatusSignal[] m_allSignals; 095 public int SuccessfulDaqs = 0; 096 public int FailedDaqs = 0; 097 MedianFilter peakRemover = new MedianFilter(3); 098 LinearFilter lowPass = LinearFilter.movingAverage(50); 099 double lastTime = 0; 100 double currentTime = 0; 101 double averageLoopTime = 0; 102 103 int lastThreadPriority = START_THREAD_PRIORITY; 104 int threadPriorityToSet = START_THREAD_PRIORITY; 105 106 public OdometryThread() { 107 super(); 108 // 4 signals for each module + 2 for Pigeon2 109 m_allSignals = new BaseStatusSignal[(ModuleCount * 4) + 2]; 110 for (int i = 0; i < ModuleCount; ++i) { 111 var signals = Modules[i].getSignals(); 112 m_allSignals[(i * 4) + 0] = signals[0]; 113 m_allSignals[(i * 4) + 1] = signals[1]; 114 m_allSignals[(i * 4) + 2] = signals[2]; 115 m_allSignals[(i * 4) + 3] = signals[3]; 116 } 117 m_allSignals[m_allSignals.length - 2] = m_pigeon2.getYaw().clone(); 118 m_allSignals[m_allSignals.length - 1] = m_pigeon2.getAngularVelocityZ().clone(); 119 } 120 121 @Override 122 public void run() { 123 /* Make sure all signals update at around 250hz */ 124 for (var sig : m_allSignals) { 125 sig.setUpdateFrequency(UpdateFrequency); 126 } 127 Threads.setCurrentThreadPriority(true, START_THREAD_PRIORITY); 128 129 /* Run as fast as possible, our signals will control the timing */ 130 while (true) { 131 /* Synchronously wait for all signals in drivetrain */ 132 /* Wait up to twice the period of the update frequency */ 133 StatusCode status; 134 if(IsOnCANFD) { 135 status = BaseStatusSignal.waitForAll(2.0 / UpdateFrequency, m_allSignals); 136 } else { 137 try { 138 /* Wait for the signals to update */ 139 Thread.sleep((long)((1.0 / UpdateFrequency) * 1000.0)); 140 } catch(InterruptedException ex) {} 141 status = BaseStatusSignal.refreshAll(m_allSignals); 142 } 143 m_stateLock.writeLock().lock(); 144 145 lastTime = currentTime; 146 currentTime = Utils.getCurrentTimeSeconds(); 147 /* We don't care about the peaks, as they correspond to GC events, and we want the period generally low passed */ 148 averageLoopTime = lowPass.calculate(peakRemover.calculate(currentTime - lastTime)); 149 150 /* Get status of first element */ 151 if (status.isOK()) { 152 SuccessfulDaqs++; 153 } else { 154 FailedDaqs++; 155 } 156 157 /* Now update odometry */ 158 /* Keep track of the change in azimuth rotations */ 159 for (int i = 0; i < ModuleCount; ++i) { 160 m_modulePositions[i] = Modules[i].getPosition(false); 161 } 162 // Assume Pigeon2 is flat-and-level so latency compensation can be performed 163 double yawDegrees = BaseStatusSignal.getLatencyCompensatedValue( 164 m_pigeon2.getYaw(), m_pigeon2.getAngularVelocityZ()); 165 166 /* Keep track of previous and current pose to account for the carpet vector */ 167 m_odometry.update(Rotation2d.fromDegrees(yawDegrees), m_modulePositions); 168 169 /* And now that we've got the new odometry, update the controls */ 170 m_requestParameters.currentPose = m_odometry.getEstimatedPosition() 171 .relativeTo(new Pose2d(0, 0, m_fieldRelativeOffset)); 172 m_requestParameters.kinematics = m_kinematics; 173 m_requestParameters.swervePositions = m_moduleLocations; 174 m_requestParameters.timestamp = currentTime; 175 176 m_requestToApply.apply(m_requestParameters, Modules); 177 178 /* Update our cached state with the newly updated data */ 179 m_cachedState.FailedDaqs = FailedDaqs; 180 m_cachedState.SuccessfulDaqs = SuccessfulDaqs; 181 m_cachedState.ModuleStates = new SwerveModuleState[Modules.length]; 182 for (int i = 0; i < Modules.length; ++i) { 183 m_cachedState.ModuleStates[i] = Modules[i].getCurrentState(); 184 } 185 m_cachedState.Pose = m_odometry.getEstimatedPosition(); 186 m_cachedState.OdometryPeriod = averageLoopTime; 187 188 if(m_telemetryFunction != null) { 189 /* Log our state */ 190 m_telemetryFunction.accept(m_cachedState); 191 } 192 193 m_stateLock.writeLock().unlock(); 194 /** 195 * This is inherently synchronous, since lastThreadPriority 196 * is only written here and threadPriorityToSet is only read here 197 */ 198 if(threadPriorityToSet != lastThreadPriority) { 199 Threads.setCurrentThreadPriority(true, threadPriorityToSet); 200 lastThreadPriority = threadPriorityToSet; 201 } 202 } 203 } 204 205 public boolean odometryIsValid() { 206 return SuccessfulDaqs > 2; // Wait at least 3 daqs before saying the odometry is valid 207 } 208 209 /** 210 * Sets the DAQ thread priority to a real time priority under the specified priority level 211 * 212 * @param priority Priority level to set the DAQ thread to. 213 * This is a value between 0 and 99, with 99 indicating higher priority and 0 indicating lower priority. 214 */ 215 public void setThreadPriority(int priority) { 216 threadPriorityToSet = priority; 217 } 218 } 219 220 protected boolean checkIsOnCanFD(String canbusName) { 221 return Unmanaged.isNetworkFD(canbusName); 222 } 223 224 /** 225 * Constructs a CTRSwerveDrivetrain using the specified constants. 226 * <p> 227 * This constructs the underlying hardware devices, so user should not construct 228 * the devices themselves. If they need the devices, they can access them 229 * through 230 * getters in the classes. 231 * 232 * @param driveTrainConstants Drivetrain-wide constants for the swerve drive 233 * @param modules Constants for each specific module 234 */ 235 public SwerveDrivetrain(SwerveDrivetrainConstants driveTrainConstants, SwerveModuleConstants... modules) { 236 this(driveTrainConstants, 250, modules); 237 } 238 239 /** 240 * Constructs a CTRSwerveDrivetrain using the specified constants. 241 * <p> 242 * This constructs the underlying hardware devices, so user should not construct 243 * the devices themselves. If they need the devices, they can access them 244 * through 245 * getters in the classes. 246 * 247 * @param driveTrainConstants Drivetrain-wide constants for the swerve drive 248 * @param OdometryUpdateFrequency The frequency to run the odometry loop. If 249 * unspecified, this is 250 hz. 250 * @param modules Constants for each specific module 251 */ 252 public SwerveDrivetrain( 253 SwerveDrivetrainConstants driveTrainConstants, double OdometryUpdateFrequency, 254 SwerveModuleConstants... modules) { 255 UpdateFrequency = OdometryUpdateFrequency; 256 ModuleCount = modules.length; 257 258 IsOnCANFD = checkIsOnCanFD(driveTrainConstants.CANbusName); 259 260 m_pigeon2 = new Pigeon2(driveTrainConstants.Pigeon2Id, driveTrainConstants.CANbusName); 261 262 Modules = new SwerveModule[ModuleCount]; 263 m_modulePositions = new SwerveModulePosition[ModuleCount]; 264 m_moduleLocations = new Translation2d[ModuleCount]; 265 266 int iteration = 0; 267 for (SwerveModuleConstants module : modules) { 268 Modules[iteration] = new SwerveModule(module, driveTrainConstants.CANbusName, 269 driveTrainConstants.SupportsPro); 270 m_moduleLocations[iteration] = new Translation2d(module.LocationX, module.LocationY); 271 m_modulePositions[iteration] = Modules[iteration].getPosition(true); 272 273 iteration++; 274 } 275 m_kinematics = new SwerveDriveKinematics(m_moduleLocations); 276 m_odometry = new SwerveDrivePoseEstimator(m_kinematics, new Rotation2d(), m_modulePositions, new Pose2d()); 277 278 m_fieldRelativeOffset = new Rotation2d(); 279 280 m_simDrive = new SimSwerveDrivetrain(m_moduleLocations, m_pigeon2, driveTrainConstants, modules); 281 282 m_odometryThread = new OdometryThread(); 283 m_odometryThread.start(); 284 } 285 286 /** 287 * Gets a reference to the data acquisition thread. 288 * 289 * @return DAQ thread 290 */ 291 public OdometryThread getDaqThread() { 292 return m_odometryThread; 293 } 294 295 /** 296 * Applies the specified control request to this swerve drivetrain. 297 * 298 * @param request Request to apply 299 */ 300 public void setControl(SwerveRequest request) { 301 try { 302 m_stateLock.writeLock().lock(); 303 304 m_requestToApply = request; 305 } finally { 306 m_stateLock.writeLock().unlock(); 307 } 308 } 309 310 /** 311 * Zero's this swerve drive's odometry entirely. 312 * <p> 313 * This will zero the entire odometry, and place the robot at 0,0 314 */ 315 public void tareEverything() { 316 try { 317 m_stateLock.writeLock().lock(); 318 319 for (int i = 0; i < ModuleCount; ++i) { 320 Modules[i].resetPosition(); 321 m_modulePositions[i] = Modules[i].getPosition(true); 322 } 323 m_odometry.resetPosition(m_pigeon2.getRotation2d(), m_modulePositions, new Pose2d()); 324 } finally { 325 m_stateLock.writeLock().unlock(); 326 } 327 } 328 329 /** 330 * Takes the current orientation of the robot and makes it X forward for 331 * field-relative 332 * maneuvers. 333 */ 334 public void seedFieldRelative() { 335 try { 336 m_stateLock.writeLock().lock(); 337 338 m_fieldRelativeOffset = getState().Pose.getRotation(); 339 } finally { 340 m_stateLock.writeLock().unlock(); 341 } 342 } 343 344 /** 345 * Takes the specified location and makes it the current pose for 346 * field-relative maneuvers 347 * 348 * @param location Pose to make the current pose at. 349 */ 350 public void seedFieldRelative(Pose2d location) { 351 try { 352 m_stateLock.writeLock().lock(); 353 354 m_fieldRelativeOffset = location.getRotation(); 355 m_odometry.resetPosition(location.getRotation(), m_modulePositions, location); 356 } finally { 357 m_stateLock.writeLock().unlock(); 358 } 359 } 360 361 /** 362 * Check if the odometry is currently valid 363 * 364 * @return True if odometry is valid 365 */ 366 public boolean odometryIsValid() { 367 try { 368 m_stateLock.readLock().lock(); 369 370 return m_odometryThread.odometryIsValid(); 371 } finally { 372 m_stateLock.readLock().unlock(); 373 } 374 } 375 376 /** 377 * Get a reference to the module at the specified index. 378 * The index corresponds to the module described in the constructor 379 * 380 * @param index Which module to get 381 * @return Reference to SwerveModule 382 */ 383 public SwerveModule getModule(int index) { 384 if (index >= Modules.length) 385 return null; 386 return Modules[index]; 387 } 388 389 /** 390 * Gets the current state of the swerve drivetrain. 391 * 392 * @return Current state of the drivetrain 393 */ 394 public SwerveDriveState getState() { 395 try { 396 m_stateLock.readLock().lock(); 397 398 return m_cachedState; 399 } finally { 400 m_stateLock.readLock().unlock(); 401 } 402 } 403 404 /** 405 * Adds a vision measurement to the Kalman Filter. This will correct the 406 * odometry pose estimate 407 * while still accounting for measurement noise. 408 * 409 * <p> 410 * This method can be called as infrequently as you want, as long as you are 411 * calling {@link 412 * SwerveDrivePoseEstimator#update} every loop. 413 * 414 * <p> 415 * To promote stability of the pose estimate and make it robust to bad vision 416 * data, we 417 * recommend only adding vision measurements that are already within one meter 418 * or so of the 419 * current pose estimate. 420 * 421 * <p> 422 * Note that the vision measurement standard deviations passed into this method 423 * will continue 424 * to apply to future measurements until a subsequent call to {@link 425 * SwerveDrivePoseEstimator#setVisionMeasurementStdDevs(Matrix)} or this method. 426 * 427 * @param visionRobotPoseMeters The pose of the robot as measured by the 428 * vision camera. 429 * @param timestampSeconds The timestamp of the vision measurement in 430 * seconds. Note that if you 431 * don't use your own time source by calling 432 * {@link 433 * SwerveDrivePoseEstimator#updateWithTime(double,Rotation2d,SwerveModulePosition[])}, 434 * then 435 * you must use a timestamp with an epoch since 436 * FPGA startup (i.e., the epoch of this 437 * timestamp is the same epoch as 438 * {@link edu.wpi.first.wpilibj.Timer#getFPGATimestamp()}). 439 * This means that you should use 440 * {@link edu.wpi.first.wpilibj.Timer#getFPGATimestamp()} 441 * as 442 * your time source in this case. 443 * @param visionMeasurementStdDevs Standard deviations of the vision pose 444 * measurement (x position 445 * in meters, y position in meters, and heading 446 * in radians). Increase these numbers to trust 447 * the vision pose measurement less. 448 */ 449 public void addVisionMeasurement( 450 Pose2d visionRobotPoseMeters, 451 double timestampSeconds, 452 Matrix<N3, N1> visionMeasurementStdDevs) { 453 try { 454 m_stateLock.writeLock().lock(); 455 m_odometry.addVisionMeasurement(visionRobotPoseMeters, timestampSeconds, visionMeasurementStdDevs); 456 } finally { 457 m_stateLock.writeLock().unlock(); 458 } 459 } 460 461 /** 462 * Adds a vision measurement to the Kalman Filter. This will correct the 463 * odometry pose estimate 464 * while still accounting for measurement noise. 465 * 466 * <p> 467 * This method can be called as infrequently as you want, as long as you are 468 * calling {@link 469 * SwerveDrivePoseEstimator#update} every loop. 470 * 471 * <p> 472 * To promote stability of the pose estimate and make it robust to bad vision 473 * data, we 474 * recommend only adding vision measurements that are already within one meter 475 * or so of the 476 * current pose estimate. 477 * 478 * @param visionRobotPoseMeters The pose of the robot as measured by the vision 479 * camera. 480 * @param timestampSeconds The timestamp of the vision measurement in 481 * seconds. Note that if you 482 * don't use your own time source by calling {@link 483 * SwerveDrivePoseEstimator#updateWithTime(double,Rotation2d,SwerveModulePosition[])} 484 * then you 485 * must use a timestamp with an epoch since FPGA 486 * startup (i.e., the epoch of this timestamp is 487 * the same epoch as 488 * {@link edu.wpi.first.wpilibj.Timer#getFPGATimestamp()}.) 489 * This means that 490 * you should use 491 * {@link edu.wpi.first.wpilibj.Timer#getFPGATimestamp()} 492 * as your time source 493 * or sync the epochs. 494 */ 495 public void addVisionMeasurement(Pose2d visionRobotPoseMeters, double timestampSeconds) { 496 try { 497 m_stateLock.writeLock().lock(); 498 m_odometry.addVisionMeasurement(visionRobotPoseMeters, timestampSeconds); 499 } finally { 500 m_stateLock.writeLock().unlock(); 501 } 502 } 503 504 /** 505 * Sets the pose estimator's trust of global measurements. This might be used to 506 * change trust in 507 * vision measurements after the autonomous period, or to change trust as 508 * distance to a vision 509 * target increases. 510 * 511 * @param visionMeasurementStdDevs Standard deviations of the vision 512 * measurements. Increase these 513 * numbers to trust global measurements from 514 * vision less. This matrix is in the form [x, 515 * y, 516 * theta]ᵀ, with units in meters and radians. 517 */ 518 public void setVisionMeasurementStdDevs(Matrix<N3, N1> visionMeasurementStdDevs) { 519 try { 520 m_stateLock.writeLock().lock(); 521 m_odometry.setVisionMeasurementStdDevs(visionMeasurementStdDevs); 522 } finally { 523 m_stateLock.writeLock().unlock(); 524 } 525 } 526 527 /** 528 * Updates all the simulation state variables for this 529 * drivetrain class. User provides the update variables for the simulation. 530 * 531 * @param dtSeconds time since last update call 532 * @param supplyVoltage voltage as seen at the motor controllers 533 */ 534 public void updateSimState(double dtSeconds, double supplyVoltage) { 535 m_simDrive.update(dtSeconds, supplyVoltage, Modules); 536 } 537 538 539 /** 540 * Register the specified lambda to be executed whenever our SwerveDriveState function 541 * is updated in our odometry thread. 542 * <p> 543 * It is imperative that this function is cheap, as it will be executed along with 544 * the odometry call, and if this takes a long time, it may negatively impact 545 * the odometry of this stack. 546 * <p> 547 * This can also be used for logging data if the function performs logging instead of telemetry 548 * 549 * @param telemetryFunction Function to call for telemetry or logging 550 */ 551 public void registerTelemetry(Consumer<SwerveDriveState> telemetryFunction) { 552 m_telemetryFunction = telemetryFunction; 553 } 554}