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}