diff --git a/.vscode/settings.json b/.vscode/settings.json index 5b6afd45..20c0cc80 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -34,10 +34,15 @@ "cSpell.words": [ "APRILTAG", "algs", + "brushless", "Cancoder", + "canbus", "CANID", "CANIDs", + "chargedup", + "closedloop", "CTRE", + "curr", "DEADBAND", "Feedforward", "gamepad", @@ -46,19 +51,27 @@ "Huskie", "jama", "javac", + "leds", "Nivore", "odometry", + "openloop", "photonvision", + "PIDF", "rapidreact", "REVPHJNI", "rezero", + "scurve", "Sedgewick", "setpoint", "SIMBOT", "Teleop", + "teleoperated", "TRACKWIDTH", "tunables", "Vandermonde", - "Xstance" + "VBAT", + "WPILOG", + "Xstance", + "YUYV" ] } diff --git a/build.gradle b/build.gradle index 02ef7b55..e4679cc9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id "java" - id "edu.wpi.first.GradleRIO" version "2023.2.1" + id "edu.wpi.first.GradleRIO" version "2023.4.3" id 'com.diffplug.spotless' version '6.11.0' id "com.peterabeles.gversion" version "1.10" } @@ -71,7 +71,8 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' - annotationProcessor "org.littletonrobotics.akit.junction:junction-autolog:2.0.0" + def akitJson = new groovy.json.JsonSlurper().parseText(new File(projectDir.getAbsolutePath() + "/vendordeps/AdvantageKit.json").text) + annotationProcessor "org.littletonrobotics.akit.junction:junction-autolog:$akitJson.version" implementation "gov.nist.math:jama:1.0.3" } diff --git a/src/main/deploy/2022-rapidreact.json b/src/main/deploy/2022-rapidreact.json deleted file mode 100644 index a6243cf3..00000000 --- a/src/main/deploy/2022-rapidreact.json +++ /dev/null @@ -1,415 +0,0 @@ -{ - "tags" : [ { - "ID" : 0, - "pose" : { - "translation" : { - "x" : -0.0035306, - "y" : 7.578928199999999, - "z" : 0.8858503999999999 - }, - "rotation" : { - "quaternion" : { - "W" : 1.0, - "X" : 0.0, - "Y" : 0.0, - "Z" : 0.0 - } - } - } - }, { - "ID" : 1, - "pose" : { - "translation" : { - "x" : 3.2327088, - "y" : 5.486654, - "z" : 1.7254728 - }, - "rotation" : { - "quaternion" : { - "W" : 1.0, - "X" : 0.0, - "Y" : 0.0, - "Z" : 0.0 - } - } - } - }, { - "ID" : 2, - "pose" : { - "translation" : { - "x" : 3.067812, - "y" : 5.3305202, - "z" : 1.3762228 - }, - "rotation" : { - "quaternion" : { - "W" : 0.7071067811865476, - "X" : 0.0, - "Y" : 0.0, - "Z" : -0.7071067811865475 - } - } - } - }, { - "ID" : 3, - "pose" : { - "translation" : { - "x" : 0.0039878, - "y" : 5.058536999999999, - "z" : 0.80645 - }, - "rotation" : { - "quaternion" : { - "W" : 1.0, - "X" : 0.0, - "Y" : 0.0, - "Z" : 0.0 - } - } - } - }, { - "ID" : 4, - "pose" : { - "translation" : { - "x" : 0.0039878, - "y" : 3.5124898, - "z" : 0.80645 - }, - "rotation" : { - "quaternion" : { - "W" : 1.0, - "X" : 0.0, - "Y" : 0.0, - "Z" : 0.0 - } - } - } - }, { - "ID" : 5, - "pose" : { - "translation" : { - "x" : 0.12110719999999998, - "y" : 1.7178274, - "z" : 0.8906002000000001 - }, - "rotation" : { - "quaternion" : { - "W" : 0.9196502204050923, - "X" : 0.0, - "Y" : 0.0, - "Z" : 0.39273842708457407 - } - } - } - }, { - "ID" : 6, - "pose" : { - "translation" : { - "x" : 0.8733027999999999, - "y" : 0.9412985999999999, - "z" : 0.8906002000000001 - }, - "rotation" : { - "quaternion" : { - "W" : 0.9196502204050923, - "X" : 0.0, - "Y" : 0.0, - "Z" : 0.39273842708457407 - } - } - } - }, { - "ID" : 7, - "pose" : { - "translation" : { - "x" : 1.6150844, - "y" : 0.15725139999999999, - "z" : 0.8906002000000001 - }, - "rotation" : { - "quaternion" : { - "W" : 0.9196502204050923, - "X" : 0.0, - "Y" : 0.0, - "Z" : 0.39273842708457407 - } - } - } - }, { - "ID" : 10, - "pose" : { - "translation" : { - "x" : 16.4627306, - "y" : 0.6506718, - "z" : 0.8858503999999999 - }, - "rotation" : { - "quaternion" : { - "W" : 6.123233995736766E-17, - "X" : 0.0, - "Y" : 0.0, - "Z" : 1.0 - } - } - } - }, { - "ID" : 11, - "pose" : { - "translation" : { - "x" : 13.2350002, - "y" : 2.743454, - "z" : 1.7254728 - }, - "rotation" : { - "quaternion" : { - "W" : 6.123233995736766E-17, - "X" : 0.0, - "Y" : 0.0, - "Z" : 1.0 - } - } - } - }, { - "ID" : 12, - "pose" : { - "translation" : { - "x" : 13.391388000000001, - "y" : 2.8998418, - "z" : 1.3762228 - }, - "rotation" : { - "quaternion" : { - "W" : 0.7071067811865476, - "X" : 0.0, - "Y" : 0.0, - "Z" : 0.7071067811865475 - } - } - } - }, { - "ID" : 13, - "pose" : { - "translation" : { - "x" : 16.4552122, - "y" : 3.1755079999999998, - "z" : 0.80645 - }, - "rotation" : { - "quaternion" : { - "W" : 6.123233995736766E-17, - "X" : 0.0, - "Y" : 0.0, - "Z" : 1.0 - } - } - } - }, { - "ID" : 14, - "pose" : { - "translation" : { - "x" : 16.4552122, - "y" : 4.7171356, - "z" : 0.80645 - }, - "rotation" : { - "quaternion" : { - "W" : 6.123233995736766E-17, - "X" : 0.0, - "Y" : 0.0, - "Z" : 1.0 - } - } - } - }, { - "ID" : 15, - "pose" : { - "translation" : { - "x" : 16.3350194, - "y" : 6.5149729999999995, - "z" : 0.8937752 - }, - "rotation" : { - "quaternion" : { - "W" : -0.37298778257580906, - "X" : -0.0, - "Y" : 0.0, - "Z" : 0.9278362538989199 - } - } - } - }, { - "ID" : 16, - "pose" : { - "translation" : { - "x" : 15.5904946, - "y" : 7.292695599999999, - "z" : 0.8906002000000001 - }, - "rotation" : { - "quaternion" : { - "W" : -0.37298778257580906, - "X" : -0.0, - "Y" : 0.0, - "Z" : 0.9278362538989199 - } - } - } - }, { - "ID" : 17, - "pose" : { - "translation" : { - "x" : 14.847188999999998, - "y" : 8.0691228, - "z" : 0.8906002000000001 - }, - "rotation" : { - "quaternion" : { - "W" : -0.37298778257580906, - "X" : -0.0, - "Y" : 0.0, - "Z" : 0.9278362538989199 - } - } - } - }, { - "ID" : 40, - "pose" : { - "translation" : { - "x" : 7.874127, - "y" : 4.9131728, - "z" : 0.7032752 - }, - "rotation" : { - "quaternion" : { - "W" : 0.5446390350150271, - "X" : 0.0, - "Y" : 0.0, - "Z" : 0.838670567945424 - } - } - } - }, { - "ID" : 41, - "pose" : { - "translation" : { - "x" : 7.4312271999999995, - "y" : 3.759327, - "z" : 0.7032752 - }, - "rotation" : { - "quaternion" : { - "W" : -0.20791169081775934, - "X" : -0.0, - "Y" : 0.0, - "Z" : 0.9781476007338057 - } - } - } - }, { - "ID" : 42, - "pose" : { - "translation" : { - "x" : 8.585073, - "y" : 3.3164272, - "z" : 0.7032752 - }, - "rotation" : { - "quaternion" : { - "W" : 0.838670567945424, - "X" : 0.0, - "Y" : 0.0, - "Z" : -0.5446390350150271 - } - } - } - }, { - "ID" : 43, - "pose" : { - "translation" : { - "x" : 9.0279728, - "y" : 4.470273, - "z" : 0.7032752 - }, - "rotation" : { - "quaternion" : { - "W" : 0.9781476007338057, - "X" : 0.0, - "Y" : 0.0, - "Z" : 0.20791169081775934 - } - } - } - }, { - "ID" : 50, - "pose" : { - "translation" : { - "x" : 7.6790296, - "y" : 4.3261534, - "z" : 2.4177244 - }, - "rotation" : { - "quaternion" : { - "W" : 0.17729273396782605, - "X" : -0.22744989571511945, - "Y" : 0.04215534644161733, - "Z" : 0.9565859910053995 - } - } - } - }, { - "ID" : 51, - "pose" : { - "translation" : { - "x" : 8.0182466, - "y" : 3.5642296, - "z" : 2.4177244 - }, - "rotation" : { - "quaternion" : { - "W" : -0.5510435465842192, - "X" : -0.19063969497246985, - "Y" : -0.13102303230819815, - "Z" : 0.8017733354717242 - } - } - } - }, { - "ID" : 52, - "pose" : { - "translation" : { - "x" : 8.7801704, - "y" : 3.9034466, - "z" : 2.4177244 - }, - "rotation" : { - "quaternion" : { - "W" : -0.9565859910053994, - "X" : -0.04215534644161739, - "Y" : -0.22744989571511942, - "Z" : 0.17729273396782633 - } - } - } - }, { - "ID" : 53, - "pose" : { - "translation" : { - "x" : 8.4409534, - "y" : 4.6653704, - "z" : 2.4177244 - }, - "rotation" : { - "quaternion" : { - "W" : 0.8017733354717241, - "X" : -0.1310230323081982, - "Y" : 0.19063969497246983, - "Z" : 0.5510435465842194 - } - } - } - } ], - "field" : { - "length" : 16.4592, - "width" : 8.2296 - } - } \ No newline at end of file diff --git a/src/main/deploy/pathplanner/DistanceTest.path b/src/main/deploy/pathplanner/DistanceTest.path new file mode 100644 index 00000000..bfbb3c47 --- /dev/null +++ b/src/main/deploy/pathplanner/DistanceTest.path @@ -0,0 +1,49 @@ +{ + "waypoints": [ + { + "anchorPoint": { + "x": 1.8184265428359916, + "y": 4.396379732930406 + }, + "prevControl": null, + "nextControl": { + "x": 2.818426542835991, + "y": 4.396379732930406 + }, + "holonomicAngle": 0, + "isReversal": false, + "velOverride": null, + "isLocked": false, + "isStopPoint": false, + "stopEvent": { + "names": [], + "executionBehavior": "parallel", + "waitBehavior": "none", + "waitTime": 0 + } + }, + { + "anchorPoint": { + "x": 5.82, + "y": 4.4 + }, + "prevControl": { + "x": 4.59818164446877, + "y": 4.4 + }, + "nextControl": null, + "holonomicAngle": 0, + "isReversal": false, + "velOverride": null, + "isLocked": false, + "isStopPoint": false, + "stopEvent": { + "names": [], + "executionBehavior": "parallel", + "waitBehavior": "none", + "waitTime": 0 + } + } + ], + "markers": [] +} \ No newline at end of file diff --git a/src/main/deploy/pathplanner/StartPoint.path b/src/main/deploy/pathplanner/StartPoint.path new file mode 100644 index 00000000..842c5f23 --- /dev/null +++ b/src/main/deploy/pathplanner/StartPoint.path @@ -0,0 +1,49 @@ +{ + "waypoints": [ + { + "anchorPoint": { + "x": 4.480580999076691, + "y": 1.0827644404103984 + }, + "prevControl": null, + "nextControl": { + "x": 5.480580999076694, + "y": 1.0827644404103984 + }, + "holonomicAngle": 0, + "isReversal": false, + "velOverride": null, + "isLocked": false, + "isStopPoint": false, + "stopEvent": { + "names": [], + "executionBehavior": "parallel", + "waitBehavior": "none", + "waitTime": 0 + } + }, + { + "anchorPoint": { + "x": 6.902554089468368, + "y": 1.1492499762250707 + }, + "prevControl": { + "x": 5.902554089468368, + "y": 1.1492499762250707 + }, + "nextControl": null, + "holonomicAngle": 0, + "isReversal": false, + "velOverride": null, + "isLocked": false, + "isStopPoint": false, + "stopEvent": { + "names": [], + "executionBehavior": "parallel", + "waitBehavior": "none", + "waitTime": 0 + } + } + ], + "markers": [] +} \ No newline at end of file diff --git a/src/main/deploy/pathplanner/testPaths1.path b/src/main/deploy/pathplanner/TestPath.path similarity index 100% rename from src/main/deploy/pathplanner/testPaths1.path rename to src/main/deploy/pathplanner/TestPath.path diff --git a/src/main/deploy/pathplanner/Tuning.path b/src/main/deploy/pathplanner/Tuning.path new file mode 100644 index 00000000..bc332ca3 --- /dev/null +++ b/src/main/deploy/pathplanner/Tuning.path @@ -0,0 +1,74 @@ +{ + "waypoints": [ + { + "anchorPoint": { + "x": 7.070195642127396, + "y": 0.34885558660006205 + }, + "prevControl": null, + "nextControl": { + "x": 8.937466045070979, + "y": 1.0829252127254254 + }, + "holonomicAngle": 90.0, + "isReversal": false, + "velOverride": null, + "isLocked": false, + "isStopPoint": false, + "stopEvent": { + "names": [], + "executionBehavior": "parallel", + "waitBehavior": "none", + "waitTime": 0 + } + }, + { + "anchorPoint": { + "x": 7.090433441283549, + "y": 5.118539523537381 + }, + "prevControl": { + "x": 8.486203715462986, + "y": 4.754665054468998 + }, + "nextControl": { + "x": 8.486203715462986, + "y": 4.754665054468998 + }, + "holonomicAngle": -90.0, + "isReversal": true, + "velOverride": null, + "isLocked": false, + "isStopPoint": false, + "stopEvent": { + "names": [], + "executionBehavior": "parallel", + "waitBehavior": "none", + "waitTime": 0 + } + }, + { + "anchorPoint": { + "x": 7.07, + "y": 0.35 + }, + "prevControl": { + "x": 8.94280093768521, + "y": 1.0549170265007195 + }, + "nextControl": null, + "holonomicAngle": 90.0, + "isReversal": false, + "velOverride": null, + "isLocked": false, + "isStopPoint": false, + "stopEvent": { + "names": [], + "executionBehavior": "parallel", + "waitBehavior": "none", + "waitTime": 0 + } + } + ], + "markers": [] +} \ No newline at end of file diff --git a/src/main/java/frc/lib/team254/drivers/TalonFXFactory.java b/src/main/java/frc/lib/team254/drivers/TalonFXFactory.java index e11346a2..7975fba4 100644 --- a/src/main/java/frc/lib/team254/drivers/TalonFXFactory.java +++ b/src/main/java/frc/lib/team254/drivers/TalonFXFactory.java @@ -37,6 +37,9 @@ public static class Configuration { public int FORWARD_SOFT_LIMIT = 0; public int REVERSE_SOFT_LIMIT = 0; + public double PEAK_OUTPUT_FORWARD = 1.0; + public double PEAK_OUTPUT_REVERSE = -1.0; + public boolean INVERTED = false; public boolean SENSOR_PHASE = false; public SensorInitializationStrategy SENSOR_INITIALIZATION_STRATEGY = @@ -55,6 +58,7 @@ public static class Configuration { public int BASE_PIDF0_STATUS_FRAME_RATE_MS = kPrimePeriods[5]; public int TURN_PIDF1_STATUS_FRAME_RATE_MS = kPrimePeriods[6]; public int FEEDBACK_INTEGRATED_STATUS_FRAME_RATE_MS = kPrimePeriods[7]; + public int BRUSHLESS_CURRENT_STATUS_FRAME_RATE_MS = kPrimePeriods[8]; public SensorVelocityMeasPeriod VELOCITY_MEASUREMENT_PERIOD = SensorVelocityMeasPeriod.Period_100Ms; @@ -72,6 +76,13 @@ public static class Configuration { public double SLOT0_KI = 0.0; public double SLOT0_KD = 0.0; public double SLOT0_KF = 0.0; + + public int REMOTE_SENSOR_DEVICE_ID = 0; + public RemoteSensorSource REMOTE_SENSOR_SOURCE = RemoteSensorSource.Off; + + public double MOTION_ACCELERATION = 0.0; + public double MOTION_CRUISE_VELOCITY = 0.0; + public int MOTION_CURVE_STRENGTH = 0; } private static final Configuration kDefaultConfiguration = new Configuration(); @@ -121,8 +132,8 @@ public static TalonFX createTalon(int id, String canBusName, Configuration confi talonFXConfig.nominalOutputReverse = 0.0; talonFXConfig.neutralDeadband = config.NEUTRAL_DEADBAND; - talonFXConfig.peakOutputForward = 1.0; - talonFXConfig.peakOutputReverse = -1.0; + talonFXConfig.peakOutputForward = config.PEAK_OUTPUT_FORWARD; + talonFXConfig.peakOutputReverse = config.PEAK_OUTPUT_REVERSE; talonFXConfig.forwardSoftLimitThreshold = config.FORWARD_SOFT_LIMIT; talonFXConfig.forwardSoftLimitEnable = config.ENABLE_SOFT_LIMIT; @@ -148,6 +159,13 @@ public static TalonFX createTalon(int id, String canBusName, Configuration confi talonFXConfig.slot0.kD = config.SLOT0_KD; talonFXConfig.slot0.kF = config.SLOT0_KF; + talonFXConfig.motionAcceleration = config.MOTION_ACCELERATION; + talonFXConfig.motionCruiseVelocity = config.MOTION_CRUISE_VELOCITY; + talonFXConfig.motionCurveStrength = config.MOTION_CURVE_STRENGTH; + + talonFXConfig.remoteFilter0.remoteSensorDeviceID = config.REMOTE_SENSOR_DEVICE_ID; + talonFXConfig.remoteFilter0.remoteSensorSource = config.REMOTE_SENSOR_SOURCE; + talon.configAllSettings(talonFXConfig, TIMEOUT_MS); talon.configSelectedFeedbackSensor(FeedbackDevice.IntegratedSensor); @@ -164,8 +182,8 @@ public static TalonFX createTalon(int id, String canBusName, Configuration confi talon.overrideSoftLimitsEnable(config.ENABLE_SOFT_LIMIT); - talon.setInverted(config.INVERTED); talon.setSensorPhase(config.SENSOR_PHASE); + talon.setInverted(config.INVERTED); talon.selectProfileSlot(0, 0); @@ -207,6 +225,10 @@ public static TalonFX createTalon(int id, String canBusName, Configuration confi StatusFrameEnhanced.Status_21_FeedbackIntegrated, config.FEEDBACK_INTEGRATED_STATUS_FRAME_RATE_MS, TIMEOUT_MS); + talon.setStatusFramePeriod( + StatusFrameEnhanced.Status_Brushless_Current, + config.BRUSHLESS_CURRENT_STATUS_FRAME_RATE_MS, + TIMEOUT_MS); talon.setControlFramePeriod(ControlFrame.Control_3_General, config.CONTROL_FRAME_PERIOD_MS); diff --git a/src/main/java/frc/lib/team3061/RobotConfig.java b/src/main/java/frc/lib/team3061/RobotConfig.java index 023c4e4c..191589ad 100644 --- a/src/main/java/frc/lib/team3061/RobotConfig.java +++ b/src/main/java/frc/lib/team3061/RobotConfig.java @@ -1,9 +1,7 @@ package frc.lib.team3061; -import edu.wpi.first.math.geometry.Rotation3d; import edu.wpi.first.math.geometry.Transform3d; import edu.wpi.first.math.geometry.Translation2d; -import edu.wpi.first.math.geometry.Translation3d; import edu.wpi.first.math.kinematics.SwerveDriveKinematics; import frc.lib.team3061.swerve.SwerveModuleConstants.SwerveType; @@ -12,6 +10,11 @@ public abstract class RobotConfig { private static RobotConfig robotConfig; + /** + * Returns the singleton instance of the RobotConfig object. + * + * @return the singleton instance of the RobotConfig object + */ public static RobotConfig getInstance() { return robotConfig; } @@ -138,7 +141,6 @@ public double getDriveKA() { */ public abstract SwerveType getSwerveType(); - // Swerve Module CAN IDs (FL, FR, BL, BR) /** * Returns the CAN IDs for the swerve modules' drive motors in the order of front left, front * right, back left, and back right. Must be overridden. @@ -238,23 +240,33 @@ public double getRobotLengthWithBumpers() { } /** - * Returns the 3D transform from the center of the robot to the center of the camera. The units - * are meters and radians. Defaults to the robot's center on the floor. + * Returns the 3D transforms from the center of the robot to the center of each camera. The units + * are meters and radians. Defaults to an empty array specifying no cameras. * - * @return the 3D transform from the center of the robot to the center of the camera + * @return the 3D transforms from the center of the robot to the center of each camera */ - public Transform3d getRobotToCameraTransform() { - return new Transform3d(new Translation3d(0, 0, 0), new Rotation3d(0, 0, 0)); + public Transform3d[] getRobotToCameraTransforms() { + return new Transform3d[] {}; } /** - * Returns the maximum velocity of the robot in meters per second. This is a measure of how fast - * the robot should be able to drive in a straight line. Must be overridden. + * Returns the maximum translational velocity of the robot in meters per second. This is a measure + * of how fast the robot should be able to drive in a straight line. Must be overridden. * * @return the maximum velocity of the robot in meters per second */ public abstract double getRobotMaxVelocity(); + /** + * Returns the multiplier for when the robot is in slow mode. Defaults to 1 (no effect in slow + * mode). + * + * @return the multiplier for when the robot is in slow mode + */ + public double getRobotSlowModeMultiplier() { + return 1.0; + } + /** * The maximum angular velocity of the robot in radians per second. This is a measure of how fast * the robot can rotate in place. By default it is calculated based on the maximum velocity and @@ -266,6 +278,26 @@ public double getRobotMaxAngularVelocity() { return getRobotMaxVelocity() / Math.hypot(getTrackwidth() / 2.0, getWheelbase() / 2.0); } + /** + * Returns the maximum translational acceleration of the robot in meters per second squared. + * Defaults to 1000 m/s/s. + * + * @return the maximum translational acceleration of the robot in meters per second squared + */ + public double getRobotMaxDriveAcceleration() { + return 1000.0; + } + + /** + * Returns the maximum angular acceleration of the robot in radians per second squared. Defaults + * to 1000 rad/s/s. + * + * @return the maximum angular acceleration of the robot in radians per second squared + */ + public double getRobotMaxTurnAcceleration() { + return 1000.0; + } + /** * Returns the maximum velocity, in meters per second, at which the robot can be moving while * disabled before the drive motors are changed from brake to coast mode. Defaults to 0. @@ -371,12 +403,13 @@ public String getCANBusName() { } /** - * Returns the name of the camera used by the vision subsystem. Defaults to "". + * Returns the names of the cameras used by the vision subsystem. Defaults to an empty array (no + * cameras). * - * @return the name of the camera used by the vision subsystem + * @return the names of the cameras used by the vision subsystem */ - public String getCameraName() { - return ""; + public String[] getCameraNames() { + return new String[] {}; } /** @@ -416,4 +449,151 @@ public int getRevHighPressureSensorChannel() { public int getRevLowPressureSensorChannel() { return 1; } + + /** + * Returns the the proportional constant for the PID controller for translational motion when + * driving to a specific pose. Defaults to 0. + * + * @return the proportional constant for the PID controller for translational motion when driving + * to a specific pose + */ + public double getDriveToPoseDriveKP() { + return 0.0; + } + + /** + * Returns the integral constant for the PID controller for translational motion when driving to a + * specific pose. Defaults to 0. + * + * @return the integral constant for the PID controller for translational motion when driving to a + * specific pose + */ + public double getDriveToPoseDriveKI() { + return 0.0; + } + + /** + * Returns the derivative constant for the PID controller for translational motion when driving to + * a specific pose. Defaults to 0. + * + * @return the derivative constant for the PID controller for translational motion when driving to + * a specific pose + */ + public double getDriveToPoseDriveKD() { + return 0.0; + } + + /** + * Returns the proportional constant for the PID controller for rotational motion when driving to + * a specific pose. Defaults to 0. + * + * @return the proportional constant for the PID controller for rotational motion when driving to + * a specific pose + */ + public double getDriveToPoseThetaKP() { + return 0.0; + } + + /** + * Returns the integral constant for the PID controller for rotational motion when driving to a + * specific pose. Defaults to 0. + * + * @return the integral constant for the PID controller for rotational motion when driving to a + * specific pose + */ + public double getDriveToPoseThetaKI() { + return 0.0; + } + + /** + * Returns the derivative constant for the PID controller for rotational motion when driving to a + * specific pose. Defaults to 0. + * + * @return the derivative constant for the PID controller for rotational motion when driving to a + * specific pose + */ + public double getDriveToPoseThetaKD() { + return 0.0; + } + + /** + * Returns the maximum translational speed, in meters per second, for the robot during the + * drive-to-pose command. Defaults to the maximum speed for autonomous. + * + * @return the maximum translational speed, in meters per second, for the robot during the + * drive-to-pose command + */ + public double getDriveToPoseDriveMaxVelocity() { + return getAutoMaxSpeed(); + } + + /** + * Returns the maximum translational acceleration, in meters per second squared, for the robot + * during the drive-to-pose command. Defaults to the maximum acceleration for autonomous. + * + * @return the maximum translational acceleration, in meters per second squared, for the robot + * during the drive-to-pose command + */ + public double getDriveToPoseDriveMaxAcceleration() { + return getAutoMaxAcceleration(); + } + + /** + * Returns the maximum rotational speed, in radians per second, for the robot during the + * drive-to-pose command. Defaults to the velocity derived from the maximum translational speed + * and hte robot's geometry. + * + * @return the maximum rotational speed, in radians per second, for the robot during the + * drive-to-pose command + */ + public double getDriveToPoseTurnMaxVelocity() { + return getDriveToPoseDriveMaxVelocity() + / Math.hypot(getTrackwidth() / 2.0, getWheelbase() / 2.0); + } + + /** + * Returns the maximum rotational acceleration, in radians per second squared, for the robot + * during the drive-to-pose command. Defaults to the acceleration derived from the maximum + * translational acceleration and the robot's geometry. + * + * @return the maximum rotational acceleration, in radians per second squared, for the robot + * during the drive-to-pose command + */ + public double getDriveToPoseTurnMaxAcceleration() { + return getDriveToPoseDriveMaxAcceleration() + / Math.hypot(getTrackwidth() / 2.0, getWheelbase() / 2.0); + } + + /** + * Returns the tolerance, in meters, for which the robot's position is considered at the specified + * pose during the drive-to-pose command. Defaults to 0. + * + * @return the tolerance, in meters, for which the robot's position is considered at the specified + * pose during the drive-to-pose command + */ + public double getDriveToPoseDriveTolerance() { + return 0.0; + } + + /** + * Returns the tolerance, in radians, for which the robot's heading is considered at the specified + * pose during the drive-to-pose command. Defaults to 0. + * + * @return the tolerance, in radians, for which the robot's heading is considered at the specified + * pose during the drive-to-pose command + */ + public double getDriveToPoseThetaTolerance() { + return 0.0; + } + + /** + * Returns the velocity, in meters per second, of the robot when driving into a field element + * during a move-to-pose command. Defaults to 0. + * + * @return the velocity, in meters per second, of the robot when driving into a field element + * during a move-to-pose command + */ + public double getMoveToPathFinalVelocity() { + return 0; + } } diff --git a/src/main/java/frc/lib/team3061/drivers/SparkMAXFactory.java b/src/main/java/frc/lib/team3061/drivers/SparkMAXFactory.java new file mode 100644 index 00000000..a7042740 --- /dev/null +++ b/src/main/java/frc/lib/team3061/drivers/SparkMAXFactory.java @@ -0,0 +1,110 @@ +package frc.lib.team3061.drivers; + +import com.revrobotics.CANSparkMax; +import com.revrobotics.CANSparkMaxLowLevel.MotorType; +import com.revrobotics.CANSparkMaxLowLevel.PeriodicFrame; + +@java.lang.SuppressWarnings({"java:S1104", "java:S116"}) + +/** + * Creates SparkMAX objects and configures all the parameters we care about to factory defaults. + * Closed-loop and sensor parameters are not set, as these are expected to be set by the + * application. + */ +public class SparkMAXFactory { + + // These periods don't share any common factors, so they shouldn't run at the same time. 255 is + // max. (initially from https://github.com/Mechanical-Advantage/RobotCode2022) + private static int[] kPrimePeriods = + new int[] {255, 254, 253, 251, 247, 241, 239, 233, 229, 227, 223, 217, 211, 199, 197}; + + public static class Configuration { + public boolean ENABLE_VOLTAGE_COMPENSATION = true; + public double VOLTAGE_COMPENSATION = 12.0; + + public boolean ENABLE_SOFT_LIMIT = false; + public int FORWARD_SOFT_LIMIT = 0; + public int REVERSE_SOFT_LIMIT = 0; + + public double CLOSED_LOOP_RAMP_RATE = 0.0; + public double OPEN_LOOP_RAMP_RATE = 0.0; + + public CANSparkMax.IdleMode IDLE_MODE = CANSparkMax.IdleMode.kCoast; + + public boolean INVERTED = false; + + public int SMART_FREE_CURRENT_LIMIT = 30; + public int SMART_STALL_CURRENT_LIMIT = 20; + + public int CAN_TIMEOUT_MS = 0; + + public int STATUS0_FRAME_RATE_MS = kPrimePeriods[0]; + public int STATUS1_FRAME_RATE_MS = kPrimePeriods[1]; + public int STATUS2_FRAME_RATE_MS = kPrimePeriods[2]; + public int STATUS3_FRAME_RATE_MS = kPrimePeriods[3]; + public int STATUS4_FRAME_RATE_MS = kPrimePeriods[4]; + public int STATUS5_FRAME_RATE_MS = kPrimePeriods[5]; + public int STATUS6_FRAME_RATE_MS = kPrimePeriods[6]; + public int CONTROL_FRAME_RATE_MS = 10; + } + + private static final Configuration kDefaultConfiguration = new Configuration(); + private static final Configuration kFollowerConfiguration = new Configuration(); + + // create a CANTalon with the default (out of the box) configuration + public static CANSparkMax createDefaultSparkMax(int id) { + return createSparkMax(id, kDefaultConfiguration); + } + + public static CANSparkMax createPermanentFollowerTalon(int id, CANSparkMax leader) { + final CANSparkMax sparkMax = createSparkMax(id, kFollowerConfiguration); + sparkMax.follow(leader); + return sparkMax; + } + + public static CANSparkMax createSparkMax(int id, Configuration config) { + CANSparkMax sparkMax = new CANSparkMax(id, MotorType.kBrushless); + + sparkMax.restoreFactoryDefaults(); + + sparkMax.setCANTimeout(config.CAN_TIMEOUT_MS); + + if (config.ENABLE_VOLTAGE_COMPENSATION) { + sparkMax.enableVoltageCompensation(config.VOLTAGE_COMPENSATION); + } + + if (config.ENABLE_SOFT_LIMIT) { + sparkMax.setSoftLimit(CANSparkMax.SoftLimitDirection.kForward, config.FORWARD_SOFT_LIMIT); + sparkMax.setSoftLimit(CANSparkMax.SoftLimitDirection.kReverse, config.REVERSE_SOFT_LIMIT); + } + + sparkMax.setClosedLoopRampRate(config.CLOSED_LOOP_RAMP_RATE); + sparkMax.setOpenLoopRampRate(config.OPEN_LOOP_RAMP_RATE); + + sparkMax.setIdleMode(config.IDLE_MODE); + + sparkMax.setInverted(config.INVERTED); + + sparkMax.setSmartCurrentLimit( + config.SMART_STALL_CURRENT_LIMIT, config.SMART_FREE_CURRENT_LIMIT); + + sparkMax.clearFaults(); + + sparkMax.setPeriodicFramePeriod(PeriodicFrame.kStatus0, config.STATUS0_FRAME_RATE_MS); + sparkMax.setPeriodicFramePeriod(PeriodicFrame.kStatus1, config.STATUS1_FRAME_RATE_MS); + sparkMax.setPeriodicFramePeriod(PeriodicFrame.kStatus2, config.STATUS2_FRAME_RATE_MS); + sparkMax.setPeriodicFramePeriod(PeriodicFrame.kStatus3, config.STATUS3_FRAME_RATE_MS); + sparkMax.setPeriodicFramePeriod(PeriodicFrame.kStatus4, config.STATUS4_FRAME_RATE_MS); + sparkMax.setPeriodicFramePeriod(PeriodicFrame.kStatus5, config.STATUS5_FRAME_RATE_MS); + sparkMax.setPeriodicFramePeriod(PeriodicFrame.kStatus6, config.STATUS6_FRAME_RATE_MS); + + sparkMax.setControlFramePeriodMs(config.CONTROL_FRAME_RATE_MS); + + // invoke set last which updates control frame period + sparkMax.set(0.0); + + return sparkMax; + } + + private SparkMAXFactory() {} +} diff --git a/src/main/java/frc/lib/team3061/gyro/GyroIO.java b/src/main/java/frc/lib/team3061/gyro/GyroIO.java index 26c16b89..d59d3d2e 100644 --- a/src/main/java/frc/lib/team3061/gyro/GyroIO.java +++ b/src/main/java/frc/lib/team3061/gyro/GyroIO.java @@ -11,22 +11,29 @@ /** * Gyro hardware abstraction interface * + *

The coordinate system for the gyro interface is the same as the Pigeon 2.0's coordinate + * system: https://store.ctr-electronics.com/content/user-manual/Pigeon2%20User's%20Guide.pdf + * *

The Gyro is modeled not as a subsystem but rather a shared resource that may be used by * multiple subsystems. Since it doesn't have a periodic method like a subsystem, it is important * that its updateInputs method is called just once via another periodic method. (This requires some * coordination, and, in this library, it is invoked via the drivetrain subsystem's periodic - * mehtod.) + * method.) * *

There is not a simulated version of this interface. Instead, the drivetrain supports - * determining the robot's rotation from the gryo when connected and via the swwerve module - * positions when not connected. + * determining the robot's rotation from the gyro when connected and via the swerve module positions + * when not connected. */ public interface GyroIO { @AutoLog public static class GyroIOInputs { public boolean connected = false; - public double positionDeg = 0.0; - public double velocityDegPerSec = 0.0; + public double yawDeg = 0.0; + public double yawDegPerSec = 0.0; + public double pitchDeg = 0.0; + public double pitchDegPerSec = 0.0; + public double rollDeg = 0.0; + public double rollDegPerSec = 0.0; } /** diff --git a/src/main/java/frc/lib/team3061/gyro/GyroIOPigeon2.java b/src/main/java/frc/lib/team3061/gyro/GyroIOPigeon2.java index 1fca73bc..b87a2b08 100644 --- a/src/main/java/frc/lib/team3061/gyro/GyroIOPigeon2.java +++ b/src/main/java/frc/lib/team3061/gyro/GyroIOPigeon2.java @@ -6,6 +6,7 @@ import com.ctre.phoenix.ErrorCode; import com.ctre.phoenix.sensors.Pigeon2; +import com.ctre.phoenix.sensors.PigeonIMU_StatusFrame; import frc.lib.team3061.RobotConfig; public class GyroIOPigeon2 implements GyroIO { @@ -14,13 +15,18 @@ public class GyroIOPigeon2 implements GyroIO { public GyroIOPigeon2(int id) { gyro = new Pigeon2(id, RobotConfig.getInstance().getCANBusName()); + this.gyro.setStatusFramePeriod(PigeonIMU_StatusFrame.CondStatus_9_SixDeg_YPR, 9); } @Override public void updateInputs(GyroIOInputs inputs) { gyro.getRawGyro(xyzDps); inputs.connected = gyro.getLastError().equals(ErrorCode.OK); - inputs.positionDeg = gyro.getYaw(); // degrees - inputs.velocityDegPerSec = xyzDps[2]; // degrees per second + inputs.yawDeg = gyro.getYaw(); // degrees + inputs.pitchDeg = gyro.getPitch(); // degrees + inputs.rollDeg = gyro.getRoll(); // degrees + inputs.rollDegPerSec = xyzDps[0]; // degrees per second + inputs.pitchDegPerSec = xyzDps[1]; // degrees per second + inputs.yawDegPerSec = xyzDps[2]; // degrees per second } } diff --git a/src/main/java/frc/lib/team3061/swerve/Conversions.java b/src/main/java/frc/lib/team3061/swerve/Conversions.java index 174fbe2d..8bb29c8e 100644 --- a/src/main/java/frc/lib/team3061/swerve/Conversions.java +++ b/src/main/java/frc/lib/team3061/swerve/Conversions.java @@ -57,13 +57,25 @@ public static double falconToMeters(double counts, double circumference, double } /** - * @param velocitycounts Falcon Velocity Counts + * @param meters meters + * @param circumference Circumference of Wheel + * @param gearRatio Gear Ratio between Falcon and Mechanism (set to 1 for Falcon RPM) + * @return Falcon Counts + */ + public static double metersToFalcon(double meters, double circumference, double gearRatio) { + double wheelRotations = meters / circumference; + double motorRotations = wheelRotations * gearRatio; + return motorRotations * 2048.0; + } + + /** + * @param velocityCounts Falcon Velocity Counts * @param circumference Circumference of Wheel * @param gearRatio Gear Ratio between Falcon and Mechanism (set to 1 for Falcon RPM) * @return Falcon Velocity Counts */ - public static double falconToMPS(double velocitycounts, double circumference, double gearRatio) { - double wheelRPM = falconToRPM(velocitycounts, gearRatio); + public static double falconToMPS(double velocityCounts, double circumference, double gearRatio) { + double wheelRPM = falconToRPM(velocityCounts, gearRatio); return (wheelRPM * circumference) / 60; } diff --git a/src/main/java/frc/lib/team3061/swerve/SwerveModule.java b/src/main/java/frc/lib/team3061/swerve/SwerveModule.java index bb54bcd9..7f233c9f 100644 --- a/src/main/java/frc/lib/team3061/swerve/SwerveModule.java +++ b/src/main/java/frc/lib/team3061/swerve/SwerveModule.java @@ -131,6 +131,15 @@ public int getModuleNumber() { return moduleNumber; } + /** + * Get the stator current of the drive motor of this swerve module. + * + * @return the stator current of the drive motor of this swerve module + */ + public double getDriveCurrent() { + return inputs.driveCurrentAmps[0]; + } + /** * Update this swerve module's inputs and log them. * diff --git a/src/main/java/frc/lib/team3061/swerve/SwerveModuleConstants.java b/src/main/java/frc/lib/team3061/swerve/SwerveModuleConstants.java index f23ac823..f0430b1d 100644 --- a/src/main/java/frc/lib/team3061/swerve/SwerveModuleConstants.java +++ b/src/main/java/frc/lib/team3061/swerve/SwerveModuleConstants.java @@ -20,7 +20,11 @@ public enum SwerveType { } /* MK4i L2 */ - public static final double MK4I_L2_WHEEL_DIAMETER_METERS = 0.10033; + + /* + Wheel diameter is best determined empirically. Refer to this document for more information: !!! + */ + public static final double MK4I_L2_WHEEL_DIAMETER_METERS = 0.096294; public static final double MK4I_L2_WHEEL_CIRCUMFERENCE = MK4I_L2_WHEEL_DIAMETER_METERS * Math.PI; public static final double MK4I_L2_DRIVE_GEAR_RATIO = 1 / ((14.0 / 50.0) * (27.0 / 17.0) * (15.0 / 45.0)); @@ -30,6 +34,10 @@ public enum SwerveType { public static final boolean MK4I_L2_CAN_CODER_INVERTED = false; /* MK4 L2 */ + + /* + Wheel diameter is best determined empirically. Refer to this document for more information: !!! + */ public static final double MK4_L2_WHEEL_DIAMETER_METERS = 0.10033; public static final double MK4_L2_WHEEL_CIRCUMFERENCE = MK4_L2_WHEEL_DIAMETER_METERS * Math.PI; public static final double MK4_L2_DRIVE_GEAR_RATIO = diff --git a/src/main/java/frc/lib/team3061/swerve/SwerveModuleIOTalonFX.java b/src/main/java/frc/lib/team3061/swerve/SwerveModuleIOTalonFX.java index 88d0a6e7..5996e32a 100644 --- a/src/main/java/frc/lib/team3061/swerve/SwerveModuleIOTalonFX.java +++ b/src/main/java/frc/lib/team3061/swerve/SwerveModuleIOTalonFX.java @@ -16,13 +16,11 @@ import edu.wpi.first.math.geometry.Rotation2d; import frc.lib.team254.drivers.TalonFXFactory; import frc.lib.team3061.RobotConfig; -import frc.lib.team3061.util.CANDeviceFinder; -import frc.lib.team3061.util.CANDeviceId.CANDeviceType; import frc.lib.team6328.util.TunableNumber; /** * Implementation of the SwerveModuleIO interface for MK4 Swerve Modules with two Falcon 500 motors - * and a CAN coder. + * and a CANcoder. */ public class SwerveModuleIOTalonFX implements SwerveModuleIO { @@ -92,11 +90,6 @@ public SwerveModuleIOTalonFX( RobotConfig.getInstance().getDriveKV() / 12, RobotConfig.getInstance().getDriveKA() / 12); - CANDeviceFinder can = new CANDeviceFinder(); - can.isDevicePresent(CANDeviceType.TALON, driveMotorID, "Mod " + moduleNumber + "Drive"); - can.isDevicePresent(CANDeviceType.TALON, angleMotorID, "Mod " + moduleNumber + "Angle"); - // check for the CANcoder on the CAN bus when supported by CANDeviceFinder - configAngleEncoder(canCoderID); configAngleMotor(angleMotorID); configDriveMotor(driveMotorID); diff --git a/src/main/java/frc/lib/team3061/util/CANDeviceFinder.java b/src/main/java/frc/lib/team3061/util/CANDeviceFinder.java deleted file mode 100644 index c58c0c97..00000000 --- a/src/main/java/frc/lib/team3061/util/CANDeviceFinder.java +++ /dev/null @@ -1,295 +0,0 @@ -/* - * Initially from https://github.com/FRC3620/FRC3620_2020_GalacticSenate - */ - -package frc.lib.team3061.util; - -import edu.wpi.first.hal.can.CANJNI; -import frc.lib.team3061.util.CANDeviceId.CANDeviceType; -import frc.lib.team6328.util.Alert; -import frc.lib.team6328.util.Alert.AlertType; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.*; - -public class CANDeviceFinder { - Set deviceSet = new TreeSet<>(); - - /* - * this is a map, keyed by CANDeviceType, whose values are sets contained - * the device numbers for all the present devices of that type. - */ - Map> byDeviceType = new TreeMap<>(); - - public CANDeviceFinder() { - super(); - find(); - } - - public boolean isDevicePresent(CANDeviceType deviceType, int id) { - return isDevicePresent(deviceType, id, null); - } - - public boolean isDevicePresent(CANDeviceType deviceType, int id, String whatItIs) { - boolean rv = false; - Set deviceTypeSet = byDeviceType.get(deviceType); - if (deviceTypeSet != null) { - rv = deviceTypeSet.contains(id); - } - if (!rv) { - if (whatItIs == null) { - Alert motor = - new Alert( - " " + deviceType + "(" + id + ")" + " is missing From the CAN bus", - AlertType.WARNING); - motor.set(true); - } else { - Alert motor = - new Alert( - " " + deviceType + "(" + id + ")" + whatItIs + " is missing From the CAN bus", - AlertType.WARNING); - motor.set(true); - } - } - return rv; - } - - /** - * @return ArrayList of strings holding the names of devices we've found. - */ - public Set getDeviceSet() { - return deviceSet; - } - - abstract class CanFinder { - int[] ids; - long[] ts0; - Set idsPresent = new TreeSet<>(); - - void pass1() { - ts0 = new long[ids.length]; - for (int i = 0; i < ids.length; ++i) { - ts0[i] = checkMessage(ids[i]); - } - } - - void pass2() { - long[] ts1 = new long[ids.length]; - for (int i = 0; i < ids.length; ++i) { - ts1[i] = checkMessage(ids[i]); - } - for (int i = 0; i < ids.length; ++i) { - if (ts0[i] >= 0 && ts1[i] >= 0 && ts0[i] != ts1[i]) { - idsPresent.add(ids[i]); - } - } - } - - private long checkMessage(int id) { - try { - targetID.clear(); - targetID.order(ByteOrder.LITTLE_ENDIAN); - targetID.asIntBuffer().put(0, id); - - timeStamp.clear(); - timeStamp.order(ByteOrder.LITTLE_ENDIAN); - timeStamp.asIntBuffer().put(0, 0x00000000); - - CANJNI.FRCNetCommCANSessionMuxReceiveMessage(targetID.asIntBuffer(), 0x1fffffff, timeStamp); - - long retval = timeStamp.getInt(); - retval &= 0xFFFFFFFF; /* undo sign-extension */ - return retval; - } catch (Exception e) { - return -1; - } - } - - abstract void report(); - } - - class DeviceFinder extends CanFinder { - Set deviceSet; - CANDeviceType canDeviceType; - - DeviceFinder( - int devType, - int mfg, - int apiId, - int maxDevices, - Set deviceSet, - CANDeviceType canDeviceType) { - super(); - - this.deviceSet = deviceSet; - this.canDeviceType = canDeviceType; - - ids = new int[maxDevices]; - for (int i = 0; i < maxDevices; i++) { - ids[i] = canBusId(devType, mfg, apiId, i); - } - } - - @Override - void report() { - for (int id : idsPresent) { - int deviceId = extractDeviceId(id); - deviceSet.add(new CANDeviceId(canDeviceType, deviceId)); // NOPMD - } - } - - private int extractDeviceId(int canId) { - return canId & 0x3f; - } - } - - class APIFinder extends CanFinder { - CANDeviceType canDeviceType; - - APIFinder(int devType, int mfg, int deviceId, CANDeviceType canDeviceType) { - super(); - this.canDeviceType = canDeviceType; - - ids = new int[1024]; - for (int i = 0; i < 1024; i++) { - ids[i] = canBusId(devType, mfg, i, deviceId); - } - } - - @Override - void report() { - for (int id : idsPresent) { - extractApiId(id); - } - } - } - - /** - * polls for received framing to determine if a device is deviceSet. This is meant to be used once - * initially (and not periodically) since this steals cached messages from the robot API. - */ - public void find() { - List finders = new ArrayList<>(); - - /* - * PDPs used to be 0x08041400. - * 2019.02.09: PDPs respond to APIs 0x50 0x51 0x52 0x59 0x5d - */ - finders.add(new DeviceFinder(8, 4, extractApiId(0x08041400), 1, deviceSet, CANDeviceType.PDP)); - - /* - * SRX used to be 0x02041400. - - As of 2019.02.08: (SRX @ devid 1) - 7 0x007 = 020401C1 - 81 0x051 = 02041441 ** using this? - 82 0x052 = 02041481 - 83 0x053 = 020414C1 - 87 0x057 = 020415C1 - 91 0x05B = 020416C1 - 92 0x05C = 02041701 - 93 0x05D = 02041741 - 94 0x05E = 02041781 - - 2020.01.20 Device id is 0x0204 (https://github.com/CrossTheRoadElec/Phoenix-api/blob/master/src/main/java/com/ctre/phoenix/motorcontrol/can/TalonSRX.java) - - Talon FX and SRX are the same. - */ - finders.add( - new DeviceFinder(2, 4, extractApiId(0x02041441), 64, deviceSet, CANDeviceType.TALON)); - - /* - SPX used to be 0x01041400. - - As of 2019.02.08: (SPX @ devid 2) - 7 0x007 = 010401C2 - 81 0x051 = 01041442 ** using this - 83 0x053 = 010414C2 - 91 0x05B = 010416C2 - 92 0x05C = 01041702 - 93 0x05D = 01041742 - 94 0x05E = 01041782 - - 2020.01.20 Device id is 0x0104 (https://github.com/CrossTheRoadElec/Phoenix-api/blob/master/src/main/java/com/ctre/phoenix/motorcontrol/can/VictorSPX.java) - */ - finders.add( - new DeviceFinder(1, 4, extractApiId(0x01041442), 64, deviceSet, CANDeviceType.VICTOR_SPX)); - - /* we always used 0x09041400 for PCMs */ - finders.add(new DeviceFinder(9, 4, extractApiId(0x09041400), 64, deviceSet, CANDeviceType.PCM)); - - // per REV (0x02051800) - finders.add( - new DeviceFinder(2, 5, extractApiId(0x02051800), 64, deviceSet, CANDeviceType.SPARK_MAX)); - - findDetails(finders); - } - - public void research() { - List finders = new ArrayList<>(); - - finders.add(new APIFinder(9, 4, 0, CANDeviceType.PCM)); // PCM - finders.add(new APIFinder(8, 4, 0, CANDeviceType.PDP)); // PDP - finders.add(new APIFinder(2, 4, 1, CANDeviceType.TALON)); // SRX #1 - finders.add(new APIFinder(1, 4, 2, CANDeviceType.VICTOR_SPX)); // SPX #2 - - findDetails(finders); - } - - void findDetails(List finders) { - deviceSet.clear(); - byDeviceType.clear(); - - for (CanFinder finder : finders) { - finder.pass1(); - } - - /* wait 200ms */ - try { - Thread.sleep(200); - } catch (InterruptedException e) { - e.printStackTrace(); // NOPMD - Thread.currentThread().interrupt(); - } - - for (CanFinder finder : finders) { - finder.pass2(); - } - - for (CanFinder finder : finders) { - finder.report(); - } - - /* - fill in the byDeviceType map. - */ - for (CANDeviceId canDeviceId : deviceSet) { - CANDeviceType canDeviceType = canDeviceId.getDeviceType(); - - Set deviceNumberSet = - byDeviceType.computeIfAbsent(canDeviceType, k -> new TreeSet<>()); - deviceNumberSet.add(canDeviceId.getDeviceNumber()); - } - } - - /* help to calculate the CAN bus ID for a devType|mfg|api|dev. - total of 32 bits: 8 bit devType, 8 bit mfg, 10 bit API, 6 bit device id. - */ - boolean logCanBusIds = false; - - private int canBusId(int devType, int mfg, int apiId, int devId) { - return ((devType & 0xff) << 24) - | ((mfg & 0xff) << 16) - | ((apiId & 0x3ff) << 6) - | (devId & 0x3f); - } - - private int extractApiId(int canId) { - return (canId & 0xffc0) >> 6; - } - - /** helper routine to get last received message for a given ID */ - private ByteBuffer targetID = ByteBuffer.allocateDirect(4); - - private ByteBuffer timeStamp = ByteBuffer.allocateDirect(4); -} diff --git a/src/main/java/frc/lib/team3061/util/CANDeviceId.java b/src/main/java/frc/lib/team3061/util/CANDeviceId.java deleted file mode 100644 index 85eea37c..00000000 --- a/src/main/java/frc/lib/team3061/util/CANDeviceId.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Initially from https://github.com/FRC3620/FRC3620_2020_GalacticSenate - */ - -package frc.lib.team3061.util; - -/** - * An object representing a CAN device on the bus. These can be put in Collections, are Hashable, - * and are Comparable. - */ -public class CANDeviceId implements Comparable { - - public enum CANDeviceType { - PDP, - PCM, - TALON, - VICTOR_SPX, - SPARK_MAX - } - - private CANDeviceType deviceType; - private int deviceNumber; - - public CANDeviceType getDeviceType() { - return deviceType; - } - - public int getDeviceNumber() { - return deviceNumber; - } - - private String toString; - - public CANDeviceId(CANDeviceType deviceType, int deviceNumber) { - this.deviceType = deviceType; - this.deviceNumber = deviceNumber; - this.toString = deviceType.toString() + " " + Integer.toString(deviceNumber); - } - - @Override - public boolean equals(Object object) { - if (this == object) return true; - if (object == null || getClass() != object.getClass()) return false; - if (!super.equals(object)) return false; - - CANDeviceId that = (CANDeviceId) object; - - if (deviceNumber != that.deviceNumber) return false; - return deviceType == that.deviceType; - } - - @Override - public int hashCode() { - int result = super.hashCode(); - result = 31 * result + deviceType.hashCode(); - result = 31 * result + deviceNumber; - return result; - } - - @Override - public java.lang.String toString() { - return this.toString; - } - - @Override - public int compareTo(CANDeviceId canDeviceId) { - int rv = Integer.compare(this.deviceType.ordinal(), canDeviceId.deviceType.ordinal()); - if (rv == 0) rv = Integer.compare(this.deviceNumber, canDeviceId.deviceNumber); - return rv; - } -} diff --git a/src/main/java/frc/lib/team3061/vision/Vision.java b/src/main/java/frc/lib/team3061/vision/Vision.java index 0122241b..57ea7586 100644 --- a/src/main/java/frc/lib/team3061/vision/Vision.java +++ b/src/main/java/frc/lib/team3061/vision/Vision.java @@ -5,43 +5,97 @@ import edu.wpi.first.apriltag.AprilTag; import edu.wpi.first.apriltag.AprilTagFieldLayout; import edu.wpi.first.apriltag.AprilTagFieldLayout.OriginPosition; +import edu.wpi.first.math.Matrix; +import edu.wpi.first.math.VecBuilder; import edu.wpi.first.math.estimator.SwerveDrivePoseEstimator; +import edu.wpi.first.math.geometry.Pose2d; import edu.wpi.first.math.geometry.Pose3d; -import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.math.geometry.Transform3d; +import edu.wpi.first.math.numbers.N1; +import edu.wpi.first.math.numbers.N3; import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.first.wpilibj.shuffleboard.Shuffleboard; +import edu.wpi.first.wpilibj.shuffleboard.ShuffleboardTab; import edu.wpi.first.wpilibj2.command.SubsystemBase; import frc.lib.team3061.RobotConfig; import frc.lib.team3061.util.RobotOdometry; import frc.lib.team3061.vision.VisionIO.VisionIOInputs; import frc.lib.team6328.util.Alert; import frc.lib.team6328.util.Alert.AlertType; +import frc.lib.team6328.util.TunableNumber; import java.io.IOException; import java.util.ArrayList; import java.util.Optional; import org.littletonrobotics.junction.Logger; -import org.photonvision.targeting.PhotonPipelineResult; import org.photonvision.targeting.PhotonTrackedTarget; +/** + * The Vision subsystem is responsible for updating the robot's estimated pose based on a collection + * of cameras capturing AprilTags. The Vision subsystem is comprised of multiple VisionIO objects, + * each of which is responsible for producing a single PhotonPipelineResult. There is a one-to-one + * relationship between each VisionIO object and each co-processor (e.g., Raspberry Pi) running + * PhotonVision. + */ public class Vision extends SubsystemBase { - private VisionIO visionIO; - private final VisionIOInputs io = new VisionIOInputs(); - private AprilTagFieldLayout layout; - private DriverStation.Alliance lastAlliance = DriverStation.Alliance.Invalid; - - private double lastTimestamp; - private SwerveDrivePoseEstimator poseEstimator; - private boolean isEnabled = true; + private VisionIO[] visionIOs; + private Transform3d[] camerasToRobots; + private final VisionIOInputs[] ios; + private double[] lastTimestamps; + private AprilTagFieldLayout layout; private Alert noAprilTagLayoutAlert = new Alert( "No AprilTag layout file found. Update APRILTAG_FIELD_LAYOUT_PATH in VisionConstants.java", AlertType.WARNING); - public Vision(VisionIO visionIO) { - this.visionIO = visionIO; + private boolean isEnabled = true; + private boolean isVisionUpdating = false; + + private SwerveDrivePoseEstimator poseEstimator; + private final TunableNumber poseDifferenceThreshold = + new TunableNumber("Vision/VisionPoseThreshold", POSE_DIFFERENCE_THRESHOLD_METERS); + private final TunableNumber stdDevSlope = new TunableNumber("Vision/stdDevSlope", 0.10); + private final TunableNumber stdDevPower = new TunableNumber("Vision/stdDevPower", 2.0); + + private static class RobotPoseFromAprilTag { + public final Pose3d robotPose; + public final double distanceToAprilTag; + + public RobotPoseFromAprilTag(Pose3d robotPose, double distance) { + this.robotPose = robotPose; + this.distanceToAprilTag = distance; + } + } + + /** + * Create a new Vision subsystem. The number of VisionIO objects passed to the constructor must + * match the number of robot-to-camera transforms returned by the RobotConfig singleton. + * + * @param visionIO One or more VisionIO objects, each of which is responsible for producing a + * single single PhotonPipelineResult. There is a one-to-one relationship between each + * VisionIO object and each co-processor (e.g., Raspberry Pi) running PhotonVision. + */ + public Vision(VisionIO[] visionIOs) { + this.visionIOs = visionIOs; + this.camerasToRobots = RobotConfig.getInstance().getRobotToCameraTransforms(); + this.lastTimestamps = new double[visionIOs.length]; + this.ios = new VisionIOInputs[visionIOs.length]; + for (int i = 0; i < visionIOs.length; i++) { + this.ios[i] = new VisionIOInputs(); + } + + // retrieve a reference to the pose estimator singleton this.poseEstimator = RobotOdometry.getInstance().getPoseEstimator(); + // add an indicator to the main Shuffleboard tab to indicate whether vision is updating in order + // to alert the drive team if it is not. + ShuffleboardTab tabMain = Shuffleboard.getTab("MAIN"); + tabMain + .addBoolean("isVisionUpdating", () -> isVisionUpdating) + .withPosition(7, 2) + .withSize(1, 2); + + // load and log all of the AprilTags in the field layout file try { layout = new AprilTagFieldLayout(VisionConstants.APRILTAG_FIELD_LAYOUT_PATH); noAprilTagLayoutAlert.set(false); @@ -55,118 +109,204 @@ public Vision(VisionIO visionIO) { } } - public double getLatestTimestamp() { - return io.lastTimestamp; - } + /** + * This method should be invoked once the alliance color is known. Refer to the RobotContainer's + * checkAllianceColor method for best practices on when to check the alliance's color. It updates + * the AprilTag field layout, logs the new location of the tags, and updates all of the VisionIO + * objects with the new alliance color. + * + * @param newAlliance the new alliance color + */ + public void updateAlliance(DriverStation.Alliance newAlliance) { - public PhotonPipelineResult getLatestResult() { - return io.lastResult; + if (newAlliance == DriverStation.Alliance.Red) { + layout.setOrigin(OriginPosition.kRedAllianceWallRightSide); + for (VisionIO visionIO : visionIOs) { + visionIO.setLayoutOrigin(OriginPosition.kRedAllianceWallRightSide); + } + } else { + layout.setOrigin(OriginPosition.kBlueAllianceWallRightSide); + for (VisionIO visionIO : visionIOs) { + visionIO.setLayoutOrigin(OriginPosition.kBlueAllianceWallRightSide); + } + } + + for (AprilTag tag : layout.getTags()) { + layout + .getTagPose(tag.ID) + .ifPresent(pose -> Logger.getInstance().recordOutput("Vision/AprilTags/" + tag.ID, pose)); + } } + /** + * This method is invoked each iteration of the scheduler. It updates the inputs for each of the + * VisionIO objects and, for each, updates the pose estimator based on the most recent detected + * AprilTags. + */ @Override public void periodic() { + isVisionUpdating = false; + for (int i = 0; i < visionIOs.length; i++) { + visionIOs[i].updateInputs(ios[i]); + Logger.getInstance().processInputs("Vision" + i, ios[i]); - visionIO.updateInputs(io); - Logger.getInstance().processInputs("Vision", io); + // only process the vision data if the timestamp is newer than the last one + if (lastTimestamps[i] < ios[i].lastTimestamp) { + lastTimestamps[i] = ios[i].lastTimestamp; + RobotPoseFromAprilTag poseAndDistance = getRobotPose(i); + Pose3d robotPose = poseAndDistance.robotPose; - if (DriverStation.getAlliance() != lastAlliance) { - lastAlliance = DriverStation.getAlliance(); - if (DriverStation.getAlliance() == DriverStation.Alliance.Red) { - layout.setOrigin(OriginPosition.kRedAllianceWallRightSide); - } else { - layout.setOrigin(OriginPosition.kBlueAllianceWallRightSide); - } - } + if (robotPose == null) return; - if (lastTimestamp < getLatestTimestamp()) { - lastTimestamp = getLatestTimestamp(); - for (PhotonTrackedTarget target : getLatestResult().getTargets()) { - if (isValidTarget(target)) { - Transform3d cameraToTarget = target.getBestCameraToTarget(); - Optional tagPoseOptional = layout.getTagPose(target.getFiducialId()); - if (tagPoseOptional.isPresent()) { - Pose3d tagPose = tagPoseOptional.get(); - Pose3d cameraPose = tagPose.transformBy(cameraToTarget.inverse()); - Pose3d robotPose = - cameraPose.transformBy( - RobotConfig.getInstance().getRobotToCameraTransform().inverse()); - - if (poseEstimator - .getEstimatedPosition() - .minus(robotPose.toPose2d()) - .getTranslation() - .getNorm() - < MAX_POSE_DIFFERENCE_METERS) { - if (isEnabled) { - poseEstimator.addVisionMeasurement(robotPose.toPose2d(), getLatestTimestamp()); - } - - Logger.getInstance().recordOutput("Vision/TagPose", tagPose); - Logger.getInstance().recordOutput("Vision/CameraPose", cameraPose); - Logger.getInstance().recordOutput("Vision/RobotPose", robotPose.toPose2d()); - Logger.getInstance().recordOutput("Vision/isEnabled", isEnabled); - } + // only update the pose estimator if the pose from the vision data is close to the estimated + // robot pose + if (poseEstimator + .getEstimatedPosition() + .minus(robotPose.toPose2d()) + .getTranslation() + .getNorm() + < MAX_POSE_DIFFERENCE_METERS) { + + // only update the pose estimator if the vision subsystem is enabled + if (isEnabled) { + // when updating the pose estimator, specify standard deviations based on the distance + // from the robot to the AprilTag (the greater the distance, the less confident we are + // in the measurement) + poseEstimator.addVisionMeasurement( + robotPose.toPose2d(), + ios[i].lastTimestamp, + getStandardDeviations(poseAndDistance.distanceToAprilTag)); + isVisionUpdating = true; } + + Logger.getInstance().recordOutput("Vision/RobotPose" + i, robotPose.toPose2d()); + Logger.getInstance().recordOutput("Vision/IsEnabled", isEnabled); } } } } + /** + * Returns true if the vision subsystem is enabled. + * + * @return true if the vision subsystem is enabled + */ public boolean isEnabled() { return isEnabled; } - public boolean tagVisible(int id) { - PhotonPipelineResult result = getLatestResult(); - for (PhotonTrackedTarget target : result.getTargets()) { - if (target.getFiducialId() == id && isValidTarget(target)) { - return true; + public Pose3d getBestRobotPose() { + Pose3d robotPoseFromClosestTarget = null; + double closestTargetDistance = Double.MAX_VALUE; + for (int i = 0; i < visionIOs.length; i++) { + RobotPoseFromAprilTag poseAndDistance = getRobotPose(i); + Pose3d robotPose = poseAndDistance.robotPose; + double distanceToAprilTag = poseAndDistance.distanceToAprilTag; + if (robotPose != null && distanceToAprilTag < closestTargetDistance) { + robotPoseFromClosestTarget = robotPose; + closestTargetDistance = distanceToAprilTag; } } - return false; + return robotPoseFromClosestTarget; } /** - * returns the best Rotation3d from the robot to the given target. + * Enable or disable the vision subsystem. * - * @param id - * @return the Transform3d or null if there isn't + * @param enable enables the vision subsystem if true; disables if false */ - public Transform3d getTransform3dToTag(int id) { - PhotonPipelineResult result = getLatestResult(); - for (PhotonTrackedTarget target : result.getTargets()) { - if (target.getFiducialId() == id && isValidTarget(target)) { - return RobotConfig.getInstance() - .getRobotToCameraTransform() - .plus(target.getBestCameraToTarget()); + public void enable(boolean enable) { + isEnabled = enable; + } + + /** + * Returns true if the robot's pose based on vision data is within the specified threshold of the + * robot's pose based on the pose estimator. This method can be used to trigger a transition from + * driver control to automated control once confident that the estimated pose is accurate. + * + * @return true if the robot's pose based on vision data is within the specified threshold of the + * robot's pose based on the pose estimator + */ + public boolean posesHaveConverged() { + for (int i = 0; i < visionIOs.length; i++) { + Pose3d robotPose = getRobotPose(i).robotPose; + if (robotPose != null + && poseEstimator + .getEstimatedPosition() + .minus(robotPose.toPose2d()) + .getTranslation() + .getNorm() + < poseDifferenceThreshold.get()) { + Logger.getInstance().recordOutput("Vision/posesInLine", true); + return true; } } - return null; + Logger.getInstance().recordOutput("Vision/posesInLine", false); + return false; } - public Rotation2d getAngleToTag(int id) { - Transform3d transform = getTransform3dToTag(id); - if (transform != null) { - return new Rotation2d(transform.getTranslation().getX(), transform.getTranslation().getY()); - } else { - return null; - } + private Matrix getStandardDeviations(double targetDistance) { + // the standard deviation of the vision measurement is a function of the distance from the robot + // to the AprilTag and can be tuned + double stdDevTrust = stdDevSlope.get() * (Math.pow(targetDistance, stdDevPower.get())); + return VecBuilder.fill(stdDevTrust, stdDevTrust, stdDevTrust); } - public double getDistanceToTag(int id) { - Transform3d transform = getTransform3dToTag(id); - if (transform != null) { - return transform.getTranslation().toTranslation2d().getNorm(); - } else { - return -1; + /** + * Returns the robot pose based on vision data and distance to the AprilTag that is closest to the + * robot. The current algorithm simply uses the AprilTag that is closest to the robot from which + * to determine the robot's pose. In the future, this method could be updated to use multiple + * tags. + * + * @param index the index of the VisionIO object to use + * @return the robot pose based on vision data and distance to the AprilTag that is closest to the + * robot + */ + private RobotPoseFromAprilTag getRobotPose(int index) { + int targetCount = 0; + Pose3d robotPoseFromClosestTarget = null; + double closestTargetDistance = Double.MAX_VALUE; + + // "zero" the tag and robot poses such that old data is not used if no new data is available; in + // terms of logging, we are assuming that a given VisionIO object won't see more than 2 tags at + // once + for (int i = 0; i < 2; i++) { + Logger.getInstance().recordOutput("Vision/TagPose" + index + "_" + i, new Pose2d()); + Logger.getInstance().recordOutput("Vision/NVRobotPose" + index + "_" + i, new Pose2d()); } - } - public void enable(boolean enable) { - isEnabled = enable; + for (PhotonTrackedTarget target : ios[index].lastResult.getTargets()) { + if (isValidTarget(target)) { + Transform3d cameraToTarget = target.getBestCameraToTarget(); + Optional tagPoseOptional = layout.getTagPose(target.getFiducialId()); + if (tagPoseOptional.isPresent()) { + Pose3d tagPose = tagPoseOptional.get(); + Pose3d cameraPose = tagPose.transformBy(cameraToTarget.inverse()); + Pose3d robotPose = cameraPose.transformBy(camerasToRobots[index].inverse()); + + Logger.getInstance() + .recordOutput("Vision/TagPose" + index + "_" + targetCount, tagPose.toPose2d()); + Logger.getInstance() + .recordOutput("Vision/NVRobotPose" + index + "_" + targetCount, robotPose.toPose2d()); + + double targetDistance = + target.getBestCameraToTarget().getTranslation().toTranslation2d().getNorm(); + + // only consider tags that are within a certain distance of the robot + if (targetDistance < VisionConstants.MAX_DISTANCE_TO_TARGET_METERS + && targetDistance < closestTargetDistance) { + closestTargetDistance = targetDistance; + robotPoseFromClosestTarget = robotPose; + } + } + } + targetCount++; + } + + return new RobotPoseFromAprilTag(robotPoseFromClosestTarget, closestTargetDistance); } - public boolean isValidTarget(PhotonTrackedTarget target) { + private boolean isValidTarget(PhotonTrackedTarget target) { return target.getFiducialId() != -1 && target.getPoseAmbiguity() != -1 && target.getPoseAmbiguity() < VisionConstants.MAXIMUM_AMBIGUITY diff --git a/src/main/java/frc/lib/team3061/vision/VisionConstants.java b/src/main/java/frc/lib/team3061/vision/VisionConstants.java index 38ed53b3..a70b6038 100644 --- a/src/main/java/frc/lib/team3061/vision/VisionConstants.java +++ b/src/main/java/frc/lib/team3061/vision/VisionConstants.java @@ -11,11 +11,22 @@ private VisionConstants() { throw new IllegalStateException(CONSTRUCTOR_EXCEPTION); } - // FIXME: add the current year's AprilTag field layout file to the deploy directory and update - // this constant with the current year's AprilTag field layout file public static final Path APRILTAG_FIELD_LAYOUT_PATH = new File(Filesystem.getDeployDirectory(), "2023-chargedup.json").toPath(); + // the pose ambiguity must be less than this value for the target to be considered valid public static final double MAXIMUM_AMBIGUITY = 0.2; - public static final double MAX_POSE_DIFFERENCE_METERS = 1.0; + + // the maximum distance between the robot's pose derived from the target and the current robot's + // estimated pose for the target to be used to update the robot's pose (essentially, always use a + // valid target to update the robot's pose) + public static final double MAX_POSE_DIFFERENCE_METERS = 1000.0; + + // the maximum distance between the robot and the target, for the target to be used to update the + // robot's pose + public static final double MAX_DISTANCE_TO_TARGET_METERS = 6.0; + + // the maximum distance between the robot's current estimated pose and the robot's pose derived + // from the target to consider the two poses as having converged. + public static final double POSE_DIFFERENCE_THRESHOLD_METERS = 0.5; } diff --git a/src/main/java/frc/lib/team3061/vision/VisionIO.java b/src/main/java/frc/lib/team3061/vision/VisionIO.java index 20ba4254..508abb3e 100644 --- a/src/main/java/frc/lib/team3061/vision/VisionIO.java +++ b/src/main/java/frc/lib/team3061/vision/VisionIO.java @@ -1,5 +1,6 @@ package frc.lib.team3061.vision; +import edu.wpi.first.apriltag.AprilTagFieldLayout.OriginPosition; import java.util.ArrayList; import java.util.List; import org.littletonrobotics.junction.LogTable; @@ -8,19 +9,26 @@ import org.photonvision.targeting.PhotonPipelineResult; import org.photonvision.targeting.PhotonTrackedTarget; +/** + * The hardware abstraction interface for a PhotonVision-based co-processor that provides + * PhotonPipelineResult objects. There is a one-to-one relationship between each VisionIO object and + * each co-processor (e.g., Raspberry Pi) running PhotonVision. + * + *

In the future, this interface may be further abstracted to not be coupled to PhotonVision. + * Currently, the abstraction is used to simulate vision. + */ public interface VisionIO { public static class VisionIOInputs implements LoggableInputs { PhotonPipelineResult lastResult = new PhotonPipelineResult(0, new ArrayList<>()); double lastTimestamp = 0.0; - boolean hasNewResult = false; public void toLog(LogTable table) { + // since AdvantageKit doesn't support the PhotonPipelineResult type, log it as a byte array byte[] photonPacketBytes = new byte[lastResult.getPacketSize()]; lastResult.populatePacket(new Packet(photonPacketBytes)); table.put("photonPacketBytes", photonPacketBytes); table.put("lastTimestamp", lastTimestamp); - table.put("hasNewResult", hasNewResult); // log targets in a human-readable way List targets = lastResult.getTargets(); @@ -38,10 +46,20 @@ public void fromLog(LogTable table) { lastResult.createFromPacket(new Packet(photonPacketBytes)); lastTimestamp = table.getDouble("lastTimestamp", 0.0); - hasNewResult = table.getBoolean("hasNewResult", false); } } - /** Updates the set of loggable inputs. */ + /** + * Updates the set of loggable inputs. + * + * @param inputs the inputs to update + */ public default void updateInputs(VisionIOInputs inputs) {} + + /** + * Sets the origin of the AprilTag field layout. This is invoked once the alliance color is known. + * + * @param origin the origin of the AprilTag field layout + */ + public default void setLayoutOrigin(OriginPosition origin) {} } diff --git a/src/main/java/frc/lib/team3061/vision/VisionIOPhotonVision.java b/src/main/java/frc/lib/team3061/vision/VisionIOPhotonVision.java index c3d8ded2..ceb15e70 100644 --- a/src/main/java/frc/lib/team3061/vision/VisionIOPhotonVision.java +++ b/src/main/java/frc/lib/team3061/vision/VisionIOPhotonVision.java @@ -3,13 +3,13 @@ import edu.wpi.first.networktables.DoubleArraySubscriber; import edu.wpi.first.networktables.NetworkTableEvent; import edu.wpi.first.networktables.NetworkTableInstance; -import edu.wpi.first.wpilibj.Timer; import frc.lib.team6328.util.Alert; import frc.lib.team6328.util.Alert.AlertType; import java.util.EnumSet; import org.photonvision.PhotonCamera; import org.photonvision.targeting.PhotonPipelineResult; +/** PhotonVision-based implementation of the VisionIO interface. */ public class VisionIOPhotonVision implements VisionIO { private Alert noCameraConnectedAlert = new Alert("specified camera not connected", AlertType.WARNING); @@ -18,6 +18,11 @@ public class VisionIOPhotonVision implements VisionIO { private double lastTimestamp = 0; private PhotonPipelineResult lastResult = new PhotonPipelineResult(); + /** + * Creates a new VisionIOPhotonVision object. + * + * @param cameraName the name of the PhotonVision camera to use; the name must be unique + */ public VisionIOPhotonVision(String cameraName) { camera = new PhotonCamera(cameraName); NetworkTableInstance inst = NetworkTableInstance.getDefault(); @@ -36,7 +41,8 @@ public VisionIOPhotonVision(String cameraName) { EnumSet.of(NetworkTableEvent.Kind.kValueAll), event -> { PhotonPipelineResult result = camera.getLatestResult(); - double timestamp = Timer.getFPGATimestamp() - (result.getLatencyMillis() / 1000.0); + + double timestamp = result.getTimestampSeconds(); synchronized (VisionIOPhotonVision.this) { lastTimestamp = timestamp; lastResult = result; @@ -44,6 +50,11 @@ public VisionIOPhotonVision(String cameraName) { }); } + /** + * Updates the specified VisionIOInputs object with the latest data from the camera. + * + * @param inputs the VisionIOInputs object to update with the latest data from the camera + */ @Override public synchronized void updateInputs(VisionIOInputs inputs) { inputs.lastTimestamp = this.lastTimestamp; diff --git a/src/main/java/frc/lib/team3061/vision/VisionIOSim.java b/src/main/java/frc/lib/team3061/vision/VisionIOSim.java index 5f42fea6..c2d96c06 100644 --- a/src/main/java/frc/lib/team3061/vision/VisionIOSim.java +++ b/src/main/java/frc/lib/team3061/vision/VisionIOSim.java @@ -9,7 +9,6 @@ import edu.wpi.first.networktables.DoubleArraySubscriber; import edu.wpi.first.networktables.NetworkTableEvent; import edu.wpi.first.networktables.NetworkTableInstance; -import edu.wpi.first.wpilibj.Timer; import java.util.EnumSet; import java.util.function.Supplier; import org.photonvision.PhotonCamera; @@ -17,9 +16,15 @@ import org.photonvision.SimVisionTarget; import org.photonvision.targeting.PhotonPipelineResult; +/** + * PhotonVision-compatible simulated implementation of the VisionIO interface. Only a single + * VisionIOSim object may be instantiated. It uses the PhotonVision SimVisionSystem to simulates the + * AprilTag targets that would be seen by a camera based on the robot's pose, which is determined + * based on its odometry. + */ public class VisionIOSim implements VisionIO { private static final String CAMERA_NAME = "simCamera"; - private static final double DIAGONAL_FOV = 70; // FOV in degrees + private static final double DIAGONAL_FOV = 160; // FOV in degrees private static final int IMG_WIDTH = 1280; // image width in px private static final int IMG_HEIGHT = 720; // image heigh in px private final PhotonCamera camera = new PhotonCamera(CAMERA_NAME); @@ -29,15 +34,25 @@ public class VisionIOSim implements VisionIO { private Supplier poseSupplier; private SimVisionSystem simVision; + private AprilTagFieldLayout layout; + /** + * Creates a new VisionIOSim object. + * + * @param layout the AprilTag field layout + * @param poseSupplier a Pose2d supplier that returns the robot's pose based on its odometry + * @param robotToCamera the transform from the robot's center to the simulated camera + */ public VisionIOSim( AprilTagFieldLayout layout, Supplier poseSupplier, Transform3d robotToCamera) { + this.layout = layout; this.poseSupplier = poseSupplier; this.simVision = new SimVisionSystem( - CAMERA_NAME, DIAGONAL_FOV, robotToCamera.inverse(), 9000, IMG_WIDTH, IMG_HEIGHT, 0); + CAMERA_NAME, DIAGONAL_FOV, robotToCamera, 9000, IMG_WIDTH, IMG_HEIGHT, 0); + // default to the blue alliance; can be changed by invoking the setLayoutOrigin method layout.setOrigin(OriginPosition.kBlueAllianceWallRightSide); for (AprilTag tag : layout.getTags()) { @@ -61,7 +76,7 @@ public VisionIOSim( EnumSet.of(NetworkTableEvent.Kind.kValueAll), event -> { PhotonPipelineResult result = camera.getLatestResult(); - double timestamp = Timer.getFPGATimestamp() - (result.getLatencyMillis() / 1000.0); + double timestamp = result.getTimestampSeconds(); synchronized (VisionIOSim.this) { lastTimestamp = timestamp; lastResult = result; @@ -69,10 +84,37 @@ public VisionIOSim( }); } + /** + * Updates the specified VisionIOInputs object with the latest data from the camera. + * + * @param inputs the VisionIOInputs object to update with the latest data from the camera + */ @Override public synchronized void updateInputs(VisionIOInputs inputs) { this.simVision.processFrame(poseSupplier.get()); inputs.lastTimestamp = this.lastTimestamp; inputs.lastResult = this.lastResult; } + + /** + * Sets the origin position of the AprilTag field layout and updates the simulated vision targets + * based on the new origin. + * + * @param origin the origin position of the AprilTag field layout + */ + @Override + public void setLayoutOrigin(OriginPosition origin) { + layout.setOrigin(origin); + + this.simVision.clearVisionTargets(); + for (AprilTag tag : layout.getTags()) { + layout + .getTagPose(tag.ID) + .ifPresent( + pose -> + this.simVision.addSimVisionTarget( + new SimVisionTarget( + pose, Units.inchesToMeters(6), Units.inchesToMeters(6), tag.ID))); + } + } } diff --git a/src/main/java/frc/lib/team6328/util/FieldConstants.java b/src/main/java/frc/lib/team6328/util/FieldConstants.java new file mode 100644 index 00000000..d322c484 --- /dev/null +++ b/src/main/java/frc/lib/team6328/util/FieldConstants.java @@ -0,0 +1,291 @@ +// Copyright (c) 2023 FRC 6328 +// http://github.com/Mechanical-Advantage +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file at +// the root directory of this project. + +package frc.lib.team6328.util; + +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.first.wpilibj.DriverStation.Alliance; +import java.util.Map; + +@java.lang.SuppressWarnings({"java:S1118", "java:S115", "java:S2386"}) + +/** + * Contains various field dimensions and useful reference points. Dimensions are in meters, and sets + * of corners start in the lower left moving clockwise. + * + *

All translations and poses are stored with the origin at the rightmost point on the BLUE + * ALLIANCE wall. Use the {@link #allianceFlip(Translation2d)} and {@link #allianceFlip(Pose2d)} + * methods to flip these values based on the current alliance color. + */ +public final class FieldConstants { + public static final double fieldLength = Units.inchesToMeters(651.25); + public static final double fieldWidth = Units.inchesToMeters(315.5); + public static final double tapeWidth = Units.inchesToMeters(2.0); + + // Dimensions for community and charging station, including the tape. + public static final class Community { + // Region dimensions + public static final double innerX = 0.0; + public static final double midX = + Units.inchesToMeters(132.375); // Tape to the left of charging station + public static final double outerX = + Units.inchesToMeters(193.25); // Tape to the right of charging station + public static final double leftY = Units.feetToMeters(18.0); + public static final double midY = leftY - Units.inchesToMeters(59.39) + tapeWidth; + public static final double rightY = 0.0; + public static final Translation2d[] regionCorners = + new Translation2d[] { + new Translation2d(innerX, rightY), + new Translation2d(innerX, leftY), + new Translation2d(midX, leftY), + new Translation2d(midX, midY), + new Translation2d(outerX, midY), + new Translation2d(outerX, rightY), + }; + + // Charging station dimensions + public static final double chargingStationLength = Units.inchesToMeters(76.125); + public static final double chargingStationWidth = Units.inchesToMeters(97.25); + public static final double chargingStationOuterX = outerX - tapeWidth; + public static final double chargingStationInnerX = + chargingStationOuterX - chargingStationLength; + public static final double chargingStationLeftY = midY - tapeWidth; + public static final double chargingStationRightY = chargingStationLeftY - chargingStationWidth; + public static final Translation2d[] chargingStationCorners = + new Translation2d[] { + new Translation2d(chargingStationInnerX, chargingStationRightY), + new Translation2d(chargingStationInnerX, chargingStationLeftY), + new Translation2d(chargingStationOuterX, chargingStationRightY), + new Translation2d(chargingStationOuterX, chargingStationLeftY) + }; + + // Cable bump + public static final double cableBumpInnerX = + innerX + Grids.outerX + Units.inchesToMeters(95.25); + public static final double cableBumpOuterX = cableBumpInnerX + Units.inchesToMeters(7); + public static final Translation2d[] cableBumpCorners = + new Translation2d[] { + new Translation2d(cableBumpInnerX, 0.0), + new Translation2d(cableBumpInnerX, chargingStationRightY), + new Translation2d(cableBumpOuterX, 0.0), + new Translation2d(cableBumpOuterX, chargingStationRightY) + }; + } + + // Dimensions for grids and nodes + public static final class Grids { + // X layout + public static final double outerX = Units.inchesToMeters(54.25); + public static final double lowX = + outerX - (Units.inchesToMeters(14.25) / 2.0); // Centered when under cube nodes + public static final double midX = outerX - Units.inchesToMeters(22.75); + public static final double highX = outerX - Units.inchesToMeters(39.75); + + // Y layout + public static final int nodeRowCount = 9; + public static final double nodeFirstY = Units.inchesToMeters(20.19); + public static final double nodeSeparationY = Units.inchesToMeters(22.0); + + // Z layout + public static final double cubeEdgeHigh = Units.inchesToMeters(3.0); + public static final double highCubeZ = Units.inchesToMeters(35.5) - cubeEdgeHigh; + public static final double midCubeZ = Units.inchesToMeters(23.5) - cubeEdgeHigh; + public static final double highConeZ = Units.inchesToMeters(46.0); + public static final double midConeZ = Units.inchesToMeters(34.0); + + // Translations (all nodes in the same column/row have the same X/Y coordinate) + public static final Translation2d[] lowTranslations = new Translation2d[nodeRowCount]; + public static final Translation2d[] midTranslations = new Translation2d[nodeRowCount]; + public static final Translation3d[] mid3dTranslations = new Translation3d[nodeRowCount]; + public static final Translation2d[] highTranslations = new Translation2d[nodeRowCount]; + public static final Translation3d[] high3dTranslations = new Translation3d[nodeRowCount]; + + static { + for (int i = 0; i < nodeRowCount; i++) { + boolean isCube = i == 1 || i == 4 || i == 7; + lowTranslations[i] = new Translation2d(lowX, nodeFirstY + nodeSeparationY * i); + midTranslations[i] = new Translation2d(midX, nodeFirstY + nodeSeparationY * i); + mid3dTranslations[i] = + new Translation3d(midX, nodeFirstY + nodeSeparationY * i, isCube ? midCubeZ : midConeZ); + high3dTranslations[i] = + new Translation3d( + highX, nodeFirstY + nodeSeparationY * i, isCube ? highCubeZ : highConeZ); + highTranslations[i] = new Translation2d(highX, nodeFirstY + nodeSeparationY * i); + } + } + + // Complex low layout (shifted to account for cube vs cone rows and wide edge nodes) + public static final double complexLowXCones = + outerX - Units.inchesToMeters(16.0) / 2.0; // Centered X under cone nodes + public static final double complexLowXCubes = lowX; // Centered X under cube nodes + public static final double complexLowOuterYOffset = + nodeFirstY - Units.inchesToMeters(3.0) - (Units.inchesToMeters(25.75) / 2.0); + + public static final Translation2d[] complexLowTranslations = + new Translation2d[] { + new Translation2d(complexLowXCones, nodeFirstY - complexLowOuterYOffset), + new Translation2d(complexLowXCubes, nodeFirstY + nodeSeparationY * 1), + new Translation2d(complexLowXCones, nodeFirstY + nodeSeparationY * 2), + new Translation2d(complexLowXCones, nodeFirstY + nodeSeparationY * 3), + new Translation2d(complexLowXCubes, nodeFirstY + nodeSeparationY * 4), + new Translation2d(complexLowXCones, nodeFirstY + nodeSeparationY * 5), + new Translation2d(complexLowXCones, nodeFirstY + nodeSeparationY * 6), + new Translation2d(complexLowXCubes, nodeFirstY + nodeSeparationY * 7), + new Translation2d( + complexLowXCones, nodeFirstY + nodeSeparationY * 8 + complexLowOuterYOffset), + }; + } + + // Dimensions for loading zone and substations, including the tape + public static final class LoadingZone { + // Region dimensions + public static final double width = Units.inchesToMeters(99.0); + public static final double innerX = FieldConstants.fieldLength; + public static final double midX = fieldLength - Units.inchesToMeters(132.25); + public static final double outerX = fieldLength - Units.inchesToMeters(264.25); + public static final double leftY = FieldConstants.fieldWidth; + public static final double midY = leftY - Units.inchesToMeters(50.5); + public static final double rightY = leftY - width; + public static final Translation2d[] regionCorners = + new Translation2d[] { + new Translation2d( + midX, rightY), // Start at lower left next to border with opponent community + new Translation2d(midX, midY), + new Translation2d(outerX, midY), + new Translation2d(outerX, leftY), + new Translation2d(innerX, leftY), + new Translation2d(innerX, rightY), + }; + + // Double substation dimensions + public static final double doubleSubstationLength = Units.inchesToMeters(14.0); + public static final double doubleSubstationWidth = Units.inchesToMeters(96.0); + public static final double doubleSubstationX = innerX - doubleSubstationLength; + public static final double doubleSubstationShelfZ = Units.inchesToMeters(37.375); + + // Single substation dimensions + public static final double singleSubstationWidth = Units.inchesToMeters(22.75); + public static final double singleSubstationLeftX = + FieldConstants.fieldLength - doubleSubstationLength - Units.inchesToMeters(88.77); + public static final double singleSubstationCenterX = + singleSubstationLeftX + (singleSubstationWidth / 2.0); + public static final double singleSubstationRightX = + singleSubstationLeftX + singleSubstationWidth; + public static final Translation2d singleSubstationTranslation = + new Translation2d(singleSubstationCenterX, leftY); + + public static final double singleSubstationHeight = Units.inchesToMeters(18.0); + public static final double singleSubstationLowZ = Units.inchesToMeters(27.125); + public static final double singleSubstationCenterZ = + singleSubstationLowZ + (singleSubstationHeight / 2.0); + public static final double singleSubstationHighZ = + singleSubstationLowZ + singleSubstationHeight; + } + + // Locations of staged game pieces + public static final class StagingLocations { + public static final double centerOffsetX = Units.inchesToMeters(47.36); + public static final double positionX = fieldLength / 2.0 - Units.inchesToMeters(47.36); + public static final double firstY = Units.inchesToMeters(36.19); + public static final double separationY = Units.inchesToMeters(48.0); + public static final Translation2d[] translations = new Translation2d[4]; + + static { + for (int i = 0; i < translations.length; i++) { + translations[i] = new Translation2d(positionX, firstY + (i * separationY)); + } + } + } + + // AprilTag locations (do not flip for red alliance) + public static final Map aprilTags = + Map.of( + 1, + new Pose3d( + Units.inchesToMeters(610.77), + Units.inchesToMeters(42.19), + Units.inchesToMeters(18.22), + new Rotation3d(0.0, 0.0, Math.PI)), + 2, + new Pose3d( + Units.inchesToMeters(610.77), + Units.inchesToMeters(108.19), + Units.inchesToMeters(18.22), + new Rotation3d(0.0, 0.0, Math.PI)), + 3, + new Pose3d( + Units.inchesToMeters(610.77), + Units.inchesToMeters(174.19), // FIRST's diagram has a typo (it says 147.19) + Units.inchesToMeters(18.22), + new Rotation3d(0.0, 0.0, Math.PI)), + 4, + new Pose3d( + Units.inchesToMeters(636.96), + Units.inchesToMeters(265.74), + Units.inchesToMeters(27.38), + new Rotation3d(0.0, 0.0, Math.PI)), + 5, + new Pose3d( + Units.inchesToMeters(14.25), + Units.inchesToMeters(265.74), + Units.inchesToMeters(27.38), + new Rotation3d()), + 6, + new Pose3d( + Units.inchesToMeters(40.45), + Units.inchesToMeters(174.19), // FIRST's diagram has a typo (it says 147.19) + Units.inchesToMeters(18.22), + new Rotation3d()), + 7, + new Pose3d( + Units.inchesToMeters(40.45), + Units.inchesToMeters(108.19), + Units.inchesToMeters(18.22), + new Rotation3d()), + 8, + new Pose3d( + Units.inchesToMeters(40.45), + Units.inchesToMeters(42.19), + Units.inchesToMeters(18.22), + new Rotation3d())); + + /** + * Flips a translation to the correct side of the field based on the current alliance color. By + * default, all translations and poses in {@link FieldConstants} are stored with the origin at the + * rightmost point on the BLUE ALLIANCE wall. + */ + public static Translation2d allianceFlip(Translation2d translation) { + if (DriverStation.getAlliance() == Alliance.Red) { + return new Translation2d(fieldLength - translation.getX(), translation.getY()); + } else { + return translation; + } + } + + /** + * Flips a pose to the correct side of the field based on the current alliance color. By default, + * all translations and poses in {@link FieldConstants} are stored with the origin at the + * rightmost point on the BLUE ALLIANCE wall. + */ + public static Pose2d allianceFlip(Pose2d pose) { + if (DriverStation.getAlliance() == Alliance.Red) { + return new Pose2d( + fieldLength - pose.getX(), + pose.getY(), + new Rotation2d(-pose.getRotation().getCos(), pose.getRotation().getSin())); + } else { + return pose; + } + } +} diff --git a/src/main/java/frc/robot/Constants.java b/src/main/java/frc/robot/Constants.java index 270811a3..badc13dd 100644 --- a/src/main/java/frc/robot/Constants.java +++ b/src/main/java/frc/robot/Constants.java @@ -17,11 +17,15 @@ * 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. * + *

Subsystem-specific constants should be defined in the subsystem's own constant class. + * Constants that vary from robot to robot should be defined in the config classes. + * *

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 { + // set to true in order to change all Tunable values via Shuffleboard public static final boolean TUNING_MODE = false; private static final RobotType ROBOT = RobotType.ROBOT_SIMBOT; @@ -31,7 +35,7 @@ public final class Constants { // FIXME: update for various robots public enum RobotType { - ROBOT_2022_SIERRA, + ROBOT_2023_NOVA, ROBOT_2023_MK4I, ROBOT_DEFAULT, ROBOT_SIMBOT @@ -55,7 +59,7 @@ public static RobotType getRobot() { public static Mode getMode() { switch (getRobot()) { case ROBOT_DEFAULT: - case ROBOT_2022_SIERRA: + case ROBOT_2023_NOVA: case ROBOT_2023_MK4I: return RobotBase.isReal() ? Mode.REAL : Mode.REPLAY; diff --git a/src/main/java/frc/robot/Field2d.java b/src/main/java/frc/robot/Field2d.java new file mode 100644 index 00000000..b5a10eb1 --- /dev/null +++ b/src/main/java/frc/robot/Field2d.java @@ -0,0 +1,246 @@ +package frc.robot; + +import com.pathplanner.lib.PathConstraints; +import com.pathplanner.lib.PathPlanner; +import com.pathplanner.lib.PathPlannerTrajectory; +import com.pathplanner.lib.PathPoint; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.wpilibj.DriverStation; +import frc.lib.team3061.RobotConfig; +import frc.lib.team6328.util.FieldConstants; +import frc.robot.subsystems.drivetrain.Drivetrain; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.Set; + +/** + * This singleton class models the field as a collection of regions. This class is used to create a + * path from a starting pose in one region to an ending pose in another region that passes through + * the transition points defined for those regions. + * + *

The coordinate system of the field is oriented such that the origin is in the lower left + * corner when the blue alliance is to the left (i.e., to the blue alliance driver's right). It can + * map poses to reflect the current alliance. + */ +public class Field2d { + private static Field2d instance = null; + + private Region2d[] regions; + private DriverStation.Alliance alliance = DriverStation.Alliance.Invalid; + + /** + * Get the singleton instance of the Field2d class. + * + * @return the singleton instance of the Field2d class + */ + public static Field2d getInstance() { + if (instance == null) { + instance = new Field2d(); + } + return instance; + } + /** + * Construct a Field2d from an array of regions. These regions should not be overlapping (aside + * from edges) and any regions with overlapping edges should be neighbors (see + * Region2d::addNeighbor). + * + * @param regions the regions that define the field + */ + public void setRegions(Region2d[] regions) { + this.regions = regions; + } + + /** + * Set the current alliance. This will adjust the coordinate system of the field and its regions. + * + * @param newAlliance the new alliance + */ + public void updateAlliance(DriverStation.Alliance newAlliance) { + this.alliance = newAlliance; + for (Region2d region : regions) { + region.updateAlliance(newAlliance); + } + } + + /** + * Translates the specified pose based on the current alliance. + * + * @param pose the pose to translate based on the current alliance + * @return the translated pose based on the current alliance + */ + public Pose2d mapPoseToCurrentAlliance(Pose2d pose) { + if (this.alliance == DriverStation.Alliance.Red) { + return new Pose2d( + new Translation2d( + pose.getTranslation().getX(), + FieldConstants.fieldWidth - pose.getTranslation().getY()), + new Rotation2d(pose.getRotation().getCos(), -pose.getRotation().getSin())); + } else { + return pose; + } + } + + /** + * Create a path from a starting pose in one region to an ending pose in another region that + * passes through the transition points defined for those regions. + * + * @param start the starting pose + * @param end the ending pose + * @param pathConstants the path constraints (i.e., max velocity, max acceleration) + * @param subsystem the drivetrain subsystem + * @return the path from the starting pose to the ending pose; null if no path exists + */ + public PathPlannerTrajectory makePath( + Pose2d start, Pose2d end, PathConstraints pathConstants, Drivetrain subsystem) { + Region2d startRegion = null; + Region2d endRegion = null; + + // find the starting and ending regions + for (Region2d region : regions) { + if (region.contains(start)) { + startRegion = region; + } + if (region.contains(end)) { + endRegion = region; + } + } + + // make sure both start and end are on the field + if (startRegion == null || endRegion == null) return null; + + // BFS to find the shortest path to the end + List path = breadthFirstSearch(startRegion, endRegion); + if (path.isEmpty()) return null; + + // create point locations + ArrayList pointLocations = new ArrayList<>(); + + // add the starting point + pointLocations.add(start.getTranslation()); + + // add all the transition points + for (int i = 0; i < path.size() - 1; i++) { + Region2d from = path.get(i); + Region2d to = path.get(i + 1); + pointLocations.add(from.getTransitionPoint(to)); + } + + // add a transition point if starting region & ending region same + if (startRegion == endRegion) { + pointLocations.add( + new Translation2d((start.getX() + end.getX()) / 2, (start.getY() + end.getY()) / 2)); + } + + // add the ending point + pointLocations.add(end.getTranslation()); + + List pathPoints = createPathPoints(start, end, subsystem, pointLocations); + return PathPlanner.generatePath(pathConstants, pathPoints); + } + + /** + * Create the path points based on the starting and ending poses and the point locations. The path + * will be created such that the first path point matches the robot's current heading and velocity + * to ensure a smooth transition to the path. The the starting and ending poses have different + * rotations, the change in rotation will occur between the first and second points. The final + * speed of the robot will be as specified by the robot's configuration class' + * getMoveToPathFinalVelocity method. + * + * @param start the starting pose + * @param end the ending pose + * @param subsystem the drivetrain subsystem + * @param pointLocations the locations of the points in the path + * @return the path points + */ + private List createPathPoints( + Pose2d start, Pose2d end, Drivetrain subsystem, ArrayList pointLocations) { + List pathPoints = new ArrayList<>(); + Rotation2d lastHeading = null; + for (int i = 0; i < pointLocations.size() - 1; i++) { + double deltaX = pointLocations.get(i + 1).getX() - pointLocations.get(i).getX(); + double deltaY = pointLocations.get(i + 1).getY() - pointLocations.get(i).getY(); + lastHeading = new Rotation2d(deltaX, deltaY); + if (i == 0) { + // if the robot is not currently moving, orient the heading towards the next point + if (subsystem.getVelocityX() == 0 && subsystem.getVelocityY() == 0) { + pathPoints.add( + new PathPoint( + pointLocations.get(i), + lastHeading, + start.getRotation(), + Math.sqrt( + Math.pow(subsystem.getVelocityX(), 2) + + Math.pow(subsystem.getVelocityY(), 2)))); + } + // if the robot is currently moving, maintain the current heading and velocity in order to + // have a smooth transition to the start of the path + else { + pathPoints.add( + new PathPoint( + pointLocations.get(i), + new Rotation2d(subsystem.getVelocityX(), subsystem.getVelocityY()), + start.getRotation(), + Math.sqrt( + Math.pow(subsystem.getVelocityX(), 2) + + Math.pow(subsystem.getVelocityY(), 2)))); + } + } else { + pathPoints.add(new PathPoint(pointLocations.get(i), lastHeading, end.getRotation())); + } + } + + // the final path point will match the ending pose's rotation and the velocity as specified by + // the robot's configuration class' getMoveToPathFinalVelocity method. + pathPoints.add( + new PathPoint( + pointLocations.get(pointLocations.size() - 1), + end.getRotation(), + end.getRotation(), + RobotConfig.getInstance().getMoveToPathFinalVelocity())); + + return pathPoints; + } + + private List breadthFirstSearch(Region2d start, Region2d end) { + Queue> todo = new LinkedList<>(); + Set explored = new HashSet<>(); + + // add the starting region to the set of explored regions + explored.add(start); + + // if the path starts and ends in the same region, return that region + if (start == end) { + return new ArrayList<>(Arrays.asList(start)); + } + + todo.add( + new ArrayList<>( + Arrays.asList(start))); // add a path starting with startRegion to the todo list + + while (!todo.isEmpty()) { // while the todo list isn't empty, keep looking over the todo list. + ArrayList path = todo.poll(); + Region2d region = path.get(path.size() - 1); // last region in the path + + for (Region2d other : region.getNeighbors()) { + if (!explored.contains(other)) { + ArrayList newPath = new ArrayList<>(path); + newPath.add(other); + + if (other == end) { + return newPath; + } + + explored.add(other); + todo.add(newPath); + } + } + } + return new ArrayList<>(); + } +} diff --git a/src/main/java/frc/robot/FieldRegionConstants.java b/src/main/java/frc/robot/FieldRegionConstants.java new file mode 100644 index 00000000..157f9c24 --- /dev/null +++ b/src/main/java/frc/robot/FieldRegionConstants.java @@ -0,0 +1,296 @@ +package frc.robot; + +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Translation2d; +import frc.lib.team6328.util.FieldConstants; + +/** + * This class defines the points for all of the regions that defines the field along with the + * transitions points between these regions. For more information refer to the Field2d and Region2d + * classes. + */ +public final class FieldRegionConstants { + private static final String CONSTRUCTOR_EXCEPTION = "constant class"; + + private FieldRegionConstants() { + throw new IllegalStateException(CONSTRUCTOR_EXCEPTION); + } + + // Points making up the Community Zone (not including Charging Station) + public static final Translation2d COMMUNITY_POINT_1 = FieldConstants.Community.regionCorners[0]; + public static final Translation2d COMMUNITY_POINT_2 = FieldConstants.Community.regionCorners[1]; + public static final Translation2d COMMUNITY_POINT_3 = + new Translation2d( + FieldConstants.Community.chargingStationInnerX, FieldConstants.Community.leftY); + public static final Translation2d COMMUNITY_POINT_4 = + new Translation2d( + FieldConstants.Community.chargingStationOuterX, FieldConstants.Community.leftY); + public static final Translation2d COMMUNITY_POINT_5 = + new Translation2d( + FieldConstants.Community.chargingStationOuterX, + FieldConstants.Community.chargingStationLeftY); + public static final Translation2d COMMUNITY_POINT_6 = + new Translation2d( + FieldConstants.Community.chargingStationInnerX, + FieldConstants.Community.chargingStationLeftY); + public static final Translation2d COMMUNITY_POINT_7 = + new Translation2d( + FieldConstants.Community.chargingStationInnerX, + FieldConstants.Community.chargingStationRightY); + public static final Translation2d COMMUNITY_POINT_8 = + new Translation2d( + FieldConstants.Community.chargingStationOuterX, + FieldConstants.Community.chargingStationRightY); + public static final Translation2d COMMUNITY_POINT_9 = + new Translation2d( + FieldConstants.Community.chargingStationOuterX, FieldConstants.Community.rightY); + public static final Translation2d COMMUNITY_POINT_10 = + new Translation2d( + FieldConstants.Community.chargingStationInnerX, FieldConstants.Community.rightY); + + // Points making up the Loading Zone + public static final Translation2d LOADING_ZONE_POINT_1 = + FieldConstants.LoadingZone.regionCorners[0]; + public static final Translation2d LOADING_ZONE_POINT_2 = + FieldConstants.LoadingZone.regionCorners[1]; + public static final Translation2d LOADING_ZONE_POINT_3 = + FieldConstants.LoadingZone.regionCorners[2]; + public static final Translation2d LOADING_ZONE_POINT_4 = + FieldConstants.LoadingZone.regionCorners[3]; + public static final Translation2d LOADING_ZONE_POINT_5 = + new Translation2d(FieldConstants.LoadingZone.midX, FieldConstants.LoadingZone.leftY); + public static final Translation2d LOADING_ZONE_POINT_6 = + FieldConstants.LoadingZone.regionCorners[4]; + public static final Translation2d LOADING_ZONE_POINT_7 = + FieldConstants.LoadingZone.regionCorners[5]; + + // Points making up the rest of the Game Field (not including opposite Alliance zones) + public static final Translation2d FIELD_POINT_1 = + new Translation2d( + FieldConstants.Community.chargingStationOuterX, FieldConstants.Community.rightY); + public static final Translation2d FIELD_POINT_2 = + new Translation2d( + FieldConstants.fieldLength - FieldConstants.Community.chargingStationOuterX, + FieldConstants.Community.rightY); + public static final Translation2d FIELD_POINT_3 = + new Translation2d( + FieldConstants.fieldLength - FieldConstants.Community.chargingStationOuterX, + (FieldConstants.Community.leftY + FieldConstants.Community.rightY) / 2.0); + public static final Translation2d FIELD_POINT_4 = + new Translation2d( + FieldConstants.fieldLength - FieldConstants.Community.chargingStationOuterX, + FieldConstants.Community.leftY); + public static final Translation2d FIELD_POINT_5 = + new Translation2d(FieldConstants.LoadingZone.midX, FieldConstants.Community.leftY); + public static final Translation2d FIELD_POINT_6 = + new Translation2d(FieldConstants.LoadingZone.midX, FieldConstants.LoadingZone.midY); + public static final Translation2d FIELD_POINT_7 = + new Translation2d( + FieldConstants.fieldLength - FieldConstants.Community.chargingStationOuterX, + FieldConstants.LoadingZone.midY); + public static final Translation2d FIELD_POINT_8 = + new Translation2d(FieldConstants.LoadingZone.outerX, FieldConstants.LoadingZone.midY); + public static final Translation2d FIELD_POINT_9 = + new Translation2d(FieldConstants.LoadingZone.outerX, FieldConstants.LoadingZone.leftY); + public static final Translation2d FIELD_POINT_10 = + new Translation2d( + FieldConstants.fieldLength - FieldConstants.LoadingZone.outerX, + FieldConstants.LoadingZone.leftY); + public static final Translation2d FIELD_POINT_11 = + new Translation2d( + FieldConstants.fieldLength - FieldConstants.LoadingZone.outerX, + FieldConstants.LoadingZone.midY); + public static final Translation2d FIELD_POINT_12 = + new Translation2d( + FieldConstants.Community.chargingStationOuterX, FieldConstants.LoadingZone.midY); + public static final Translation2d FIELD_POINT_13 = + new Translation2d( + FieldConstants.Community.chargingStationOuterX, + (FieldConstants.Community.leftY + FieldConstants.Community.rightY) / 2.0); + + protected static final Translation2d[] COMMUNITY_REGION_POINTS_1 = + new Translation2d[] { + COMMUNITY_POINT_1, COMMUNITY_POINT_2, COMMUNITY_POINT_3, COMMUNITY_POINT_10 + }; + protected static final Translation2d[] COMMUNITY_REGION_POINTS_2 = + new Translation2d[] { + COMMUNITY_POINT_6, COMMUNITY_POINT_3, COMMUNITY_POINT_4, COMMUNITY_POINT_5 + }; + protected static final Translation2d[] COMMUNITY_REGION_POINTS_3 = + new Translation2d[] { + COMMUNITY_POINT_10, COMMUNITY_POINT_7, COMMUNITY_POINT_8, COMMUNITY_POINT_9 + }; + + protected static final Translation2d[] LOADING_ZONE_REGION_POINTS_1 = + new Translation2d[] { + LOADING_ZONE_POINT_1, LOADING_ZONE_POINT_7, LOADING_ZONE_POINT_6, LOADING_ZONE_POINT_5 + }; + protected static final Translation2d[] LOADING_ZONE_REGION_POINTS_2 = + new Translation2d[] { + LOADING_ZONE_POINT_3, LOADING_ZONE_POINT_2, LOADING_ZONE_POINT_5, LOADING_ZONE_POINT_4 + }; + + protected static final Translation2d[] FIELD_ZONE_REGION_POINTS_1 = + new Translation2d[] {FIELD_POINT_13, FIELD_POINT_3, FIELD_POINT_7, FIELD_POINT_12}; + protected static final Translation2d[] FIELD_ZONE_REGION_POINTS_2 = + new Translation2d[] {FIELD_POINT_1, FIELD_POINT_2, FIELD_POINT_3, FIELD_POINT_13}; + protected static final Translation2d[] FIELD_ZONE_REGION_POINTS_3 = + new Translation2d[] {FIELD_POINT_4, FIELD_POINT_5, FIELD_POINT_6, FIELD_POINT_7}; + protected static final Translation2d[] FIELD_ZONE_REGION_POINTS_4 = + new Translation2d[] {FIELD_POINT_8, FIELD_POINT_9, FIELD_POINT_10, FIELD_POINT_11}; + + public static final Region2d COMMUNITY_REGION_1 = new Region2d(COMMUNITY_REGION_POINTS_1); + public static final Region2d COMMUNITY_REGION_2 = new Region2d(COMMUNITY_REGION_POINTS_2); + public static final Region2d COMMUNITY_REGION_3 = new Region2d(COMMUNITY_REGION_POINTS_3); + public static final Region2d LOADING_ZONE_REGION_1 = new Region2d(LOADING_ZONE_REGION_POINTS_1); + public static final Region2d LOADING_ZONE_REGION_2 = new Region2d(LOADING_ZONE_REGION_POINTS_2); + public static final Region2d FIELD_ZONE_REGION_1 = new Region2d(FIELD_ZONE_REGION_POINTS_1); + public static final Region2d FIELD_ZONE_REGION_2 = new Region2d(FIELD_ZONE_REGION_POINTS_2); + public static final Region2d FIELD_ZONE_REGION_3 = new Region2d(FIELD_ZONE_REGION_POINTS_3); + public static final Region2d FIELD_ZONE_REGION_4 = new Region2d(FIELD_ZONE_REGION_POINTS_4); + + // Points setting the transition points between each region + public static final Translation2d COMMUNITY_REGION_1_2_TRANSITION_POINT = + new Translation2d( + FieldConstants.Community.chargingStationInnerX, + (FieldConstants.Community.leftY + FieldConstants.Community.chargingStationLeftY) / 2.0); + public static final Translation2d COMMUNITY_REGION_2_1_TRANSITION_POINT = + new Translation2d( + FieldConstants.Community.chargingStationInnerX - 0.5, + (FieldConstants.Community.leftY + FieldConstants.Community.chargingStationLeftY) / 2.0 + - 0.5); + public static final Translation2d COMMUNITY_REGION_1_3_TRANSITION_POINT = + new Translation2d( + FieldConstants.Community.chargingStationInnerX, + FieldConstants.Community.chargingStationRightY / 2.0); + public static final Translation2d COMMUNITY_REGION_3_1_TRANSITION_POINT = + new Translation2d( + FieldConstants.Community.chargingStationInnerX - 0.5, + FieldConstants.Community.chargingStationRightY / 2.0 + 0.5); + + public static final Translation2d LOADING_ZONE_REGION_1_2_TRANSITION_POINT = + new Translation2d( + FieldConstants.LoadingZone.midX - 0.5, + (FieldConstants.LoadingZone.leftY + FieldConstants.LoadingZone.midY) / 2.0); + public static final Translation2d LOADING_ZONE_REGION_2_1_TRANSITION_POINT = + new Translation2d( + FieldConstants.LoadingZone.midX + 0.5, + (FieldConstants.LoadingZone.leftY + FieldConstants.LoadingZone.midY) / 2.0); + + public static final Translation2d FIELD_ZONE_REGION_1_2_TRANSITION_POINT = + new Translation2d( + FieldConstants.fieldLength / 2.0, + (FieldConstants.Community.leftY + FieldConstants.Community.rightY) / 2.0 - 0.5); + public static final Translation2d FIELD_ZONE_REGION_2_1_TRANSITION_POINT = + new Translation2d( + FieldConstants.fieldLength / 2.0, + (FieldConstants.Community.leftY + FieldConstants.Community.rightY) / 2.0 + 0.5); + public static final Translation2d FIELD_ZONE_REGION_1_3_TRANSITION_POINT = + new Translation2d( + FieldConstants.LoadingZone.midX - 0.5, + (FieldConstants.LoadingZone.midY + FieldConstants.LoadingZone.rightY) / 2.0); + public static final Translation2d FIELD_ZONE_REGION_3_1_TRANSITION_POINT = + new Translation2d( + FieldConstants.fieldLength - FieldConstants.Community.chargingStationOuterX - 0.5, + (FieldConstants.LoadingZone.midY + FieldConstants.LoadingZone.rightY) / 2.0); + public static final Translation2d FIELD_ZONE_REGION_1_4_TRANSITION_POINT = + new Translation2d( + FieldConstants.LoadingZone.outerX + 0.5, + (FieldConstants.LoadingZone.leftY + FieldConstants.LoadingZone.midY) / 2.0); + public static final Translation2d FIELD_ZONE_REGION_4_1_TRANSITION_POINT = + new Translation2d( + FieldConstants.fieldLength / 2.0 - 0.5, + (FieldConstants.LoadingZone.midY + FieldConstants.LoadingZone.rightY) / 2.0); + + public static final Translation2d COMMUNITY_2_TO_FIELD_1_TRANSITION_POINT = + new Translation2d( + FieldConstants.Community.chargingStationOuterX + 0.5, + (FieldConstants.LoadingZone.midY + FieldConstants.Community.chargingStationLeftY) / 2.0); + public static final Translation2d COMMUNITY_3_TO_FIELD_2_TRANSITION_POINT = + new Translation2d( + FieldConstants.Community.chargingStationOuterX + 0.5, + FieldConstants.Community.chargingStationRightY / 2.0); + + public static final Translation2d FIELD_1_TO_COMMUNITY_2_TRANSITION_POINT = + new Translation2d( + FieldConstants.Community.chargingStationOuterX - 0.5, + (FieldConstants.Community.leftY + FieldConstants.Community.chargingStationLeftY) / 2.0); + public static final Translation2d FIELD_2_TO_COMMUNITY_3_TRANSITION_POINT = + new Translation2d( + FieldConstants.Community.chargingStationOuterX - 0.5, + FieldConstants.Community.chargingStationRightY / 2.0); + public static final Translation2d FIELD_3_TO_LOADING_1_TRANSITION_POINT = + new Translation2d(FieldConstants.LoadingZone.midX + 0.5, FieldConstants.LoadingZone.midY); + public static final Translation2d FIELD_4_TO_LOADING_2_TRANSITION_POINT = + new Translation2d(FieldConstants.LoadingZone.outerX + 0.5, FieldConstants.LoadingZone.midY); + + public static final Translation2d LOADING_2_TO_FIELD_4_TRANSITION_POINT = + new Translation2d(FieldConstants.LoadingZone.outerX - 0.5, FieldConstants.LoadingZone.midY); + + // Points setting the node locations + public static final Pose2d GRID_1_NODE_1 = + new Pose2d( + FieldConstants.Grids.outerX, + FieldConstants.Grids.nodeFirstY, + Rotation2d.fromDegrees(180)); + public static final Pose2d GRID_1_NODE_2 = + new Pose2d( + FieldConstants.Grids.outerX, + FieldConstants.Grids.nodeFirstY + FieldConstants.Grids.nodeSeparationY, + Rotation2d.fromDegrees(180)); + public static final Pose2d GRID_1_NODE_3 = + new Pose2d( + FieldConstants.Grids.outerX, + FieldConstants.Grids.nodeFirstY + FieldConstants.Grids.nodeSeparationY * 2, + Rotation2d.fromDegrees(180)); + + public static final Pose2d GRID_2_NODE_1 = + new Pose2d( + FieldConstants.Grids.outerX, + FieldConstants.Grids.nodeFirstY + FieldConstants.Grids.nodeSeparationY * 3, + Rotation2d.fromDegrees(180)); + public static final Pose2d GRID_2_NODE_2 = + new Pose2d( + FieldConstants.Grids.outerX, + FieldConstants.Grids.nodeFirstY + FieldConstants.Grids.nodeSeparationY * 4, + Rotation2d.fromDegrees(180)); + public static final Pose2d GRID_2_NODE_3 = + new Pose2d( + FieldConstants.Grids.outerX, + FieldConstants.Grids.nodeFirstY + FieldConstants.Grids.nodeSeparationY * 5, + Rotation2d.fromDegrees(180)); + + public static final Pose2d GRID_3_NODE_1 = + new Pose2d( + FieldConstants.Grids.outerX, + FieldConstants.Grids.nodeFirstY + FieldConstants.Grids.nodeSeparationY * 6, + Rotation2d.fromDegrees(180)); + public static final Pose2d GRID_3_NODE_2 = + new Pose2d( + FieldConstants.Grids.outerX, + FieldConstants.Grids.nodeFirstY + FieldConstants.Grids.nodeSeparationY * 7, + Rotation2d.fromDegrees(180)); + public static final Pose2d GRID_3_NODE_3 = + new Pose2d( + FieldConstants.Grids.outerX, + FieldConstants.Grids.nodeFirstY + FieldConstants.Grids.nodeSeparationY * 8, + Rotation2d.fromDegrees(180)); + + // Points setting the substation locations + public static final Pose2d SINGLE_SUBSTATION = + new Pose2d( + FieldConstants.LoadingZone.singleSubstationTranslation, Rotation2d.fromDegrees(90)); + public static final Pose2d DOUBLE_SUBSTATION_UPPER = + new Pose2d( + FieldConstants.LoadingZone.doubleSubstationX, + FieldConstants.LoadingZone.leftY + - ((FieldConstants.LoadingZone.doubleSubstationWidth / 4) - 0.1016), + Rotation2d.fromDegrees(0)); + public static final Pose2d DOUBLE_SUBSTATION_LOWER = + new Pose2d( + FieldConstants.LoadingZone.doubleSubstationX, + FieldConstants.LoadingZone.leftY + - ((3 * FieldConstants.LoadingZone.doubleSubstationWidth / 4) + 0.178), + Rotation2d.fromDegrees(0)); +} diff --git a/src/main/java/frc/robot/Region2d.java b/src/main/java/frc/robot/Region2d.java new file mode 100644 index 00000000..f038d3c6 --- /dev/null +++ b/src/main/java/frc/robot/Region2d.java @@ -0,0 +1,149 @@ +package frc.robot; + +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.wpilibj.DriverStation; +import frc.lib.team6328.util.FieldConstants; +import java.awt.geom.*; +import java.util.HashMap; +import java.util.Map.Entry; +import java.util.Set; +import org.littletonrobotics.junction.Logger; + +/** + * This class models a region of the field. It is defined by its vertices and the transition points + * to neighboring regions. + */ +public class Region2d { + private Path2D shape; + private HashMap transitionMap; + private DriverStation.Alliance alliance = DriverStation.Alliance.Invalid; + + /** + * Create a Region2d, a polygon, from an array of Translation2d specifying vertices of a polygon. + * The polygon is created using the even-odd winding rule. + * + * @param points the array of Translation2d that define the vertices of the region. + */ + public Region2d(Translation2d[] points) { + this.transitionMap = new HashMap<>(); + this.shape = new Path2D.Double(Path2D.WIND_EVEN_ODD, points.length); + this.shape.moveTo(points[0].getX(), points[0].getY()); + + for (int i = 1; i < points.length; i++) { + this.shape.lineTo(points[i].getX(), points[i].getY()); + } + + this.shape.closePath(); + } + + /** + * Log the bounding rectangle of the region and the transition points to neighboring regions. + * These can be visualized using AdvantageScope to confirm that the regions are properly defined. + */ + public void logPoints() { + // log the bounding rectangle of the region + Rectangle2D rect = this.shape.getBounds2D(); + Logger.getInstance() + .recordOutput("Region2d/point0", new Pose2d(rect.getX(), rect.getY(), new Rotation2d())); + Logger.getInstance() + .recordOutput( + "Region2d/point1", + new Pose2d(rect.getX() + rect.getWidth(), rect.getY(), new Rotation2d())); + Logger.getInstance() + .recordOutput( + "Region2d/point2", + new Pose2d(rect.getX(), rect.getY() + rect.getHeight(), new Rotation2d())); + Logger.getInstance() + .recordOutput( + "Region2d/point3", + new Pose2d( + rect.getX() + rect.getWidth(), rect.getY() + rect.getHeight(), new Rotation2d())); + + // assume that there are at most 4 neighbors + for (int i = 0; i < 4; i++) { + Logger.getInstance().recordOutput("Region2d/transition" + i, new Pose2d()); + } + int i = 0; + for (Entry entry : transitionMap.entrySet()) { + Translation2d point = entry.getValue(); + Logger.getInstance() + .recordOutput( + "Region2d/transition" + i, new Pose2d(point.getX(), point.getY(), new Rotation2d())); + i++; + } + } + + /** + * Returns true if the region contains a given Pose2d accounting for the alliance color. + * + * @param other the given pose2d + * @return if the pose is inside the region + */ + public boolean contains(Pose2d other) { + + if (this.alliance == DriverStation.Alliance.Red) { + return this.shape.contains( + new Point2D.Double(other.getX(), FieldConstants.fieldWidth - other.getY())); + } else { + return this.shape.contains(new Point2D.Double(other.getX(), other.getY())); + } + } + + /** + * Add a neighboring Region2d and the ideal point through which to transition from this region to + * the other region. Normally, this method will be invoked once per transition. The transition + * point doesn't need to be on the boundary between the two regions. It may be advantageous for + * the transition point to be located within the other region to influence the generated path. + * Therefore, the transition point from one region to another may not be the same when traversing + * the regions in the opposite direction. + * + * @param other the other region + * @param point a Translation2d representing the transition point + */ + public void addNeighbor(Region2d other, Translation2d point) { + transitionMap.put(other, point); + } + + /** + * Returns a Set of this region's neighbors + * + * @return + */ + public Set getNeighbors() { + return transitionMap.keySet(); + } + + /** + * Get the transition pont between this region and another region. Returns null if they aren't a + * neighbor. + * + * @param other the other region + * @return the transition point, represented as a Translation2d + */ + public Translation2d getTransitionPoint(Region2d other) { + if (getNeighbors().contains(other)) { + + if (this.alliance == DriverStation.Alliance.Red) { + return new Translation2d( + transitionMap.get(other).getX(), + FieldConstants.fieldWidth - transitionMap.get(other).getY()); + } else { + return transitionMap.get(other); + } + + } else { + return null; + } + } + + /** + * Sets the current alliance color which is used to transform the coordinates of the region. + * + * @param newAlliance the new alliance color + */ + public void updateAlliance(DriverStation.Alliance newAlliance) { + this.alliance = newAlliance; + } +} diff --git a/src/main/java/frc/robot/Robot.java b/src/main/java/frc/robot/Robot.java index 8718179e..8d0d2ed1 100644 --- a/src/main/java/frc/robot/Robot.java +++ b/src/main/java/frc/robot/Robot.java @@ -57,7 +57,7 @@ public void robotInit() { logger.recordMetadata(GIT_DIRTY, "All changes committed"); break; case 1: - logger.recordMetadata(GIT_DIRTY, "Uncomitted changes"); + logger.recordMetadata(GIT_DIRTY, "Uncommitted changes"); break; default: logger.recordMetadata(GIT_DIRTY, "Unknown"); @@ -114,7 +114,7 @@ public void robotInit() { /** * This method is called every robot packet, no matter the mode. Use this for items like - * diagnostics that you want ran during disabled, autonomous, teleoperated and test. + * diagnostics that you want ran during disabled, autonomous, teleop and test. * *

This runs after the mode specific periodic methods, but before LiveWindow and SmartDashboard * integrated updating. @@ -132,9 +132,14 @@ public void robotPeriodic() { logReceiverQueueAlert.set(Logger.getInstance().getReceiverQueueFault()); } + /** This method is invoked periodically when the robot is in the disabled state. */ @Override public void disabledPeriodic() { + // check if the operator interface (e.g., joysticks) has changed robotContainer.updateOI(); + + // check if the alliance color has changed based on the FMS data + robotContainer.checkAllianceColor(); } /** @@ -143,6 +148,11 @@ public void disabledPeriodic() { */ @Override public void autonomousInit() { + // check if the alliance color has changed based on the FMS data; the current alliance color is + // not guaranteed to be correct until the start of autonomous + robotContainer.checkAllianceColor(); + + robotContainer.autonomousInit(); autonomousCommand = robotContainer.getAutonomousCommand(); // schedule the autonomous command @@ -151,7 +161,7 @@ public void autonomousInit() { } } - /** This method is invoked at the start of the teleoperated period. */ + /** This method is invoked at the start of the teleop period. */ @Override public void teleopInit() { /* @@ -162,6 +172,13 @@ public void teleopInit() { if (autonomousCommand != null) { autonomousCommand.cancel(); } + + // check if the alliance color has changed based on the FMS data; if the robot power cycled + // during a match, this would be the first opportunity to check the alliance color based on FMS + // data. + robotContainer.checkAllianceColor(); + + robotContainer.teleopInit(); } /** This method is invoked at the start of the test period. */ diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java index 28f27f16..b9620c7e 100644 --- a/src/main/java/frc/robot/RobotContainer.java +++ b/src/main/java/frc/robot/RobotContainer.java @@ -4,14 +4,19 @@ package frc.robot; +import static frc.robot.Constants.*; +import static frc.robot.FieldRegionConstants.*; + import com.pathplanner.lib.PathPlanner; import com.pathplanner.lib.PathPlannerTrajectory; import com.pathplanner.lib.commands.FollowPathWithEvents; +import com.pathplanner.lib.server.PathPlannerServer; import edu.wpi.first.apriltag.AprilTagFieldLayout; +import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.first.wpilibj.DriverStation.Alliance; import edu.wpi.first.wpilibj.livewindow.LiveWindow; import edu.wpi.first.wpilibj.shuffleboard.Shuffleboard; import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.CommandScheduler; import edu.wpi.first.wpilibj2.command.Commands; import edu.wpi.first.wpilibj2.command.InstantCommand; import frc.lib.team3061.RobotConfig; @@ -33,13 +38,17 @@ import frc.robot.commands.FeedForwardCharacterization; import frc.robot.commands.FeedForwardCharacterization.FeedForwardCharacterizationData; import frc.robot.commands.FollowPath; +import frc.robot.commands.RotateToAngle; import frc.robot.commands.TeleopSwerve; import frc.robot.configs.DefaultRobotConfig; import frc.robot.configs.MK4IRobotConfig; -import frc.robot.configs.SierraRobotConfig; +import frc.robot.configs.NovaRobotConfig; import frc.robot.operator_interface.OISelector; import frc.robot.operator_interface.OperatorInterface; import frc.robot.subsystems.drivetrain.Drivetrain; +import frc.robot.subsystems.subsystem.Subsystem; +import frc.robot.subsystems.subsystem.SubsystemIO; +import frc.robot.subsystems.subsystem.SubsystemIOTalonFX; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; @@ -57,7 +66,9 @@ public class RobotContainer { private OperatorInterface oi = new OperatorInterface() {}; private RobotConfig config; private Drivetrain drivetrain; + private Alliance lastAlliance = DriverStation.Alliance.Invalid; private Vision vision; + private Subsystem subsystem; // use AdvantageKit's LoggedDashboardChooser instead of SendableChooser to ensure accurate logging private final LoggedDashboardChooser autoChooser = @@ -65,31 +76,27 @@ public class RobotContainer { // RobotContainer singleton private static RobotContainer robotContainer = new RobotContainer(); + private final Map autoEventMap = new HashMap<>(); - /** Create the container for the robot. Contains subsystems, OI devices, and commands. */ + /** + * Create the container for the robot. Contains subsystems, operator interface (OI) devices, and + * commands. + */ public RobotContainer() { /* * IMPORTANT: The RobotConfig subclass object *must* be created before any other objects * that use it directly or indirectly. If this isn't done, a null pointer exception will result. */ + createRobotConfig(); // create real, simulated, or replay subsystems based on the mode and robot specified if (Constants.getMode() != Mode.REPLAY) { switch (Constants.getRobot()) { case ROBOT_DEFAULT: + case ROBOT_2023_NOVA: case ROBOT_2023_MK4I: - case ROBOT_2022_SIERRA: { - // create the specific RobotConfig subclass instance first - if (Constants.getRobot() == Constants.RobotType.ROBOT_2023_MK4I) { - config = new MK4IRobotConfig(); - } else if (Constants.getRobot() == Constants.RobotType.ROBOT_2022_SIERRA) { - config = new SierraRobotConfig(); - } else { - config = new DefaultRobotConfig(); - } - GyroIO gyro = new GyroIOPigeon2(config.getGyroCANID()); int[] driveMotorCANIDs = config.getSwerveDriveMotorCANIDs(); @@ -141,13 +148,22 @@ public RobotContainer() { config.getRobotMaxVelocity()); drivetrain = new Drivetrain(gyro, flModule, frModule, blModule, brModule); - new Pneumatics(new PneumaticsIORev()); - vision = new Vision(new VisionIOPhotonVision(config.getCameraName())); + + String[] cameraNames = config.getCameraNames(); + VisionIO[] visionIOs = new VisionIO[cameraNames.length]; + for (int i = 0; i < visionIOs.length; i++) { + visionIOs[i] = new VisionIOPhotonVision(cameraNames[i]); + } + vision = new Vision(visionIOs); + subsystem = new Subsystem(new SubsystemIOTalonFX()); + + if (Constants.getRobot() == Constants.RobotType.ROBOT_DEFAULT) { + new Pneumatics(new PneumaticsIORev()); + } break; } case ROBOT_SIMBOT: { - config = new MK4IRobotConfig(); SwerveModule flModule = new SwerveModule(new SwerveModuleIOSim(), 0, config.getRobotMaxVelocity()); @@ -169,10 +185,13 @@ public RobotContainer() { } vision = new Vision( - new VisionIOSim( - layout, - drivetrain::getPose, - RobotConfig.getInstance().getRobotToCameraTransform())); + new VisionIO[] { + new VisionIOSim( + layout, + drivetrain::getPose, + RobotConfig.getInstance().getRobotToCameraTransforms()[0]) + }); + subsystem = new Subsystem(new SubsystemIO() {}); break; } @@ -193,42 +212,99 @@ public RobotContainer() { SwerveModule brModule = new SwerveModule(new SwerveModuleIO() {}, 3, config.getRobotMaxVelocity()); drivetrain = new Drivetrain(new GyroIO() {}, flModule, frModule, blModule, brModule); - new Pneumatics(new PneumaticsIO() {}); - vision = new Vision(new VisionIO() {}); + + String[] cameraNames = config.getCameraNames(); + VisionIO[] visionIOs = new VisionIO[cameraNames.length]; + for (int i = 0; i < visionIOs.length; i++) { + visionIOs[i] = new VisionIO() {}; + } + vision = new Vision(visionIOs); + subsystem = new Subsystem(new SubsystemIO() {}); } // disable all telemetry in the LiveWindow to reduce the processing during each iteration LiveWindow.disableAllTelemetry(); + constructField(); + updateOI(); configureAutoCommands(); } /** - * This method scans for any changes to the connected joystick. If anything changed, it creates - * new OI objects and binds all of the buttons to commands. + * The RobotConfig subclass object *must* be created before any other objects that use it directly + * or indirectly. If this isn't done, a null pointer exception will result. */ - public void updateOI() { - if (!OISelector.didJoysticksChange()) { - return; + private void createRobotConfig() { + switch (Constants.getRobot()) { + case ROBOT_DEFAULT: + config = new DefaultRobotConfig(); + break; + case ROBOT_2023_NOVA: + case ROBOT_SIMBOT: + config = new NovaRobotConfig(); + break; + case ROBOT_2023_MK4I: + config = new MK4IRobotConfig(); + break; } + } - CommandScheduler.getInstance().getActiveButtonLoop().clear(); - oi = OISelector.findOperatorInterface(); + /** + * Creates the field from the defined regions and transition points from one region to its + * neighbor. The field is used to generate paths. + */ + private void constructField() { + Field2d.getInstance() + .setRegions( + new Region2d[] { + COMMUNITY_REGION_1, + COMMUNITY_REGION_2, + COMMUNITY_REGION_3, + LOADING_ZONE_REGION_1, + LOADING_ZONE_REGION_2, + FIELD_ZONE_REGION_1, + FIELD_ZONE_REGION_2, + FIELD_ZONE_REGION_3, + FIELD_ZONE_REGION_4 + }); + + COMMUNITY_REGION_1.addNeighbor(COMMUNITY_REGION_2, COMMUNITY_REGION_1_2_TRANSITION_POINT); + COMMUNITY_REGION_2.addNeighbor(COMMUNITY_REGION_1, COMMUNITY_REGION_2_1_TRANSITION_POINT); + COMMUNITY_REGION_1.addNeighbor(COMMUNITY_REGION_3, COMMUNITY_REGION_1_3_TRANSITION_POINT); + COMMUNITY_REGION_3.addNeighbor(COMMUNITY_REGION_1, COMMUNITY_REGION_3_1_TRANSITION_POINT); + COMMUNITY_REGION_2.addNeighbor(FIELD_ZONE_REGION_1, COMMUNITY_2_TO_FIELD_1_TRANSITION_POINT); + COMMUNITY_REGION_3.addNeighbor(FIELD_ZONE_REGION_2, COMMUNITY_3_TO_FIELD_2_TRANSITION_POINT); + + LOADING_ZONE_REGION_1.addNeighbor( + LOADING_ZONE_REGION_2, LOADING_ZONE_REGION_1_2_TRANSITION_POINT); + LOADING_ZONE_REGION_2.addNeighbor( + LOADING_ZONE_REGION_1, LOADING_ZONE_REGION_2_1_TRANSITION_POINT); + LOADING_ZONE_REGION_2.addNeighbor(FIELD_ZONE_REGION_4, LOADING_2_TO_FIELD_4_TRANSITION_POINT); + + FIELD_ZONE_REGION_1.addNeighbor(FIELD_ZONE_REGION_2, FIELD_ZONE_REGION_1_2_TRANSITION_POINT); + FIELD_ZONE_REGION_2.addNeighbor(FIELD_ZONE_REGION_1, FIELD_ZONE_REGION_2_1_TRANSITION_POINT); + FIELD_ZONE_REGION_1.addNeighbor(FIELD_ZONE_REGION_3, FIELD_ZONE_REGION_1_3_TRANSITION_POINT); + FIELD_ZONE_REGION_3.addNeighbor(FIELD_ZONE_REGION_1, FIELD_ZONE_REGION_3_1_TRANSITION_POINT); + FIELD_ZONE_REGION_1.addNeighbor(FIELD_ZONE_REGION_4, FIELD_ZONE_REGION_1_4_TRANSITION_POINT); + FIELD_ZONE_REGION_4.addNeighbor(FIELD_ZONE_REGION_1, FIELD_ZONE_REGION_4_1_TRANSITION_POINT); + FIELD_ZONE_REGION_1.addNeighbor(COMMUNITY_REGION_2, FIELD_1_TO_COMMUNITY_2_TRANSITION_POINT); + FIELD_ZONE_REGION_2.addNeighbor(COMMUNITY_REGION_3, FIELD_2_TO_COMMUNITY_3_TRANSITION_POINT); + FIELD_ZONE_REGION_3.addNeighbor(LOADING_ZONE_REGION_1, FIELD_3_TO_LOADING_1_TRANSITION_POINT); + FIELD_ZONE_REGION_4.addNeighbor(LOADING_ZONE_REGION_2, FIELD_4_TO_LOADING_2_TRANSITION_POINT); + } - /* - * Set up the default command for the drivetrain. The joysticks' values map to percentage of the - * maximum velocities. The velocities may be specified from either the robot's frame of - * reference or the field's frame of reference. In the robot's frame of reference, the positive - * x direction is forward; the positive y direction, left; position rotation, CCW. In the field - * frame of reference, the origin of the field to the lower left corner (i.e., the corner of the - * field to the driver's right). Zero degrees is away from the driver and increases in the CCW - * direction. This is why the left joystick's y axis specifies the velocity in the x direction - * and the left joystick's x axis specifies the velocity in the y direction. - */ - drivetrain.setDefaultCommand( - new TeleopSwerve(drivetrain, oi::getTranslateX, oi::getTranslateY, oi::getRotate)); + /** + * This method scans for any changes to the connected operator interface (e.g., joysticks). If + * anything changed, it creates a new OI object and binds all of the buttons to commands. + */ + public void updateOI() { + OperatorInterface prevOI = oi; + oi = OISelector.getOperatorInterface(); + if (oi == prevOI) { + return; + } configureButtonBindings(); } @@ -244,69 +320,76 @@ public static RobotContainer getInstance() { /** Use this method to define your button->command mappings. */ private void configureButtonBindings() { - // field-relative toggle - oi.getFieldRelativeButton() - .toggleOnTrue( - Commands.either( - Commands.runOnce(drivetrain::disableFieldRelative, drivetrain), - Commands.runOnce(drivetrain::enableFieldRelative, drivetrain), - drivetrain::getFieldRelative)); - // reset gyro to 0 degrees - oi.getResetGyroButton().onTrue(Commands.runOnce(drivetrain::zeroGyroscope, drivetrain)); + configureDrivetrainCommands(); - // x-stance - oi.getXStanceButton().onTrue(Commands.runOnce(drivetrain::enableXstance, drivetrain)); - oi.getXStanceButton().onFalse(Commands.runOnce(drivetrain::disableXstance, drivetrain)); - oi.getVisionIsEnabledSwitch() - .toggleOnTrue( - Commands.either( - Commands.parallel( - Commands.runOnce(() -> vision.enable(false)), - Commands.runOnce(drivetrain::resetPoseRotationToGyro)), - Commands.runOnce(() -> vision.enable(true), vision), - vision::isEnabled)); + configureSubsystemCommands(); + + configureVisionCommands(); + + // interrupt all commands by running a command that requires every subsystem. This is used to + // recover to a known state if the robot becomes "stuck" in a command. + oi.getInterruptAll() + .onTrue( + Commands.parallel( + Commands.runOnce(drivetrain::disableXstance), + Commands.runOnce(() -> subsystem.setMotorPower(0)), + new TeleopSwerve(drivetrain, oi::getTranslateX, oi::getTranslateY, oi::getRotate))); } /** Use this method to define your commands for autonomous mode. */ private void configureAutoCommands() { + // Waypoints autoEventMap.put("event1", Commands.print("passed marker 1")); autoEventMap.put("event2", Commands.print("passed marker 2")); // build auto path commands + + // add commands to the auto chooser + autoChooser.addDefaultOption("Do Nothing", new InstantCommand()); + + /************ Test Path ************ + * + * demonstration of PathPlanner path group with event markers + * + */ List auto1Paths = PathPlanner.loadPathGroup( - "testPaths1", config.getAutoMaxSpeed(), config.getAutoMaxAcceleration()); + "TestPath", config.getAutoMaxSpeed(), config.getAutoMaxAcceleration()); Command autoTest = Commands.sequence( new FollowPathWithEvents( - new FollowPath(auto1Paths.get(0), drivetrain, true), + new FollowPath(auto1Paths.get(0), drivetrain, true, true), auto1Paths.get(0).getMarkers(), autoEventMap), Commands.runOnce(drivetrain::enableXstance, drivetrain), Commands.waitSeconds(5.0), Commands.runOnce(drivetrain::disableXstance, drivetrain), new FollowPathWithEvents( - new FollowPath(auto1Paths.get(1), drivetrain, false), + new FollowPath(auto1Paths.get(1), drivetrain, false, true), auto1Paths.get(1).getMarkers(), autoEventMap)); - - // add commands to the auto chooser - autoChooser.addDefaultOption("Do Nothing", new InstantCommand()); - - // demonstration of PathPlanner path group with event markers autoChooser.addOption("Test Path", autoTest); - // "auto" command for tuning the drive velocity PID - autoChooser.addOption( - "Drive Velocity Tuning", - Commands.sequence( - Commands.runOnce(drivetrain::disableFieldRelative, drivetrain), - Commands.deadline( - Commands.waitSeconds(5.0), - Commands.run(() -> drivetrain.drive(1.5, 0.0, 0.0, false), drivetrain)))); + /************ Start Point ************ + * + * useful for initializing the pose of the robot to a known location + * + */ - // "auto" command for characterizing the drivetrain + PathPlannerTrajectory startPointPath = + PathPlanner.loadPath( + "StartPoint", config.getAutoMaxSpeed(), config.getAutoMaxAcceleration()); + Command startPoint = + Commands.runOnce( + () -> drivetrain.resetOdometry(startPointPath.getInitialState()), drivetrain); + autoChooser.addOption("Start Point", startPoint); + + /************ Drive Characterization ************ + * + * useful for characterizing the drivetrain (i.e, determining kS and kV) + * + */ autoChooser.addOption( "Drive Characterization", new FeedForwardCharacterization( @@ -316,7 +399,119 @@ private void configureAutoCommands() { drivetrain::runCharacterizationVolts, drivetrain::getCharacterizationVelocity)); + /************ Distance Test ************ + * + * used for empirically determining the wheel diameter + * + */ + PathPlannerTrajectory distanceTestPath = PathPlanner.loadPath("DistanceTest", 2, 2); + Command distanceTestPathCommand = new FollowPath(distanceTestPath, drivetrain, true, true); + autoChooser.addOption("Distance Path", distanceTestPathCommand); + + /************ Auto Tuning ************ + * + * useful for tuning the autonomous PID controllers + * + */ + PathPlannerTrajectory tuningPath = PathPlanner.loadPath("Tuning", 2.0, 3.0); + Command tuningCommand = new FollowPath(tuningPath, drivetrain, true, true); + autoChooser.addOption("Auto Tuning", tuningCommand); + + /************ Drive Velocity Tuning ************ + * + * useful for tuning the drive velocity PID controller + * + */ + autoChooser.addOption( + "Drive Velocity Tuning", + Commands.sequence( + Commands.runOnce(drivetrain::disableFieldRelative, drivetrain), + Commands.deadline( + Commands.waitSeconds(5.0), + Commands.run(() -> drivetrain.drive(1.5, 0.0, 0.0, false, false), drivetrain)))); + Shuffleboard.getTab("MAIN").add(autoChooser.getSendableChooser()); + + // enable the path planner server so we can update paths without redeploying code + if (TUNING_MODE) { + PathPlannerServer.startServer(3061); + } + } + + private void configureDrivetrainCommands() { + /* + * Set up the default command for the drivetrain. The joysticks' values map to percentage of the + * maximum velocities. The velocities may be specified from either the robot's frame of + * reference or the field's frame of reference. In the robot's frame of reference, the positive + * x direction is forward; the positive y direction, left; position rotation, CCW. In the field + * frame of reference, the origin of the field to the lower left corner (i.e., the corner of the + * field to the driver's right). Zero degrees is away from the driver and increases in the CCW + * direction. This is why the left joystick's y axis specifies the velocity in the x direction + * and the left joystick's x axis specifies the velocity in the y direction. + */ + drivetrain.setDefaultCommand( + new TeleopSwerve(drivetrain, oi::getTranslateX, oi::getTranslateY, oi::getRotate)); + + // lock rotation to the nearest 180° while driving + oi.getLock180Button() + .onTrue( + new RotateToAngle( + drivetrain, + oi::getTranslateX, + oi::getTranslateY, + () -> + (drivetrain.getPose().getRotation().getDegrees() > -90 + && drivetrain.getPose().getRotation().getDegrees() < 90) + ? 0.0 + : 180.0)); + + // field-relative toggle + oi.getFieldRelativeButton() + .toggleOnTrue( + Commands.either( + Commands.runOnce(drivetrain::disableFieldRelative, drivetrain), + Commands.runOnce(drivetrain::enableFieldRelative, drivetrain), + drivetrain::getFieldRelative)); + + // slow-mode toggle + oi.getTranslationSlowModeButton() + .onTrue(Commands.runOnce(drivetrain::enableTranslationSlowMode, drivetrain)); + oi.getTranslationSlowModeButton() + .onFalse(Commands.runOnce(drivetrain::disableTranslationSlowMode, drivetrain)); + oi.getRotationSlowModeButton() + .onTrue(Commands.runOnce(drivetrain::enableRotationSlowMode, drivetrain)); + oi.getRotationSlowModeButton() + .onFalse(Commands.runOnce(drivetrain::disableRotationSlowMode, drivetrain)); + + // reset gyro to 0 degrees + oi.getResetGyroButton().onTrue(Commands.runOnce(drivetrain::zeroGyroscope, drivetrain)); + + // reset pose based on vision + oi.getResetPoseToVisionButton() + .onTrue( + Commands.runOnce(() -> drivetrain.resetPoseToVision(() -> vision.getBestRobotPose()))); + + // x-stance + oi.getXStanceButton().onTrue(Commands.runOnce(drivetrain::enableXstance, drivetrain)); + oi.getXStanceButton().onFalse(Commands.runOnce(drivetrain::disableXstance, drivetrain)); + + // turbo + oi.getTurboButton().onTrue(Commands.runOnce(drivetrain::enableTurbo, drivetrain)); + oi.getTurboButton().onFalse(Commands.runOnce(drivetrain::disableTurbo, drivetrain)); + } + + private void configureSubsystemCommands() { + // FIXME: add commands for the subsystem + } + + private void configureVisionCommands() { + // enable/disable vision + oi.getVisionIsEnabledSwitch().onTrue(Commands.runOnce(() -> vision.enable(true))); + oi.getVisionIsEnabledSwitch() + .onFalse( + Commands.parallel( + Commands.runOnce(() -> vision.enable(false), vision), + Commands.runOnce(drivetrain::resetPoseRotationToGyro))); } /** @@ -327,4 +522,28 @@ private void configureAutoCommands() { public Command getAutonomousCommand() { return autoChooser.get(); } + + /** + * Check if the alliance color has changed; if so, update the vision subsystem and Field2d + * singleton. + */ + public void checkAllianceColor() { + if (DriverStation.getAlliance() != lastAlliance) { + lastAlliance = DriverStation.getAlliance(); + vision.updateAlliance(lastAlliance); + Field2d.getInstance().updateAlliance(lastAlliance); + } + } + + public void autonomousInit() { + // when the LED subsystem is pulled in, we will change the LEDs here + } + + public void teleopInit() { + // when the LED subsystem is pulled in, we will change the LEDs here + } + + public void disabledPeriodic() { + // when the LED subsystem is pulled in, we will change the LEDs here + } } diff --git a/src/main/java/frc/robot/commands/DriveToPose.java b/src/main/java/frc/robot/commands/DriveToPose.java new file mode 100644 index 00000000..1b22126a --- /dev/null +++ b/src/main/java/frc/robot/commands/DriveToPose.java @@ -0,0 +1,238 @@ +// Copyright (c) 2023 FRC 6328 +// http://github.com/Mechanical-Advantage +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file at +// the root directory of this project. + +// originally from https://github.com/Mechanical-Advantage/RobotCode2023 + +package frc.robot.commands; + +import static frc.robot.Constants.*; + +import edu.wpi.first.math.controller.ProfiledPIDController; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.trajectory.TrapezoidProfile; +import edu.wpi.first.wpilibj.Timer; +import edu.wpi.first.wpilibj2.command.CommandBase; +import frc.lib.team3061.RobotConfig; +import frc.lib.team6328.util.TunableNumber; +import frc.robot.subsystems.drivetrain.Drivetrain; +import java.util.function.Supplier; +import org.littletonrobotics.junction.Logger; + +/** + * This command, when executed, instructs the drivetrain subsystem to drive to the specified pose in + * a straight line. The specified pose is not translated based on the alliance color. The execute + * method invokes the drivetrain subsystem's drive method. For following a predetermined path, refer + * to the FollowPath Command class. For generating a path on the fly and following that path, refer + * to the MoveToPose Command class. + * + *

Requires: the Drivetrain subsystem + * + *

Finished When: the robot is at the specified pose (within the specified tolerances) + * + *

At End: stops the drivetrain + */ +public class DriveToPose extends CommandBase { + private final Drivetrain drivetrain; + private final Supplier poseSupplier; + private Pose2d targetPose; + + private boolean running = false; + private Timer timer; + + private static final TunableNumber driveKp = + new TunableNumber("DriveToPose/DriveKp", RobotConfig.getInstance().getDriveToPoseDriveKP()); + private static final TunableNumber driveKd = + new TunableNumber("DriveToPose/DriveKd", RobotConfig.getInstance().getDriveToPoseDriveKD()); + private static final TunableNumber driveKi = new TunableNumber("DriveToPose/DriveKi", 0); + private static final TunableNumber thetaKp = + new TunableNumber("DriveToPose/ThetaKp", RobotConfig.getInstance().getDriveToPoseThetaKP()); + private static final TunableNumber thetaKd = + new TunableNumber("DriveToPose/ThetaKd", RobotConfig.getInstance().getDriveToPoseThetaKD()); + private static final TunableNumber thetaKi = + new TunableNumber("DriveToPose/ThetaKi", RobotConfig.getInstance().getDriveToPoseThetaKI()); + private static final TunableNumber driveMaxVelocity = + new TunableNumber( + "DriveToPose/DriveMaxVelocity", + RobotConfig.getInstance().getDriveToPoseDriveMaxVelocity()); + private static final TunableNumber driveMaxAcceleration = + new TunableNumber( + "DriveToPose/DriveMaxAcceleration", + RobotConfig.getInstance().getDriveToPoseDriveMaxAcceleration()); + private static final TunableNumber thetaMaxVelocity = + new TunableNumber( + "DriveToPose/ThetaMaxVelocity", + RobotConfig.getInstance().getDriveToPoseTurnMaxVelocity()); + private static final TunableNumber thetaMaxAcceleration = + new TunableNumber( + "DriveToPose/ThetaMaxAcceleration", + RobotConfig.getInstance().getDriveToPoseTurnMaxAcceleration()); + private static final TunableNumber driveTolerance = + new TunableNumber( + "DriveToPose/DriveTolerance", RobotConfig.getInstance().getDriveToPoseDriveTolerance()); + private static final TunableNumber thetaTolerance = + new TunableNumber( + "DriveToPose/ThetaTolerance", RobotConfig.getInstance().getDriveToPoseThetaTolerance()); + private static final TunableNumber timeout = new TunableNumber("DriveToPose/timeout", 2.0); + + private final ProfiledPIDController xController = + new ProfiledPIDController( + driveKp.get(), + driveKi.get(), + driveKd.get(), + new TrapezoidProfile.Constraints(driveMaxVelocity.get(), driveMaxAcceleration.get()), + LOOP_PERIOD_SECS); + private final ProfiledPIDController yController = + new ProfiledPIDController( + driveKp.get(), + driveKi.get(), + driveKd.get(), + new TrapezoidProfile.Constraints(driveMaxVelocity.get(), driveMaxAcceleration.get()), + LOOP_PERIOD_SECS); + private final ProfiledPIDController thetaController = + new ProfiledPIDController( + thetaKp.get(), + thetaKi.get(), + thetaKd.get(), + new TrapezoidProfile.Constraints(thetaMaxVelocity.get(), thetaMaxAcceleration.get()), + LOOP_PERIOD_SECS); + + /** + * Constructs a new DriveToPose command that drives the robot in a straight line to the specified + * pose. A pose supplier is specified instead of a pose since the target pose may not be known + * when this command is created. + * + * @param drivetrain the drivetrain subsystem required by this command + * @param poseSupplier a supplier that returns the pose to drive to + */ + public DriveToPose(Drivetrain drivetrain, Supplier poseSupplier) { + this.drivetrain = drivetrain; + this.poseSupplier = poseSupplier; + this.timer = new Timer(); + addRequirements(drivetrain); + thetaController.enableContinuousInput(-Math.PI, Math.PI); + } + + /** + * This method is invoked once when this command is scheduled. It resets all the PID controllers + * and initializes the current and target poses. It is critical that this initialization occurs in + * this method and not the constructor as this object is constructed well before the command is + * scheduled and the robot's pose will definitely have changed and the target pose may not be + * known until this command is scheduled. + */ + @Override + public void initialize() { + Logger.getInstance().recordOutput("ActiveCommands/DriveToPose", true); + + // Reset all controllers + Pose2d currentPose = drivetrain.getPose(); + xController.reset(currentPose.getX()); + yController.reset(currentPose.getY()); + thetaController.reset(currentPose.getRotation().getRadians()); + xController.setTolerance(driveTolerance.get()); + yController.setTolerance(driveTolerance.get()); + thetaController.setTolerance(thetaTolerance.get()); + this.targetPose = poseSupplier.get(); + + Logger.getInstance().recordOutput("DriveToPose/targetPose", targetPose); + + this.timer.restart(); + } + + /** + * This method is invoked periodically while this command is scheduled. It calculates the + * velocities based on the current and target poses and invokes the drivetrain subsystem's drive + * method. + */ + @Override + public void execute() { + // set running to true in this method to capture that the calculate method has been invoked on + // the PID controllers. This is important since these controllers will return true for atGoal if + // the calculate method has not yet been invoked. + running = true; + + // Update from tunable numbers + if (driveKp.hasChanged() + || driveKd.hasChanged() + || driveKi.hasChanged() + || thetaKp.hasChanged() + || thetaKd.hasChanged() + || thetaKi.hasChanged() + || driveMaxVelocity.hasChanged() + || driveMaxAcceleration.hasChanged() + || thetaMaxVelocity.hasChanged() + || thetaMaxAcceleration.hasChanged() + || driveTolerance.hasChanged() + || thetaTolerance.hasChanged()) { + xController.setP(driveKp.get()); + xController.setD(driveKd.get()); + xController.setI(driveKi.get()); + xController.setConstraints( + new TrapezoidProfile.Constraints(driveMaxVelocity.get(), driveMaxAcceleration.get())); + xController.setTolerance(driveTolerance.get()); + yController.setP(driveKp.get()); + yController.setD(driveKd.get()); + yController.setI(driveKi.get()); + yController.setConstraints( + new TrapezoidProfile.Constraints(driveMaxVelocity.get(), driveMaxAcceleration.get())); + yController.setTolerance(driveTolerance.get()); + thetaController.setP(thetaKp.get()); + thetaController.setD(thetaKd.get()); + thetaController.setI(thetaKi.get()); + thetaController.setConstraints( + new TrapezoidProfile.Constraints(thetaMaxVelocity.get(), thetaMaxAcceleration.get())); + thetaController.setTolerance(thetaTolerance.get()); + } + + Pose2d currentPose = drivetrain.getPose(); + + double xVelocity = xController.calculate(currentPose.getX(), this.targetPose.getX()); + double yVelocity = yController.calculate(currentPose.getY(), this.targetPose.getY()); + double thetaVelocity = + thetaController.calculate( + currentPose.getRotation().getRadians(), this.targetPose.getRotation().getRadians()); + if (xController.atGoal()) xVelocity = 0.0; + if (yController.atGoal()) yVelocity = 0.0; + if (thetaController.atGoal()) thetaVelocity = 0.0; + + drivetrain.drive(xVelocity, yVelocity, thetaVelocity, true, true); + } + + /** + * This method returns true if the command has finished. It is invoked periodically while this + * command is scheduled (after execute is invoked). This command is considered finished if the + * move-to-pose feature is disabled on the drivetrain subsystem or if the timeout has elapsed or + * if all the PID controllers are at their goal. + * + * @return true if the command has finished + */ + @Override + public boolean isFinished() { + Logger.getInstance().recordOutput("DriveToPose/xErr", xController.atGoal()); + Logger.getInstance().recordOutput("DriveToPose/yErr", yController.atGoal()); + Logger.getInstance().recordOutput("DriveToPose/tErr", thetaController.atGoal()); + + // check that running is true (i.e., the calculate method has been invoked on the PID + // controllers) and that each of the controllers is at their goal. This is important since these + // controllers will return true for atGoal if the calculate method has not yet been invoked. + return !drivetrain.isMoveToPoseEnabled() + || this.timer.hasElapsed(timeout.get()) + || (running && xController.atGoal() && yController.atGoal() && thetaController.atGoal()); + } + + /** + * This method will be invoked when this command finishes or is interrupted. It stops the motion + * of the drivetrain. + * + * @param interrupted true if the command was interrupted by another command being scheduled + */ + @Override + public void end(boolean interrupted) { + drivetrain.stop(); + running = false; + Logger.getInstance().recordOutput("ActiveCommands/DriveToPose", false); + } +} diff --git a/src/main/java/frc/robot/commands/FollowPath.java b/src/main/java/frc/robot/commands/FollowPath.java index 1bafad4f..6c5b27fd 100644 --- a/src/main/java/frc/robot/commands/FollowPath.java +++ b/src/main/java/frc/robot/commands/FollowPath.java @@ -1,7 +1,9 @@ package frc.robot.commands; import com.pathplanner.lib.PathPlannerTrajectory; +import com.pathplanner.lib.PathPlannerTrajectory.PathPlannerState; import com.pathplanner.lib.commands.PPSwerveControllerCommand; +import edu.wpi.first.wpilibj.DriverStation; import frc.lib.team3061.RobotConfig; import frc.robot.subsystems.drivetrain.Drivetrain; import org.littletonrobotics.junction.Logger; @@ -21,6 +23,7 @@ public class FollowPath extends PPSwerveControllerCommand { private Drivetrain drivetrain; private PathPlannerTrajectory trajectory; private boolean initialPath; + private boolean useAllianceColor; /** * Constructs a new FollowPath object. @@ -32,8 +35,15 @@ public class FollowPath extends PPSwerveControllerCommand { * of trajectory; false, if this trajectory is a subsequent trajectory in which case the gyro * and odometry will not be re-initialized in order to ensure a smooth transition between * trajectories + * @param useAllianceColor if true, the path states will be automatically transformed based on + * alliance color. In order for this to work properly, you MUST create your path on the blue + * side of the field. */ - public FollowPath(PathPlannerTrajectory trajectory, Drivetrain subsystem, boolean initialPath) { + public FollowPath( + PathPlannerTrajectory trajectory, + Drivetrain subsystem, + boolean initialPath, + boolean useAllianceColor) { super( trajectory, subsystem::getPose, @@ -42,11 +52,13 @@ public FollowPath(PathPlannerTrajectory trajectory, Drivetrain subsystem, boolea subsystem.getAutoYController(), subsystem.getAutoThetaController(), subsystem::setSwerveModuleStates, + useAllianceColor, subsystem); this.drivetrain = subsystem; this.trajectory = trajectory; this.initialPath = initialPath; + this.useAllianceColor = useAllianceColor; } /** @@ -59,11 +71,19 @@ public FollowPath(PathPlannerTrajectory trajectory, Drivetrain subsystem, boolea */ @Override public void initialize() { + Logger.getInstance().recordOutput("ActiveCommands/FollowPath", true); + super.initialize(); + // reset odometry to the starting pose of the trajectory if (initialPath) { - // reset odometry to the starting pose of the trajectory - this.drivetrain.resetOdometry(this.trajectory.getInitialState()); + PathPlannerState initialState = this.trajectory.getInitialState(); + if (this.useAllianceColor) { + initialState = + PathPlannerTrajectory.transformStateForAlliance( + initialState, DriverStation.getAlliance()); + } + this.drivetrain.resetOdometry(initialState); } // reset the theta controller such that old accumulated ID values aren't used with the new path @@ -85,5 +105,6 @@ public void initialize() { public void end(boolean interrupted) { this.drivetrain.stop(); super.end(interrupted); + Logger.getInstance().recordOutput("ActiveCommands/FollowPath", false); } } diff --git a/src/main/java/frc/robot/commands/MoveToPose.java b/src/main/java/frc/robot/commands/MoveToPose.java new file mode 100644 index 00000000..abf6533e --- /dev/null +++ b/src/main/java/frc/robot/commands/MoveToPose.java @@ -0,0 +1,217 @@ +package frc.robot.commands; + +import com.pathplanner.lib.PathConstraints; +import com.pathplanner.lib.PathPlannerTrajectory; +import com.pathplanner.lib.commands.PPSwerveControllerCommand; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.wpilibj2.command.CommandBase; +import frc.lib.team3061.RobotConfig; +import frc.lib.team6328.util.Alert; +import frc.lib.team6328.util.Alert.AlertType; +import frc.lib.team6328.util.TunableNumber; +import frc.robot.Field2d; +import frc.robot.subsystems.drivetrain.Drivetrain; +import java.util.function.Supplier; +import org.littletonrobotics.junction.Logger; + +/** + * This command, when executed, generates a trajectory from the current pose (projected into the + * future to account for the average latency introduced by generating the trajectory) of the robot + * to a specified pose on the field and instructs the drivetrain subsystem to follow the specified + * trajectory using a PPSwerveControllerCommand object. The specified pose will be translated based + * on the alliance color. The initial speed will match the current speed and the final speed will + * match the speed returned by the RobotConfig's getStallAgainstElementVelocity method. The + * PPSwerveControllerCommand contained by this command will never be scheduled; this command invoke + * each of the command methods at the appropriate time. + * + *

For following a predetermined trajectory, refer to the FollowPath Command class. For driving + * in a straight line to a pose, refer to the DriveToPose Command class. + * + *

Requires: the Drivetrain subsystem + * + *

Finished When: trajectory is null or time of specified trajectory has elapsed (from + * PPSwerveControllerCommand object) + * + *

At End: stops the drivetrain, sets trajectory to null + */ +public class MoveToPose extends CommandBase { + private Supplier endPoseSupplier; + private Drivetrain drivetrain; + private double minTrajectoryTraversalTime; + private PathPlannerTrajectory trajectory; + private PPSwerveControllerCommand ppSwerveControllerCommand; + private Alert noTrajectoryAlert = + new Alert("No trajectory between start and end pose", AlertType.WARNING); + private final TunableNumber timeOffset = new TunableNumber("MoveToPose/TIME_OFFSET", 0.1); + + /** + * Constructs a new MoveToPose command object. A pose supplier is specified instead of a pose + * since the target pose may not be known when this command is created. + * + * @param subsystem the drivetrain subsystem required by this command + * @param endPoseSupplier a supplier of the end pose of the trajectory + */ + public MoveToPose(Drivetrain subsystem, Supplier endPoseSupplier) { + this(subsystem, endPoseSupplier, 0); + } + + /** + * Constructs a new MoveToPose command object. A pose supplier is specified instead of a pose + * since the target pose may not be known when this command is created. The generated trajectory + * will be constrained such that the time to traverse the trajectory is at least the specified + * minimum time. This is useful for coordinating this command with the motion of a mechanism. + * + * @param subsystem + * @param endPoseSupplier + * @param minTrajectoryTraversalTime + */ + public MoveToPose( + Drivetrain subsystem, Supplier endPoseSupplier, double minTrajectoryTraversalTime) { + this.endPoseSupplier = endPoseSupplier; + this.drivetrain = subsystem; + this.minTrajectoryTraversalTime = minTrajectoryTraversalTime; + + addRequirements(subsystem); + } + + @Override + public void initialize() { + Logger.getInstance().recordOutput("ActiveCommands/MoveToPose", true); + + // reset the theta controller such that old accumulated ID values aren't used with the new + // trajectory + // this doesn't matter if only the P value is non-zero, which is the current behavior + this.drivetrain.getAutoXController().reset(); + this.drivetrain.getAutoYController().reset(); + this.drivetrain.getAutoThetaController().reset(); + + double beforeCalc = Logger.getInstance().getRealTimestamp(); + + Pose2d endPose = endPose(); + + // calculate the starting pose by projecting the current pose forward in time (assuming the + // robot is not rotating) + Pose2d startingPose = + new Pose2d( + this.drivetrain.getPose().getX() + this.drivetrain.getVelocityX() * timeOffset.get(), + this.drivetrain.getPose().getY() + this.drivetrain.getVelocityY() * timeOffset.get(), + this.drivetrain.getPose().getRotation()); + + // set the maximum velocity to accommodate the minimum trajectory traversal time + double distance = endPose.minus(this.drivetrain.getPose()).getTranslation().getNorm(); + double maxVelocity = RobotConfig.getInstance().getAutoMaxSpeed(); + if (minTrajectoryTraversalTime != 0) { + maxVelocity = distance / minTrajectoryTraversalTime; + } + maxVelocity = Math.min(maxVelocity, RobotConfig.getInstance().getAutoMaxSpeed()); + + // the Field2d singleton generates the trajectory from the poses and constraints + this.trajectory = + Field2d.getInstance() + .makePath( + startingPose, + endPose, + new PathConstraints( + maxVelocity, RobotConfig.getInstance().getAutoMaxAcceleration()), + this.drivetrain); + + noTrajectoryAlert.set(this.trajectory == null); + + if (this.trajectory != null) { + // create the wrapped PPSwerveControllerCommand object and invoke its initialize method since + // it will never be scheduled + this.ppSwerveControllerCommand = + new PPSwerveControllerCommand( + this.trajectory, + this.drivetrain::getPose, + RobotConfig.getInstance().getSwerveDriveKinematics(), + this.drivetrain.getAutoXController(), + this.drivetrain.getAutoYController(), + this.drivetrain.getAutoThetaController(), + this.drivetrain::setSwerveModuleStates); + this.ppSwerveControllerCommand.initialize(); + + double afterCalc = Logger.getInstance().getRealTimestamp(); + Logger.getInstance().recordOutput("Odometry/trajectoryCalcTime", afterCalc - beforeCalc); + + Logger.getInstance().recordOutput("Odometry/trajectory", trajectory); + } + } + + /** + * This method is invoked periodically while this command is scheduled. It executes the contained + * PPSwerveControllerCommand object. + */ + @Override + public void execute() { + if (this.ppSwerveControllerCommand != null) { + this.ppSwerveControllerCommand.execute(); + } + } + + /** + * This method returns true if the command has finished. It is invoked periodically while this + * command is scheduled (after execute is invoked). This command is considered finished if the + * move-to-pose feature is disabled on the drivetrain subsystem, if a trajectory failed to be + * generated, or if the contained PPSwerveControllerCommand command is finished. + * + * @return true if the command has finished + */ + @Override + public boolean isFinished() { + return !drivetrain.isMoveToPoseEnabled() + || this.trajectory == null + || ppSwerveControllerCommand.isFinished(); + } + + /** + * This method will be invoked when this command finishes or is interrupted. It stops the motion + * of the drivetrain if a trajectory was successfully created. + * + * @param interrupted true if the command was interrupted by another command being scheduled + */ + @Override + public void end(boolean interrupted) { + if (this.ppSwerveControllerCommand != null) { + this.ppSwerveControllerCommand.end(interrupted); + } + + if (this.trajectory != null) { + this.drivetrain.stop(); + } + this.trajectory = null; + this.ppSwerveControllerCommand = null; + + Logger.getInstance().recordOutput("ActiveCommands/MoveToPose", false); + } + + /** + * Returns the total time in seconds of the trajectory. + * + * @return the total time in seconds of the trajectory + */ + public double getTotalTimeSeconds() { + if (this.trajectory != null) { + return this.trajectory.getTotalTimeSeconds(); + } else { + return 0; + } + } + + /** + * Calculates the end pose based on the pose from the pose supplier. The end pose is mapped based + * on the alliance color. + * + *

This method can be overridden to translate the end pose for a specific scenario (e.g., + * account for the robot width). + */ + protected Pose2d endPose() { + Pose2d endPose = endPoseSupplier.get(); + + Pose2d translatedEndPose = Field2d.getInstance().mapPoseToCurrentAlliance(endPose); + + Logger.getInstance().recordOutput("MoveToPose/endPose", translatedEndPose); + + return translatedEndPose; + } +} diff --git a/src/main/java/frc/robot/commands/RotateToAngle.java b/src/main/java/frc/robot/commands/RotateToAngle.java new file mode 100644 index 00000000..9925e7e3 --- /dev/null +++ b/src/main/java/frc/robot/commands/RotateToAngle.java @@ -0,0 +1,172 @@ +package frc.robot.commands; + +import static frc.robot.Constants.*; + +import edu.wpi.first.math.controller.ProfiledPIDController; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.trajectory.TrapezoidProfile; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.wpilibj2.command.CommandBase; +import frc.lib.team3061.RobotConfig; +import frc.lib.team6328.util.TunableNumber; +import frc.robot.subsystems.drivetrain.Drivetrain; +import java.util.function.DoubleSupplier; +import org.littletonrobotics.junction.Logger; + +/** + * This command, when executed, instructs the drivetrain subsystem to rotate to the specified angle + * while driving based on the supplied x and y values (e.g., from a joystick). The execute method + * invokes the drivetrain subsystem's drive method. + * + *

Requires: the Drivetrain subsystem + * + *

Finished When: the robot is at the specified angle (within the specified tolerances) + * + *

At End: nothing (the drivetrain is left in whatever state it was in when the command finished) + */ +public class RotateToAngle extends CommandBase { + private final Drivetrain drivetrain; + private final DoubleSupplier targetAngleSupplier; + private final DoubleSupplier translationXSupplier; + private final DoubleSupplier translationYSupplier; + + protected static final TunableNumber thetaKp = new TunableNumber("RotateToAngle/ThetaKp", 2); + protected static final TunableNumber thetaKi = new TunableNumber("RotateToAngle/ThetaKi", 10.0); + protected static final TunableNumber thetaKd = new TunableNumber("RotateToAngle/ThetaKd", 0.1); + protected static final TunableNumber thetaMaxVelocity = + new TunableNumber("RotateToAngle/ThetaMaxVelocity", 8); + protected static final TunableNumber thetaMaxAcceleration = + new TunableNumber("RotateToAngle/ThetaMaxAcceleration", 100); + protected static final TunableNumber thetaTolerance = + new TunableNumber("RotateToAngle/ThetaTolerance", 0.008); + + protected final ProfiledPIDController thetaController = + new ProfiledPIDController( + thetaKp.get(), + thetaKi.get(), + thetaKd.get(), + new TrapezoidProfile.Constraints(thetaMaxVelocity.get(), thetaMaxAcceleration.get()), + LOOP_PERIOD_SECS); + + /** + * Constructs a new RotateToAngle command that, when executed, instructs the drivetrain subsystem + * to rotate to the specified angle in place. + * + * @param drivetrain the drivetrain subsystem required by this command + * @param targetAngleSupplier the supplier of the target angle, in degrees. Zero degrees is away + * from the driver and increases in the CCW direction. + */ + public RotateToAngle(Drivetrain drivetrain, DoubleSupplier targetAngleSupplier) { + this(drivetrain, () -> 0, () -> 0, targetAngleSupplier); + } + + /** + * Constructs a new RotateToAngle command that, when executed, instructs the drivetrain subsystem + * to rotate to the specified angle while driving based on the supplied x and y values (e.g., from + * a joystick). + * + * @param drivetrain the drivetrain subsystem required by this command + * @param translationXSupplier the supplier of the x value as a percentage of the maximum velocity + * in the x direction as defined by the standard field or robot coordinate system + * @param translationYSupplier the supplier of the y value as a percentage of the maximum velocity + * in the y direction as defined by the standard field or robot coordinate system + * @param targetAngleSupplier the supplier of the target angle, in degrees. Zero degrees is away + * from the driver and increases in the CCW direction. + */ + public RotateToAngle( + Drivetrain drivetrain, + DoubleSupplier translationXSupplier, + DoubleSupplier translationYSupplier, + DoubleSupplier targetAngleSupplier) { + this.drivetrain = drivetrain; + addRequirements(drivetrain); + this.translationXSupplier = translationXSupplier; + this.translationYSupplier = translationYSupplier; + this.targetAngleSupplier = targetAngleSupplier; + } + + /** + * This method is invoked once when this command is scheduled. It resets all the PID controller + * and queries the target angle. It is critical that this initialization occurs in this method and + * not the constructor as this object is constructed well before the command is scheduled and the + * robot's pose will definitely have changed and the target angle may not be known until this + * command is scheduled. + */ + @Override + public void initialize() { + Logger.getInstance().recordOutput("ActiveCommands/RotateToAngle", true); + + Pose2d currentPose = drivetrain.getPose(); + thetaController.reset(currentPose.getRotation().getRadians()); + thetaController.setTolerance(thetaTolerance.get()); + + // configure the controller such that the range of values is centered on the target angle + thetaController.enableContinuousInput( + Units.degreesToRadians(this.targetAngleSupplier.getAsDouble()) - Math.PI, + Units.degreesToRadians(this.targetAngleSupplier.getAsDouble()) + Math.PI); + + Logger.getInstance().recordOutput("RotateToAngle/AngleDeg", targetAngleSupplier.getAsDouble()); + } + + /** + * This method is invoked periodically while this command is scheduled. It calculates the + * velocities based on the current and target rotation and invokes the drivetrain subsystem's + * drive method. + */ + @Override + public void execute() { + // update from tunable numbers + if (thetaKp.hasChanged() + || thetaKd.hasChanged() + || thetaKi.hasChanged() + || thetaMaxVelocity.hasChanged() + || thetaMaxAcceleration.hasChanged() + || thetaTolerance.hasChanged()) { + thetaController.setP(thetaKp.get()); + thetaController.setI(thetaKi.get()); + thetaController.setD(thetaKd.get()); + thetaController.setConstraints( + new TrapezoidProfile.Constraints(thetaMaxVelocity.get(), thetaMaxAcceleration.get())); + thetaController.setTolerance(thetaTolerance.get()); + } + + Pose2d currentPose = drivetrain.getPose(); + double thetaVelocity = + thetaController.calculate( + currentPose.getRotation().getRadians(), + Units.degreesToRadians(this.targetAngleSupplier.getAsDouble())); + if (thetaController.atGoal()) { + thetaVelocity = 0.0; + } + double xPercentage = TeleopSwerve.modifyAxis(translationXSupplier.getAsDouble(), 2.0); + double yPercentage = TeleopSwerve.modifyAxis(translationYSupplier.getAsDouble(), 2.0); + + double xVelocity = xPercentage * RobotConfig.getInstance().getRobotMaxVelocity(); + double yVelocity = yPercentage * RobotConfig.getInstance().getRobotMaxVelocity(); + + drivetrain.drive(xVelocity, yVelocity, thetaVelocity, true, false); + } + + /** + * This method returns true if the command has finished. It is invoked periodically while this + * command is scheduled (after execute is invoked). This command is considered finished if the + * rotational controller is at its goal. + * + * @return true if the command has finished + */ + @Override + public boolean isFinished() { + return thetaController.atGoal(); + } + + /** + * This method will be invoked when this command finishes or is interrupted. The drivetrain is + * left in whatever state it was in when the command finished. + * + * @param interrupted true if the command was interrupted by another command being scheduled + */ + @Override + public void end(boolean interrupted) { + Logger.getInstance().recordOutput("ActiveCommands/RotateToAngle", false); + } +} diff --git a/src/main/java/frc/robot/commands/TeleopSwerve.java b/src/main/java/frc/robot/commands/TeleopSwerve.java index d164e7c6..f5941d7e 100644 --- a/src/main/java/frc/robot/commands/TeleopSwerve.java +++ b/src/main/java/frc/robot/commands/TeleopSwerve.java @@ -2,13 +2,17 @@ import edu.wpi.first.wpilibj2.command.CommandBase; import frc.lib.team3061.RobotConfig; +import frc.lib.team6328.util.TunableNumber; +import frc.robot.Constants; import frc.robot.subsystems.drivetrain.Drivetrain; import java.util.function.DoubleSupplier; import org.littletonrobotics.junction.Logger; /** * This command, when executed, instructs the drivetrain subsystem to drive based on the specified - * values from the controller(s). This command is designed to be the default command for the + * values from the controller(s) and enabled features. Configurable deadband and power function are + * applied to the controller inputs. If the robot isn't in "turbo" mode, the acceleration is limited + * based on the configurable constraints. This command is designed to be the default command for the * drivetrain subsystem. * *

Requires: the Drivetrain subsystem @@ -24,12 +28,25 @@ public class TeleopSwerve extends CommandBase { private final DoubleSupplier translationYSupplier; private final DoubleSupplier rotationSupplier; - public static final double DEADBAND = 0.1; + private static final double DEADBAND = 0.1; + private double lastAngularVelocity; + private double lastXVelocity; + private double lastYVelocity; private final double maxVelocityMetersPerSecond = RobotConfig.getInstance().getRobotMaxVelocity(); private final double maxAngularVelocityRadiansPerSecond = RobotConfig.getInstance().getRobotMaxAngularVelocity(); + private final TunableNumber maxTurnAcceleration = + new TunableNumber( + "TeleopSwerve/maxTurnAcceleration", + RobotConfig.getInstance().getRobotMaxTurnAcceleration()); + private final TunableNumber joystickPower = new TunableNumber("TeleopSwerve/joystickPower", 2.0); + private final TunableNumber maxDriveAcceleration = + new TunableNumber( + "TeleopSwerve/maxDriveAcceleration", + RobotConfig.getInstance().getRobotMaxDriveAcceleration()); + /** * Create a new TeleopSwerve command object. * @@ -54,33 +71,76 @@ public TeleopSwerve( addRequirements(drivetrain); } + @Override + public void initialize() { + Logger.getInstance().recordOutput("ActiveCommands/TeleopSwerve", true); + } + + /** + * This method is invoked periodically while this command is scheduled. It calculates the + * velocities based on the supplied values and enabled features and invokes the drivetrain + * subsystem's drive method. + */ @Override public void execute() { - // invert the controller input and apply the deadband and squaring to make the robot more - // responsive to small changes in the controller - double xPercentage = modifyAxis(translationXSupplier.getAsDouble()); - double yPercentage = modifyAxis(translationYSupplier.getAsDouble()); - double rotationPercentage = modifyAxis(rotationSupplier.getAsDouble()); + // invert the controller input and apply the deadband and exponential function to make the robot + // more responsive to small changes in the controller + double xPercentage = modifyAxis(translationXSupplier.getAsDouble(), joystickPower.get()); + double yPercentage = modifyAxis(translationYSupplier.getAsDouble(), joystickPower.get()); + double rotationPercentage = modifyAxis(rotationSupplier.getAsDouble(), joystickPower.get()); double xVelocity = xPercentage * maxVelocityMetersPerSecond; double yVelocity = yPercentage * maxVelocityMetersPerSecond; double rotationalVelocity = rotationPercentage * maxAngularVelocityRadiansPerSecond; - Logger.getInstance().recordOutput("ActiveCommands/TeleopSwerve", true); Logger.getInstance().recordOutput("TeleopSwerve/xVelocity", xVelocity); Logger.getInstance().recordOutput("TeleopSwerve/yVelocity", yVelocity); Logger.getInstance().recordOutput("TeleopSwerve/rotationalVelocity", rotationalVelocity); - drivetrain.drive(xVelocity, yVelocity, rotationalVelocity, true); + // if the robot is not in turbo mode, limit the acceleration + if (!drivetrain.getTurbo()) { + double driveAccelerationMetersPer20Ms = + maxDriveAcceleration.get() * Constants.LOOP_PERIOD_SECS; + + if (Math.abs(xVelocity - lastXVelocity) > driveAccelerationMetersPer20Ms) { + xVelocity = + lastXVelocity + + Math.copySign(driveAccelerationMetersPer20Ms, xVelocity - lastXVelocity); + } + + if (Math.abs(yVelocity - lastYVelocity) > driveAccelerationMetersPer20Ms) { + yVelocity = + lastYVelocity + + Math.copySign(driveAccelerationMetersPer20Ms, yVelocity - lastYVelocity); + } + + double turnAccelerationRadiansPer20Ms = + maxTurnAcceleration.get() * Constants.LOOP_PERIOD_SECS; + + if (Math.abs(rotationalVelocity - lastAngularVelocity) > turnAccelerationRadiansPer20Ms) { + rotationalVelocity = + lastAngularVelocity + + Math.copySign( + turnAccelerationRadiansPer20Ms, rotationalVelocity - lastAngularVelocity); + } + } + + lastXVelocity = xVelocity; + lastYVelocity = yVelocity; + lastAngularVelocity = rotationalVelocity; + + drivetrain.drive(xVelocity, yVelocity, rotationalVelocity, true, false); } + /** + * This method will be invoked when this command finishes or is interrupted. The drivetrain is + * left in whatever state it was in when the command finished. + * + * @param interrupted true if the command was interrupted by another command being scheduled + */ @Override public void end(boolean interrupted) { - this.drivetrain.stop(); - - super.end(interrupted); - Logger.getInstance().recordOutput("ActiveCommands/TeleopSwerve", false); } @@ -88,17 +148,14 @@ public void end(boolean interrupted) { * Squares the specified value, while preserving the sign. This method is used on all joystick * inputs. This is useful as a non-linear range is more natural for the driver. * - * @param value - * @return + * @param value the raw controller value + * @param power the power to which the value should be raised + * @return the modified value */ - private static double modifyAxis(double value) { - // Deadband - value = deadband(value, DEADBAND); - - // Square the axis - value = Math.copySign(value * value, value); - - return value; + public static double modifyAxis(double value, double power) { + double modifiedValue = deadband(value, DEADBAND); + modifiedValue = Math.copySign(Math.pow(modifiedValue, power), modifiedValue); + return modifiedValue; } private static double deadband(double value, double deadband) { diff --git a/src/main/java/frc/robot/configs/DefaultRobotConfig.java b/src/main/java/frc/robot/configs/DefaultRobotConfig.java index 0dc03087..e85146e1 100644 --- a/src/main/java/frc/robot/configs/DefaultRobotConfig.java +++ b/src/main/java/frc/robot/configs/DefaultRobotConfig.java @@ -72,7 +72,7 @@ public class DefaultRobotConfig extends RobotConfig { private static final String CAN_BUS_NAME = ""; // FIXME: specify the name of the camera used for detecting AprilTags - private static final String CAMERA_NAME = "ov9268"; + private static final String CAMERA_NAME = "OV9281"; // FIXME: update this with the actual transform from the robot to the camera private static final Transform3d ROBOT_TO_CAMERA = @@ -221,8 +221,8 @@ public double getRobotLengthWithBumpers() { } @Override - public Transform3d getRobotToCameraTransform() { - return ROBOT_TO_CAMERA; + public Transform3d[] getRobotToCameraTransforms() { + return new Transform3d[] {ROBOT_TO_CAMERA}; } @Override @@ -281,8 +281,8 @@ public String getCANBusName() { } @Override - public String getCameraName() { - return CAMERA_NAME; + public String[] getCameraNames() { + return new String[] {CAMERA_NAME}; } @Override diff --git a/src/main/java/frc/robot/configs/MK4IRobotConfig.java b/src/main/java/frc/robot/configs/MK4IRobotConfig.java index 76d23b28..48eab3f7 100644 --- a/src/main/java/frc/robot/configs/MK4IRobotConfig.java +++ b/src/main/java/frc/robot/configs/MK4IRobotConfig.java @@ -12,39 +12,35 @@ */ public class MK4IRobotConfig extends RobotConfig { - // FIXME: update all CAN IDs and steer offsets - private static final int FRONT_LEFT_MODULE_DRIVE_MOTOR = 7; - private static final int FRONT_LEFT_MODULE_STEER_MOTOR = 6; - private static final int FRONT_LEFT_MODULE_STEER_ENCODER = 8; - private static final double FRONT_LEFT_MODULE_STEER_OFFSET = 118.0371; - - private static final int FRONT_RIGHT_MODULE_DRIVE_MOTOR = 13; - private static final int FRONT_RIGHT_MODULE_STEER_MOTOR = 12; - private static final int FRONT_RIGHT_MODULE_STEER_ENCODER = 14; - private static final double FRONT_RIGHT_MODULE_STEER_OFFSET = 102.9968; - - private static final int BACK_LEFT_MODULE_DRIVE_MOTOR = 10; - private static final int BACK_LEFT_MODULE_STEER_MOTOR = 9; - private static final int BACK_LEFT_MODULE_STEER_ENCODER = 11; - private static final double BACK_LEFT_MODULE_STEER_OFFSET = -189.7051; - - private static final int BACK_RIGHT_MODULE_DRIVE_MOTOR = 16; - private static final int BACK_RIGHT_MODULE_STEER_MOTOR = 15; - private static final int BACK_RIGHT_MODULE_STEER_ENCODER = 17; - private static final double BACK_RIGHT_MODULE_STEER_OFFSET = 40.3335; + private static final int FRONT_LEFT_MODULE_DRIVE_MOTOR = 13; + private static final int FRONT_LEFT_MODULE_STEER_MOTOR = 12; + private static final int FRONT_LEFT_MODULE_STEER_ENCODER = 14; + private static final double FRONT_LEFT_MODULE_STEER_OFFSET = 37.35; + + private static final int FRONT_RIGHT_MODULE_DRIVE_MOTOR = 16; + private static final int FRONT_RIGHT_MODULE_STEER_MOTOR = 15; + private static final int FRONT_RIGHT_MODULE_STEER_ENCODER = 17; + private static final double FRONT_RIGHT_MODULE_STEER_OFFSET = 83.49; + + private static final int BACK_LEFT_MODULE_DRIVE_MOTOR = 7; + private static final int BACK_LEFT_MODULE_STEER_MOTOR = 6; + private static final int BACK_LEFT_MODULE_STEER_ENCODER = 8; + private static final double BACK_LEFT_MODULE_STEER_OFFSET = 259.62; + + private static final int BACK_RIGHT_MODULE_DRIVE_MOTOR = 10; + private static final int BACK_RIGHT_MODULE_STEER_MOTOR = 9; + private static final int BACK_RIGHT_MODULE_STEER_ENCODER = 11; + private static final double BACK_RIGHT_MODULE_STEER_OFFSET = 9.14; private static final int GYRO_ID = 18; - // FIXME: update robot dimensions - private static final double TRACKWIDTH_METERS = 0.5715; // 22.5 inches - private static final double WHEELBASE_METERS = 0.5969; // 23.5 inches - private static final double ROBOT_WIDTH_WITH_BUMPERS = 0.89; // meters - private static final double ROBOT_LENGTH_WITH_BUMPERS = 0.91; // meters - - // FIXME: tune PID values for the angle and drive motors for the swerve modules + private static final double TRACKWIDTH_METERS = 0.574675; // 22.625 inches + private static final double WHEELBASE_METERS = 0.619125; // 24.375 inches + private static final double ROBOT_WIDTH_WITH_BUMPERS = 0.8255; // meters + private static final double ROBOT_LENGTH_WITH_BUMPERS = 0.8255; // meters /* Angle Motor PID Values */ - private static final double ANGLE_KP = 0.6; + private static final double ANGLE_KP = 0.4; private static final double ANGLE_KI = 0.0; private static final double ANGLE_KD = 12.0; private static final double ANGLE_KF = 0.0; @@ -52,48 +48,51 @@ public class MK4IRobotConfig extends RobotConfig { /* Drive Motor PID Values */ private static final double DRIVE_KP = 0.10; private static final double DRIVE_KI = 0.0; - private static final double DRIVE_KD = 0.0; + private static final double DRIVE_KD = 5.5; private static final double DRIVE_KF = 0.0; - // FIXME: characterize the drivetrain and update these constants - private static final double DRIVE_KS = 0.55493; - private static final double DRIVE_KV = 2.3014; + private static final double DRIVE_KS = 0.25988; + private static final double DRIVE_KV = 2.46330; private static final double DRIVE_KA = 0.12872; - // FIXME: specify the type of swerve module (MK4 and MK4i are supported) private static final SwerveType SWERVE_TYPE = SwerveType.MK4I; - // FIXME: determine maximum velocities empirically - private static final double MAX_VELOCITY_METERS_PER_SECOND = 4.25; + private static final double MAX_VELOCITY_METERS_PER_SECOND = 4.78; private static final double MAX_COAST_VELOCITY_METERS_PER_SECOND = 0.05; + private static final double SLOW_MODE_MULTIPLIER = 0.75; + + private static final double MAX_DRIVE_ACCELERATION_METERS_PER_SECOND_SQUARED = 4.0; + private static final double MAX_TURN_ACCELERATION_RADIANS_PER_SECOND_SQUARED = 2.0 * Math.PI; - // FIXME: specify the name of the CANivore CAN FD bus as appropriate (an empty string uses the - // default CAN bus) - private static final String CAN_BUS_NAME = ""; + private static final String CAN_BUS_NAME = "canbus1"; - // FIXME: specify the name of the camera used for detecting AprilTags - private static final String CAMERA_NAME = "ov9268"; + private static final String CAMERA_NAME = "OV9281"; - // FIXME: update this with the actual transform from the robot to the camera private static final Transform3d ROBOT_TO_CAMERA = - new Transform3d(new Translation3d(0, 0, 0), new Rotation3d(0, 0, 0)); + new Transform3d(new Translation3d(0.356, 0.0, 0.465), new Rotation3d(0, 0, 0)); - // FIXME: specify the configuration for pneumatics private static final int PNEUMATICS_HUB_ID = 20; private static final int FLOW_SENSOR_CHANNEL = 0; private static final int REV_HIGH_PRESSURE_SENSOR_CHANNEL = 0; private static final int REV_LOW_PRESSURE_SENSOR_CHANNEL = 1; - // FIXME: specify maximum velocity and acceleration and tune PID values for auto paths - private static final double AUTO_MAX_SPEED_METERS_PER_SECOND = 2.0; private static final double AUTO_MAX_ACCELERATION_METERS_PER_SECOND_SQUARED = 2.0; private static final double AUTO_DRIVE_P_CONTROLLER = 6.0; private static final double AUTO_DRIVE_I_CONTROLLER = 0.0; private static final double AUTO_DRIVE_D_CONTROLLER = 0.0; - private static final double AUTO_TURN_P_CONTROLLER = 10.0; - private static final double AUTO_TURN_I_CONTROLLER = 0.0; - private static final double AUTO_TURN_D_CONTROLLER = 0.0; + private static final double AUTO_TURN_P_CONTROLLER = 0.4; + private static final double AUTO_TURN_I_CONTROLLER = 1.6; + private static final double AUTO_TURN_D_CONTROLLER = 1.0; + + // Drive to Pose constants + private static final double DRIVE_TO_POSE_DRIVE_KP = 2.5; + private static final double DRIVE_TO_POSE_DRIVE_KD = 0.0; + private static final double DRIVE_TO_POSE_THETA_KP = 18.0; + private static final double DRIVE_TO_POSE_THETA_KI = 10.0; + private static final double DRIVE_TO_POSE_THETA_KD = 0.0; + private static final double DRIVE_TO_POSE_DRIVE_TOLERANCE_METERS = 0.08; + private static final double DRIVE_TO_POSE_THETA_TOLERANCE_RADIANS = 0.008; @Override public double getSwerveAngleKP() { @@ -221,8 +220,8 @@ public double getRobotLengthWithBumpers() { } @Override - public Transform3d getRobotToCameraTransform() { - return ROBOT_TO_CAMERA; + public Transform3d[] getRobotToCameraTransforms() { + return new Transform3d[] {ROBOT_TO_CAMERA}; } @Override @@ -230,6 +229,21 @@ public double getRobotMaxVelocity() { return MAX_VELOCITY_METERS_PER_SECOND; } + @Override + public double getRobotMaxDriveAcceleration() { + return MAX_DRIVE_ACCELERATION_METERS_PER_SECOND_SQUARED; + } + + @Override + public double getRobotMaxTurnAcceleration() { + return MAX_TURN_ACCELERATION_RADIANS_PER_SECOND_SQUARED; + } + + @Override + public double getRobotSlowModeMultiplier() { + return SLOW_MODE_MULTIPLIER; + } + @Override public double getRobotMaxCoastVelocity() { return MAX_COAST_VELOCITY_METERS_PER_SECOND; @@ -281,8 +295,8 @@ public String getCANBusName() { } @Override - public String getCameraName() { - return CAMERA_NAME; + public String[] getCameraNames() { + return new String[] {CAMERA_NAME}; } @Override @@ -304,4 +318,61 @@ public int getRevHighPressureSensorChannel() { public int getRevLowPressureSensorChannel() { return REV_LOW_PRESSURE_SENSOR_CHANNEL; } + + @Override + public double getDriveToPoseDriveKP() { + return DRIVE_TO_POSE_DRIVE_KP; + } + + @Override + public double getDriveToPoseDriveKD() { + return DRIVE_TO_POSE_DRIVE_KD; + } + + @Override + public double getDriveToPoseThetaKI() { + return DRIVE_TO_POSE_THETA_KI; + } + + @Override + public double getDriveToPoseThetaKP() { + return DRIVE_TO_POSE_THETA_KP; + } + + @Override + public double getDriveToPoseThetaKD() { + return DRIVE_TO_POSE_THETA_KD; + } + + @Override + public double getDriveToPoseDriveMaxVelocity() { + return 0.5; + } + + @Override + public double getDriveToPoseDriveMaxAcceleration() { + return getAutoMaxAcceleration(); + } + + @Override + public double getDriveToPoseTurnMaxVelocity() { + return getDriveToPoseDriveMaxVelocity() + / Math.hypot(getTrackwidth() / 2.0, getWheelbase() / 2.0); + } + + @Override + public double getDriveToPoseTurnMaxAcceleration() { + return getDriveToPoseDriveMaxAcceleration() + / Math.hypot(getTrackwidth() / 2.0, getWheelbase() / 2.0); + } + + @Override + public double getDriveToPoseDriveTolerance() { + return DRIVE_TO_POSE_DRIVE_TOLERANCE_METERS; + } + + @Override + public double getDriveToPoseThetaTolerance() { + return DRIVE_TO_POSE_THETA_TOLERANCE_RADIANS; + } } diff --git a/src/main/java/frc/robot/configs/NovaRobotConfig.java b/src/main/java/frc/robot/configs/NovaRobotConfig.java new file mode 100644 index 00000000..84a2f105 --- /dev/null +++ b/src/main/java/frc/robot/configs/NovaRobotConfig.java @@ -0,0 +1,381 @@ +package frc.robot.configs; + +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Transform3d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.math.util.Units; +import frc.lib.team3061.RobotConfig; +import frc.lib.team3061.swerve.SwerveModuleConstants.SwerveType; + +/* + * Refer to the README for how to represent your robot's configuration. For more information on + * these methods, refer to the documentation in the RobotConfig class. + */ +public class NovaRobotConfig extends RobotConfig { + + private static final int FRONT_LEFT_MODULE_DRIVE_MOTOR = 13; + private static final int FRONT_LEFT_MODULE_STEER_MOTOR = 12; + private static final int FRONT_LEFT_MODULE_STEER_ENCODER = 14; + private static final double FRONT_LEFT_MODULE_STEER_OFFSET = 37.53; + + private static final int FRONT_RIGHT_MODULE_DRIVE_MOTOR = 16; + private static final int FRONT_RIGHT_MODULE_STEER_MOTOR = 15; + private static final int FRONT_RIGHT_MODULE_STEER_ENCODER = 17; + private static final double FRONT_RIGHT_MODULE_STEER_OFFSET = 100.02; + + private static final int BACK_LEFT_MODULE_DRIVE_MOTOR = 7; + private static final int BACK_LEFT_MODULE_STEER_MOTOR = 6; + private static final int BACK_LEFT_MODULE_STEER_ENCODER = 8; + private static final double BACK_LEFT_MODULE_STEER_OFFSET = 268.51; + + private static final int BACK_RIGHT_MODULE_DRIVE_MOTOR = 10; + private static final int BACK_RIGHT_MODULE_STEER_MOTOR = 9; + private static final int BACK_RIGHT_MODULE_STEER_ENCODER = 11; + private static final double BACK_RIGHT_MODULE_STEER_OFFSET = 8.88; + + private static final int GYRO_ID = 3; + + private static final double TRACKWIDTH_METERS = 0.523875; // 20.625 + private static final double WHEELBASE_METERS = 0.52705; // 20.75 + private static final double ROBOT_WIDTH_WITH_BUMPERS = 0.8382; // meters //33 in + private static final double ROBOT_LENGTH_WITH_BUMPERS = 0.8382; // meters // 33 in + + /* Angle Motor PID Values */ + private static final double ANGLE_KP = 0.4; + private static final double ANGLE_KI = 0.0; + private static final double ANGLE_KD = 12.0; + private static final double ANGLE_KF = 0.0; + + /* Drive Motor PID Values */ + private static final double DRIVE_KP = 0.10; + private static final double DRIVE_KI = 0.0; + private static final double DRIVE_KD = 5.5; + private static final double DRIVE_KF = 0.0; + + private static final double DRIVE_KS = 0.28006; + private static final double DRIVE_KV = 2.32216; + private static final double DRIVE_KA = 0.12872; + + private static final SwerveType SWERVE_TYPE = SwerveType.MK4I; + + private static final double MAX_VELOCITY_METERS_PER_SECOND = 4.78; + private static final double MAX_COAST_VELOCITY_METERS_PER_SECOND = 0.05; + private static final double SLOW_MODE_MULTIPLIER = 0.75; + + private static final double MAX_DRIVE_ACCELERATION_METERS_PER_SECOND_SQUARED = 10.0; + private static final double MAX_TURN_ACCELERATION_RADIANS_PER_SECOND_SQUARED = 4.0 * Math.PI; + + private static final String CAN_BUS_NAME = "canbus1"; + + private static final String CAMERA_NAME_0 = "OV9281"; + + private static final String CAMERA_NAME_1 = "OV9281R"; + + private static final Transform3d ROBOT_TO_CAMERA_0 = + new Transform3d( + new Translation3d( + Units.inchesToMeters(-10.406), + Units.inchesToMeters(6.603), + Units.inchesToMeters(49.240)), + new Rotation3d(0, Units.degreesToRadians(25), Units.degreesToRadians(30))); + + private static final Transform3d ROBOT_TO_CAMERA_1 = + new Transform3d( + new Translation3d( + Units.inchesToMeters(-10.406), + Units.inchesToMeters(-6.603), + Units.inchesToMeters(49.240)), + new Rotation3d(0, Units.degreesToRadians(25), Units.degreesToRadians(-30))); + + private static final double AUTO_MAX_SPEED_METERS_PER_SECOND = 2.0; + private static final double AUTO_MAX_ACCELERATION_METERS_PER_SECOND_SQUARED = 2.0; + private static final double AUTO_DRIVE_P_CONTROLLER = 8.0; + private static final double AUTO_DRIVE_I_CONTROLLER = 0.0; + private static final double AUTO_DRIVE_D_CONTROLLER = 0.0; + private static final double AUTO_TURN_P_CONTROLLER = 0.2; + private static final double AUTO_TURN_I_CONTROLLER = 0.0; + private static final double AUTO_TURN_D_CONTROLLER = 0.0; + + // Drive to Pose constants + private static final double DRIVE_TO_POSE_DRIVE_KP = 2.5; + private static final double DRIVE_TO_POSE_DRIVE_KD = 0.0; + private static final double DRIVE_TO_POSE_THETA_KP = 18.0; + private static final double DRIVE_TO_POSE_THETA_KI = 10.0; + private static final double DRIVE_TO_POSE_THETA_KD = 0.0; + private static final double DRIVE_TO_POSE_DRIVE_TOLERANCE_METERS = 0.08; + private static final double DRIVE_TO_POSE_THETA_TOLERANCE_RADIANS = 0.008; + + private static final double SQUARING_SPEED_METERS_PER_SECOND = 1.0; + + @Override + public double getSwerveAngleKP() { + return ANGLE_KP; + } + + @Override + public double getSwerveAngleKI() { + return ANGLE_KI; + } + + @Override + public double getSwerveAngleKD() { + return ANGLE_KD; + } + + @Override + public double getSwerveAngleKF() { + return ANGLE_KF; + } + + @Override + public double getSwerveDriveKP() { + return DRIVE_KP; + } + + @Override + public double getSwerveDriveKI() { + return DRIVE_KI; + } + + @Override + public double getSwerveDriveKD() { + return DRIVE_KD; + } + + @Override + public double getSwerveDriveKF() { + return DRIVE_KF; + } + + @Override + public double getDriveKS() { + return DRIVE_KS; + } + + @Override + public double getDriveKV() { + return DRIVE_KV; + } + + @Override + public double getDriveKA() { + return DRIVE_KA; + } + + @Override + public SwerveType getSwerveType() { + return SWERVE_TYPE; + } + + @Override + public int[] getSwerveDriveMotorCANIDs() { + return new int[] { + FRONT_LEFT_MODULE_DRIVE_MOTOR, + FRONT_RIGHT_MODULE_DRIVE_MOTOR, + BACK_LEFT_MODULE_DRIVE_MOTOR, + BACK_RIGHT_MODULE_DRIVE_MOTOR + }; + } + + @Override + public int[] getSwerveSteerMotorCANIDs() { + return new int[] { + FRONT_LEFT_MODULE_STEER_MOTOR, + FRONT_RIGHT_MODULE_STEER_MOTOR, + BACK_LEFT_MODULE_STEER_MOTOR, + BACK_RIGHT_MODULE_STEER_MOTOR + }; + } + + @Override + public int[] getSwerveSteerEncoderCANIDs() { + return new int[] { + FRONT_LEFT_MODULE_STEER_ENCODER, + FRONT_RIGHT_MODULE_STEER_ENCODER, + BACK_LEFT_MODULE_STEER_ENCODER, + BACK_RIGHT_MODULE_STEER_ENCODER + }; + } + + @Override + public double[] getSwerveSteerOffsets() { + return new double[] { + FRONT_LEFT_MODULE_STEER_OFFSET, + FRONT_RIGHT_MODULE_STEER_OFFSET, + BACK_LEFT_MODULE_STEER_OFFSET, + BACK_RIGHT_MODULE_STEER_OFFSET + }; + } + + @Override + public int getGyroCANID() { + return GYRO_ID; + } + + @Override + public double getTrackwidth() { + return TRACKWIDTH_METERS; + } + + @Override + public double getWheelbase() { + return WHEELBASE_METERS; + } + + @Override + public double getRobotWidthWithBumpers() { + return ROBOT_WIDTH_WITH_BUMPERS; + } + + @Override + public double getRobotLengthWithBumpers() { + return ROBOT_LENGTH_WITH_BUMPERS; + } + + @Override + public Transform3d[] getRobotToCameraTransforms() { + return new Transform3d[] {ROBOT_TO_CAMERA_0, ROBOT_TO_CAMERA_1}; + } + + @Override + public double getRobotMaxVelocity() { + return MAX_VELOCITY_METERS_PER_SECOND; + } + + @Override + public double getRobotMaxDriveAcceleration() { + return MAX_DRIVE_ACCELERATION_METERS_PER_SECOND_SQUARED; + } + + @Override + public double getRobotMaxTurnAcceleration() { + return MAX_TURN_ACCELERATION_RADIANS_PER_SECOND_SQUARED; + } + + @Override + public double getRobotSlowModeMultiplier() { + return SLOW_MODE_MULTIPLIER; + } + + @Override + public double getRobotMaxCoastVelocity() { + return MAX_COAST_VELOCITY_METERS_PER_SECOND; + } + + @Override + public double getAutoMaxSpeed() { + return AUTO_MAX_SPEED_METERS_PER_SECOND; + } + + @Override + public double getAutoMaxAcceleration() { + return AUTO_MAX_ACCELERATION_METERS_PER_SECOND_SQUARED; + } + + @Override + public double getAutoDriveKP() { + return AUTO_DRIVE_P_CONTROLLER; + } + + @Override + public double getAutoDriveKI() { + return AUTO_DRIVE_I_CONTROLLER; + } + + @Override + public double getAutoDriveKD() { + return AUTO_DRIVE_D_CONTROLLER; + } + + @Override + public double getAutoTurnKP() { + return AUTO_TURN_P_CONTROLLER; + } + + @Override + public double getAutoTurnKI() { + return AUTO_TURN_I_CONTROLLER; + } + + @Override + public double getAutoTurnKD() { + return AUTO_TURN_D_CONTROLLER; + } + + @Override + public String getCANBusName() { + return CAN_BUS_NAME; + } + + @Override + public String[] getCameraNames() { + return new String[] {CAMERA_NAME_0, CAMERA_NAME_1}; + } + + @Override + public double getDriveToPoseDriveKP() { + return DRIVE_TO_POSE_DRIVE_KP; + } + + @Override + public double getDriveToPoseDriveKD() { + return DRIVE_TO_POSE_DRIVE_KD; + } + + @Override + public double getDriveToPoseThetaKI() { + return DRIVE_TO_POSE_THETA_KI; + } + + @Override + public double getDriveToPoseThetaKP() { + return DRIVE_TO_POSE_THETA_KP; + } + + @Override + public double getDriveToPoseThetaKD() { + return DRIVE_TO_POSE_THETA_KD; + } + + @Override + public double getDriveToPoseDriveMaxVelocity() { + return 0.5; + } + + @Override + public double getDriveToPoseDriveMaxAcceleration() { + return getAutoMaxAcceleration(); + } + + @Override + public double getDriveToPoseTurnMaxVelocity() { + return getDriveToPoseDriveMaxVelocity() + / Math.hypot(getTrackwidth() / 2.0, getWheelbase() / 2.0); + } + + @Override + public double getDriveToPoseTurnMaxAcceleration() { + return getDriveToPoseDriveMaxAcceleration() + / Math.hypot(getTrackwidth() / 2.0, getWheelbase() / 2.0); + } + + @Override + public double getDriveToPoseDriveTolerance() { + return DRIVE_TO_POSE_DRIVE_TOLERANCE_METERS; + } + + @Override + public double getDriveToPoseThetaTolerance() { + return DRIVE_TO_POSE_THETA_TOLERANCE_RADIANS; + } + + @Override + public int getPneumaticsHubCANID() { + return 0; + } + + @Override + public double getMoveToPathFinalVelocity() { + return SQUARING_SPEED_METERS_PER_SECOND; + } +} diff --git a/src/main/java/frc/robot/configs/SierraRobotConfig.java b/src/main/java/frc/robot/configs/SierraRobotConfig.java index cd782ed0..6fc70dee 100644 --- a/src/main/java/frc/robot/configs/SierraRobotConfig.java +++ b/src/main/java/frc/robot/configs/SierraRobotConfig.java @@ -30,7 +30,7 @@ public class SierraRobotConfig extends RobotConfig { private static final int BACK_RIGHT_MODULE_DRIVE_MOTOR = 16; private static final int BACK_RIGHT_MODULE_STEER_MOTOR = 15; private static final int BACK_RIGHT_MODULE_STEER_ENCODER = 17; - private static final double BACK_RIGHT_MODULE_STEER_OFFSET = 40.86; + private static final double BACK_RIGHT_MODULE_STEER_OFFSET = 40.78; private static final int GYRO_ID = 18; @@ -59,12 +59,15 @@ public class SierraRobotConfig extends RobotConfig { private static final double MAX_VELOCITY_METERS_PER_SECOND = 4.25; private static final double MAX_COAST_VELOCITY_METERS_PER_SECOND = 0.05; + private static final double MAX_DRIVE_ACCELERATION_METERS_PER_SECOND_SQUARED = 4.0; + private static final double MAX_TURN_ACCELERATION_RADIANS_PER_SECOND_SQUARED = 4.0; + private static final double SLOW_MODE_MULTIPLIER = 0.75; private static final String CAN_BUS_NAME = ""; private static final String CAMERA_NAME = "OV9281"; - private static final Transform3d ROBOT_TO_CAMERA = + private static final Transform3d ROBOT_TO_CAMERA_0 = new Transform3d(new Translation3d(0.254, 0, 0.648), new Rotation3d(0, 0, 0)); private static final int PNEUMATICS_HUB_ID = 20; @@ -81,6 +84,14 @@ public class SierraRobotConfig extends RobotConfig { private static final double AUTO_TURN_I_CONTROLLER = 0.0; private static final double AUTO_TURN_D_CONTROLLER = 0.0; + // Drive to Pose constants + private static final double DRIVE_TO_POSE_DRIVE_KP = 2.5; + private static final double DRIVE_TO_POSE_DRIVE_KD = 0.0; + private static final double DRIVE_TO_POSE_THETA_KP = 7.0; + private static final double DRIVE_TO_POSE_THETA_KD = 0.0; + private static final double DRIVE_TO_POSE_DRIVE_TOLERANCE_METERS = 0.01; + private static final double DRIVE_TO_POSE_THETA_TOLERANCE_RADIANS = 0.035; + @Override public double getSwerveAngleKP() { return ANGLE_KP; @@ -207,8 +218,8 @@ public double getRobotLengthWithBumpers() { } @Override - public Transform3d getRobotToCameraTransform() { - return ROBOT_TO_CAMERA; + public Transform3d[] getRobotToCameraTransforms() { + return new Transform3d[] {ROBOT_TO_CAMERA_0}; } @Override @@ -216,6 +227,21 @@ public double getRobotMaxVelocity() { return MAX_VELOCITY_METERS_PER_SECOND; } + @Override + public double getRobotMaxDriveAcceleration() { + return MAX_DRIVE_ACCELERATION_METERS_PER_SECOND_SQUARED; + } + + @Override + public double getRobotMaxTurnAcceleration() { + return MAX_TURN_ACCELERATION_RADIANS_PER_SECOND_SQUARED; + } + + @Override + public double getRobotSlowModeMultiplier() { + return SLOW_MODE_MULTIPLIER; + } + @Override public double getRobotMaxCoastVelocity() { return MAX_COAST_VELOCITY_METERS_PER_SECOND; @@ -267,8 +293,8 @@ public String getCANBusName() { } @Override - public String getCameraName() { - return CAMERA_NAME; + public String[] getCameraNames() { + return new String[] {CAMERA_NAME}; } @Override @@ -290,4 +316,56 @@ public int getRevHighPressureSensorChannel() { public int getRevLowPressureSensorChannel() { return REV_LOW_PRESSURE_SENSOR_CHANNEL; } + + @Override + public double getDriveToPoseDriveKP() { + return DRIVE_TO_POSE_DRIVE_KP; + } + + @Override + public double getDriveToPoseDriveKD() { + return DRIVE_TO_POSE_DRIVE_KD; + } + + @Override + public double getDriveToPoseThetaKP() { + return DRIVE_TO_POSE_THETA_KP; + } + + @Override + public double getDriveToPoseThetaKD() { + return DRIVE_TO_POSE_THETA_KD; + } + + @Override + public double getDriveToPoseDriveMaxVelocity() { + return getAutoMaxSpeed(); + } + + @Override + public double getDriveToPoseDriveMaxAcceleration() { + return getAutoMaxAcceleration(); + } + + @Override + public double getDriveToPoseTurnMaxVelocity() { + return getDriveToPoseDriveMaxVelocity() + / Math.hypot(getTrackwidth() / 2.0, getWheelbase() / 2.0); + } + + @Override + public double getDriveToPoseTurnMaxAcceleration() { + return getDriveToPoseDriveMaxAcceleration() + / Math.hypot(getTrackwidth() / 2.0, getWheelbase() / 2.0); + } + + @Override + public double getDriveToPoseDriveTolerance() { + return DRIVE_TO_POSE_DRIVE_TOLERANCE_METERS; + } + + @Override + public double getDriveToPoseThetaTolerance() { + return DRIVE_TO_POSE_THETA_TOLERANCE_RADIANS; + } } diff --git a/src/main/java/frc/robot/operator_interface/DualJoysticksOI.java b/src/main/java/frc/robot/operator_interface/DualJoysticksOI.java index 8fe7c938..c2ae54cb 100644 --- a/src/main/java/frc/robot/operator_interface/DualJoysticksOI.java +++ b/src/main/java/frc/robot/operator_interface/DualJoysticksOI.java @@ -7,7 +7,7 @@ import edu.wpi.first.wpilibj2.command.button.CommandJoystick; import edu.wpi.first.wpilibj2.command.button.Trigger; -/** Class for controlling the robot with two Xbox controllers. */ +/** Class for controlling the robot with two joysticks. */ public class DualJoysticksOI implements OperatorInterface { private final CommandJoystick translateJoystick; private final CommandJoystick rotateJoystick; @@ -56,11 +56,17 @@ public Trigger getResetGyroButton() { @Override public Trigger getXStanceButton() { - return translateJoystickButtons[1]; + return rotateJoystickButtons[4]; } @Override public Trigger getVisionIsEnabledSwitch() { - return rotateJoystickButtons[1]; + // vision is always enabled with dual joysticks as there is no switch to disable + return new Trigger(() -> true); + } + + @Override + public Trigger getTurboButton() { + return translateJoystickButtons[1]; } } diff --git a/src/main/java/frc/robot/operator_interface/FullOperatorConsoleOI.java b/src/main/java/frc/robot/operator_interface/FullOperatorConsoleOI.java new file mode 100644 index 00000000..058734d1 --- /dev/null +++ b/src/main/java/frc/robot/operator_interface/FullOperatorConsoleOI.java @@ -0,0 +1,104 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package frc.robot.operator_interface; + +import edu.wpi.first.wpilibj.XboxController; +import edu.wpi.first.wpilibj2.command.button.CommandJoystick; +import edu.wpi.first.wpilibj2.command.button.Trigger; + +/** + * Class for controlling the robot with two joysticks, 1 Xbox controller, and 1 operator button + * panel. + */ +public class FullOperatorConsoleOI implements OperatorInterface { + private final CommandJoystick translateJoystick; + private final Trigger[] translateJoystickButtons; + + private final CommandJoystick rotateJoystick; + private final Trigger[] rotateJoystickButtons; + + private final XboxController operatorController; + + private final CommandJoystick operatorPanel; + private final Trigger[] operatorPanelButtons; + + public FullOperatorConsoleOI( + int translatePort, int rotatePort, int operatorControllerPort, int operatorPanelPort) { + translateJoystick = new CommandJoystick(translatePort); + rotateJoystick = new CommandJoystick(rotatePort); + operatorController = new XboxController(operatorControllerPort); + operatorPanel = new CommandJoystick(operatorPanelPort); + + // buttons use 1-based indexing such that the index matches the button number; leave index 0 set + // to null + this.translateJoystickButtons = new Trigger[13]; + this.rotateJoystickButtons = new Trigger[13]; + this.operatorPanelButtons = new Trigger[13]; + + for (int i = 1; i < translateJoystickButtons.length; i++) { + translateJoystickButtons[i] = translateJoystick.button(i); + rotateJoystickButtons[i] = rotateJoystick.button(i); + } + for (int i = 1; i < operatorPanelButtons.length; i++) { + operatorPanelButtons[i] = operatorPanel.button(i); + } + } + + // Translate Joystick + @Override + public double getTranslateX() { + return -translateJoystick.getY(); + } + + @Override + public double getTranslateY() { + return -translateJoystick.getX(); + } + + @Override + public Trigger getLock180Button() { + return translateJoystickButtons[3]; + } + + @Override + public Trigger getResetGyroButton() { + return translateJoystickButtons[4]; + } + + @Override + public Trigger getFieldRelativeButton() { + return translateJoystickButtons[9]; + } + + // Rotate Joystick + + @Override + public double getRotate() { + return -rotateJoystick.getX(); + } + + @Override + public Trigger getXStanceButton() { + return rotateJoystickButtons[4]; + } + + @Override + public Trigger getResetPoseToVisionButton() { + return rotateJoystickButtons[5]; + } + + // Operator Controller + @Override + public Trigger getInterruptAll() { + return new Trigger(operatorController::getStartButton); + } + + // Operator Panel + + @Override + public Trigger getVisionIsEnabledSwitch() { + return operatorPanelButtons[10]; + } +} diff --git a/src/main/java/frc/robot/operator_interface/OISelector.java b/src/main/java/frc/robot/operator_interface/OISelector.java index 114beea8..7c4b92b2 100644 --- a/src/main/java/frc/robot/operator_interface/OISelector.java +++ b/src/main/java/frc/robot/operator_interface/OISelector.java @@ -9,9 +9,12 @@ package frc.robot.operator_interface; import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.first.wpilibj2.command.CommandScheduler; import frc.lib.team6328.util.Alert; import frc.lib.team6328.util.Alert.AlertType; +@java.lang.SuppressWarnings({"java:S3776"}) + /** * Utility class for selecting the appropriate OI implementations based on the connected joysticks. */ @@ -22,13 +25,23 @@ public class OISelector { private static final Alert nonCompetitionOperatorInterfaceWarning = new Alert("Non-competition operator controller connected.", AlertType.WARNING); + private static OperatorInterface oi = null; + private OISelector() {} + public static OperatorInterface getOperatorInterface() { + if (didJoysticksChange()) { + CommandScheduler.getInstance().getActiveButtonLoop().clear(); + oi = findOperatorInterface(); + } + return oi; + } + /** * Returns whether the connected joysticks have changed since the last time this method was * called. */ - public static boolean didJoysticksChange() { + private static boolean didJoysticksChange() { boolean joysticksChanged = false; for (int port = 0; port < DriverStation.kJoystickPorts; port++) { String name = DriverStation.getJoystickName(port); @@ -44,10 +57,11 @@ public static boolean didJoysticksChange() { * Instantiates and returns an appropriate OperatorInterface object based on the connected * joysticks. */ - public static OperatorInterface findOperatorInterface() { + private static OperatorInterface findOperatorInterface() { Integer firstPort = null; Integer secondPort = null; Integer xBoxPort = null; + Integer thirdPort = null; for (int port = 0; port < DriverStation.kJoystickPorts; port++) { if (DriverStation.getJoystickName(port).toLowerCase().contains("xbox")) { if (xBoxPort == null) { @@ -58,13 +72,19 @@ public static OperatorInterface findOperatorInterface() { firstPort = port; } else if (secondPort == null) { secondPort = port; + } else { // thirdPort == null + thirdPort = port; } } } - if (firstPort != null && secondPort != null) { + if (firstPort != null && secondPort != null && xBoxPort != null && thirdPort != null) { noOperatorInterfaceWarning.set(false); nonCompetitionOperatorInterfaceWarning.set(false); + return new FullOperatorConsoleOI(firstPort, secondPort, xBoxPort, thirdPort); + } else if (firstPort != null && secondPort != null) { + noOperatorInterfaceWarning.set(false); + nonCompetitionOperatorInterfaceWarning.set(true); return new DualJoysticksOI(firstPort, secondPort); } else if (xBoxPort != null) { noOperatorInterfaceWarning.set(false); @@ -72,7 +92,7 @@ public static OperatorInterface findOperatorInterface() { return new SingleHandheldOI(xBoxPort); } else { noOperatorInterfaceWarning.set(true); - nonCompetitionOperatorInterfaceWarning.set(false); + nonCompetitionOperatorInterfaceWarning.set(true); return new OperatorInterface() {}; } } diff --git a/src/main/java/frc/robot/operator_interface/OperatorInterface.java b/src/main/java/frc/robot/operator_interface/OperatorInterface.java index ff30aad2..e5b69b64 100644 --- a/src/main/java/frc/robot/operator_interface/OperatorInterface.java +++ b/src/main/java/frc/robot/operator_interface/OperatorInterface.java @@ -8,7 +8,7 @@ package frc.robot.operator_interface; -import edu.wpi.first.wpilibj2.command.button.Trigger; +import edu.wpi.first.wpilibj2.command.button.*; /** Interface for all driver and operator controls. */ public interface OperatorInterface { @@ -33,11 +33,35 @@ public default Trigger getResetGyroButton() { return new Trigger(() -> false); } + public default Trigger getResetPoseToVisionButton() { + return new Trigger(() -> false); + } + public default Trigger getXStanceButton() { return new Trigger(() -> false); } + public default Trigger getTranslationSlowModeButton() { + return new Trigger(() -> false); + } + + public default Trigger getRotationSlowModeButton() { + return new Trigger(() -> false); + } + + public default Trigger getLock180Button() { + return new Trigger(() -> false); + } + public default Trigger getVisionIsEnabledSwitch() { return new Trigger(() -> false); } + + public default Trigger getTurboButton() { + return new Trigger(() -> false); + } + + public default Trigger getInterruptAll() { + return new Trigger(() -> false); + } } diff --git a/src/main/java/frc/robot/operator_interface/SingleHandheldOI.java b/src/main/java/frc/robot/operator_interface/SingleHandheldOI.java index e26439c0..d3a71345 100644 --- a/src/main/java/frc/robot/operator_interface/SingleHandheldOI.java +++ b/src/main/java/frc/robot/operator_interface/SingleHandheldOI.java @@ -45,8 +45,19 @@ public Trigger getXStanceButton() { return new Trigger(controller::getYButton); } + @Override + public Trigger getTranslationSlowModeButton() { + return new Trigger(controller::getLeftBumper); + } + + @Override + public Trigger getRotationSlowModeButton() { + return new Trigger(controller::getRightBumper); + } + @Override public Trigger getVisionIsEnabledSwitch() { - return new Trigger(controller::getXButton); + // vision is always enabled with Xbox as there is no switch to disable + return new Trigger(() -> true); } } diff --git a/src/main/java/frc/robot/subsystems/drivetrain/Drivetrain.java b/src/main/java/frc/robot/subsystems/drivetrain/Drivetrain.java index e9c7d92a..a5ba060b 100644 --- a/src/main/java/frc/robot/subsystems/drivetrain/Drivetrain.java +++ b/src/main/java/frc/robot/subsystems/drivetrain/Drivetrain.java @@ -27,7 +27,10 @@ import frc.lib.team3061.gyro.GyroIOInputsAutoLogged; import frc.lib.team3061.swerve.SwerveModule; import frc.lib.team3061.util.RobotOdometry; +import frc.lib.team6328.util.Alert; +import frc.lib.team6328.util.Alert.AlertType; import frc.lib.team6328.util.TunableNumber; +import java.util.function.Supplier; import org.littletonrobotics.junction.Logger; /** @@ -36,6 +39,7 @@ * robot's rotation. */ public class Drivetrain extends SubsystemBase { + private final GyroIO gyroIO; private final GyroIOInputsAutoLogged gyroInputs = new GyroIOInputsAutoLogged(); @@ -88,6 +92,9 @@ public class Drivetrain extends SubsystemBase { private boolean isFieldRelative; + private boolean isTranslationSlowMode = false; + private boolean isRotationSlowMode = false; + private double gyroOffset; private ChassisSpeeds chassisSpeeds; @@ -98,11 +105,28 @@ public class Drivetrain extends SubsystemBase { private final SwerveDrivePoseEstimator poseEstimator; private boolean brakeMode; + private Timer brakeModeTimer = new Timer(); + private static final double BREAK_MODE_DELAY_SEC = 10.0; private DriveMode driveMode = DriveMode.NORMAL; private double characterizationVoltage = 0.0; - /** Constructs a new DrivetrainSubsystem object. */ + private boolean isTurbo; + + private boolean isMoveToPoseEnabled; + + private Alert noPoseAlert = + new Alert("Attempted to reset pose from vision, but no pose was found.", AlertType.WARNING); + + /** + * Creates a new Drivetrain subsystem. + * + * @param gyroIO the abstracted interface for the gyro for the drivetrain + * @param flModule the front left swerve module + * @param frModule the front right swerve module + * @param blModule the back left swerve module + * @param brModule the back right swerve module + */ public Drivetrain( GyroIO gyroIO, SwerveModule flModule, @@ -121,7 +145,7 @@ public Drivetrain( this.zeroGyroscope(); - this.isFieldRelative = false; + this.isFieldRelative = true; this.gyroOffset = 0; @@ -129,10 +153,21 @@ public Drivetrain( this.poseEstimator = RobotOdometry.getInstance().getPoseEstimator(); + // based on testing we can drive in turbo mode all the time + this.isTurbo = true; + + this.isMoveToPoseEnabled = true; + ShuffleboardTab tabMain = Shuffleboard.getTab("MAIN"); - tabMain.addNumber("Gyroscope Angle", () -> getRotation().getDegrees()); - tabMain.addBoolean("X-Stance On?", this::isXstance); - tabMain.addBoolean("Field-Relative Enabled?", () -> this.isFieldRelative); + tabMain + .addNumber("Gyroscope Angle", () -> getRotation().getDegrees()) + .withPosition(9, 0) + .withSize(1, 1); + tabMain.addBoolean("X-Stance On?", this::isXstance).withPosition(7, 0).withSize(1, 1); + tabMain + .addBoolean("Field-Relative Enabled?", () -> this.isFieldRelative) + .withPosition(8, 0) + .withSize(1, 1); if (DEBUGGING) { ShuffleboardTab tab = Shuffleboard.getTab(SUBSYSTEM_NAME); @@ -154,9 +189,30 @@ public Drivetrain( } } + /** Enables "turbo" mode (i.e., acceleration is not limited by software) */ + public void enableTurbo() { + this.isTurbo = true; + } + + /** Disables "turbo" mode (i.e., acceleration is limited by software) */ + public void disableTurbo() { + this.isTurbo = false; + } + + /** + * Returns true if the robot is in "turbo" mode (i.e., acceleration is not limited by software); + * false otherwise + * + * @return true if the robot is in "turbo" mode (i.e., acceleration is not limited by software); + * false otherwise + */ + public boolean getTurbo() { + return this.isTurbo; + } + /** * Zeroes the gyroscope. This sets the current rotation of the robot to zero degrees. This method - * is intended to be invoked only when the alignment beteween the robot's rotation and the gyro is + * is intended to be invoked only when the alignment between the robot's rotation and the gyro is * sufficiently different to make field-relative driving difficult. The robot needs to be * positioned facing away from the driver, ideally aligned to a field wall before this method is * invoked. @@ -173,14 +229,43 @@ public void zeroGyroscope() { * * @return the rotation of the robot */ - private Rotation2d getRotation() { + public Rotation2d getRotation() { if (gyroInputs.connected) { - return Rotation2d.fromDegrees(gyroInputs.positionDeg + this.gyroOffset); + return Rotation2d.fromDegrees(gyroInputs.yawDeg + this.gyroOffset); } else { return estimatedPoseWithoutGyro.getRotation(); } } + /** + * Returns the yaw of the drivetrain as reported by the gyro in degrees. This value does not + * include the local offset and will likely be different than value returned by the getRotation + * method, which should probably be invoked instead. + * + * @return the yaw of the drivetrain as reported by the gyro in degrees + */ + public double getYaw() { + return gyroInputs.yawDeg; + } + + /** + * Returns the pitch of the drivetrain as reported by the gyro in degrees. + * + * @return the pitch of the drivetrain as reported by the gyro in degrees + */ + public double getPitch() { + return gyroInputs.pitchDeg; + } + + /** + * Returns the roll of the drivetrain as reported by the gyro in degrees. + * + * @return the roll of the drivetrain as reported by the gyro in degrees + */ + public double getRoll() { + return gyroInputs.rollDeg; + } + /** * Sets the rotation of the robot to the specified value. This method should only be invoked when * the rotation of the robot is known (e.g., at the start of an autonomous path). Zero degrees is @@ -193,7 +278,7 @@ public void setGyroOffset(double expectedYaw) { // taking effect. As a result, it is recommended to never set the yaw and // adjust the local offset instead. if (gyroInputs.connected) { - this.gyroOffset = expectedYaw - gyroInputs.positionDeg; + this.gyroOffset = expectedYaw - gyroInputs.yawDeg; } else { this.gyroOffset = 0; this.estimatedPoseWithoutGyro = @@ -238,6 +323,11 @@ public void resetOdometry(PathPlannerState state) { new Pose2d(state.poseMeters.getTranslation(), state.holonomicRotation)); } + /** + * Sets the robot's odometry's rotation based on the gyro. This method is intended to be invoked + * when the vision subsystem is first disabled and may have negatively impacted the pose + * estimator. + */ public void resetPoseRotationToGyro() { for (int i = 0; i < 4; i++) { swerveModulePositions[i] = swerveModules[i].getPosition(); @@ -248,6 +338,25 @@ public void resetPoseRotationToGyro() { swerveModulePositions, new Pose2d(this.getPose().getTranslation(), this.getRotation())); } + + /** + * Sets the odometry of the robot based on the supplied pose (e.g., from the vision subsystem). + * When testing, the robot can be positioned in front of an AprilTag and this method can be + * invoked to reset the robot's pose based on tag. + * + * @param poseSupplier the supplier of the pose to which set the robot's odometry + */ + public void resetPoseToVision(Supplier poseSupplier) { + Pose3d pose = poseSupplier.get(); + if (pose != null) { + noPoseAlert.set(false); + setGyroOffset(pose.toPose2d().getRotation().getDegrees()); + poseEstimator.resetPosition(this.getRotation(), swerveModulePositions, pose.toPose2d()); + } else { + noPoseAlert.set(true); + } + } + /** * Controls the drivetrain to move the robot with the desired velocities in the x, y, and * rotational directions. The velocities may be specified from either the robot's frame of @@ -257,22 +366,46 @@ public void resetPoseRotationToGyro() { * field to the driver's right). Zero degrees is away from the driver and increases in the CCW * direction. * - *

If the drive mode is XSTANCE, the robot will ignore the specified velocities and turn the - * swerve modules into the x-stance orientation. + *

If the translation or rotation slow mode features are enabled, the corresponding velocities + * will be scaled to enable finer control. + * + *

If the drive mode is X, the robot will ignore the specified velocities and turn the swerve + * modules into the x-stance orientation. * *

If the drive mode is CHARACTERIZATION, the robot will ignore the specified velocities and - * run the characterization routine. + * run the characterization routine. Refer to the FeedForwardCharacterization command class for + * more information. * - * @param translationXSupplier the desired velocity in the x direction (m/s) - * @param translationYSupplier the desired velocity in the y direction (m/s) - * @param rotationSupplier the desired rotational velcoity (rad/s) + * @param xVelocity the desired velocity in the x direction (m/s) + * @param yVelocity the desired velocity in the y direction (m/s) + * @param rotationalVelocity the desired rotational velocity (rad/s) + * @param isOpenLoop true for open-loop control; false for closed-loop control + * @param overrideFieldRelative true to force field-relative motion; false to use the current + * setting */ public void drive( - double xVelocity, double yVelocity, double rotationalVelocity, boolean isOpenLoop) { + double xVelocity, + double yVelocity, + double rotationalVelocity, + boolean isOpenLoop, + boolean overrideFieldRelative) { switch (driveMode) { case NORMAL: - if (isFieldRelative) { + // get the slow-mode multiplier from the config + double slowModeMultiplier = RobotConfig.getInstance().getRobotSlowModeMultiplier(); + // if translation or rotation is in slow mode, multiply the x and y velocities by the + // slow-mode multiplier + if (isTranslationSlowMode) { + xVelocity *= slowModeMultiplier; + yVelocity *= slowModeMultiplier; + } + // if rotation is in slow mode, multiply the rotational velocity by the slow-mode multiplier + if (isRotationSlowMode) { + rotationalVelocity *= slowModeMultiplier; + } + + if (isFieldRelative || overrideFieldRelative) { chassisSpeeds = ChassisSpeeds.fromFieldRelativeSpeeds( xVelocity, yVelocity, rotationalVelocity, getRotation()); @@ -282,11 +415,11 @@ public void drive( } Logger.getInstance() - .recordOutput("Drivetrain/chassisSpeedVx", chassisSpeeds.vxMetersPerSecond); + .recordOutput("Drivetrain/ChassisSpeedVx", chassisSpeeds.vxMetersPerSecond); Logger.getInstance() - .recordOutput("Drivetrain/chassisSpeedVy", chassisSpeeds.vyMetersPerSecond); + .recordOutput("Drivetrain/ChassisSpeedVy", chassisSpeeds.vyMetersPerSecond); Logger.getInstance() - .recordOutput("Drivetrain/chassisSpeedVo", chassisSpeeds.omegaRadiansPerSecond); + .recordOutput("Drivetrain/ChassisSpeedVo", chassisSpeeds.omegaRadiansPerSecond); SwerveModuleState[] swerveModuleStates = kinematics.toSwerveModuleStates(chassisSpeeds, centerGravity); @@ -308,7 +441,6 @@ public void drive( case X: this.setXStance(); - break; } } @@ -334,17 +466,19 @@ public void periodic() { // update and log gyro inputs gyroIO.updateInputs(gyroInputs); - Logger.getInstance().processInputs("Drive/Gyro", gyroInputs); + Logger.getInstance().processInputs("Drivetrain/Gyro", gyroInputs); - // update and log the swerve moudles inputs + // update and log the swerve modules inputs for (SwerveModule swerveModule : swerveModules) { swerveModule.updateAndProcessInputs(); } + Logger.getInstance().recordOutput("Drivetrain/AvgDriveCurrent", this.getAverageDriveCurrent()); - // update estimated poses + // update swerve module states and positions SwerveModuleState[] states = new SwerveModuleState[4]; for (int i = 0; i < 4; i++) { states[i] = swerveModules[i].getState(); + prevSwerveModulePositions[i] = swerveModulePositions[i]; swerveModulePositions[i] = swerveModules[i].getPosition(); } @@ -367,6 +501,7 @@ public void periodic() { estimatedPoseWithoutGyro = estimatedPoseWithoutGyro.exp(twist); } + // update the pose estimator based on the gyro and swerve module positions poseEstimator.updateWithTime( Timer.getFPGATimestamp(), this.getRotation(), swerveModulePositions); @@ -388,39 +523,7 @@ public void periodic() { Logger.getInstance().recordOutput("Odometry/Robot", poseEstimatorPose); Logger.getInstance().recordOutput("3DField", new Pose3d(poseEstimatorPose)); Logger.getInstance().recordOutput("SwerveModuleStates", states); - Logger.getInstance().recordOutput(SUBSYSTEM_NAME + "/gyroOffset", this.gyroOffset); - } - - /** - * If the robot is enabled and brake mode is not enabled, enable it. If the robot is disabled, has - * stopped moving, and brake mode is enabled, disable it. - */ - private void updateBrakeMode() { - if (DriverStation.isEnabled() && !brakeMode) { - brakeMode = true; - setBrakeMode(true); - - } else { - boolean stillMoving = false; - for (SwerveModule mod : swerveModules) { - if (Math.abs(mod.getState().speedMetersPerSecond) - > RobotConfig.getInstance().getRobotMaxCoastVelocity()) { - stillMoving = true; - } - } - - if (brakeMode && !stillMoving) { - brakeMode = false; - setBrakeMode(false); - } - } - } - - private void setBrakeMode(boolean enable) { - for (SwerveModule mod : swerveModules) { - mod.setAngleBrakeMode(enable); - mod.setDriveBrakeMode(enable); - } + Logger.getInstance().recordOutput("Drivetrain/GyroOffset", this.gyroOffset); } /** @@ -466,10 +569,42 @@ public void disableFieldRelative() { this.isFieldRelative = false; } + /** + * Enables slow mode for translation. When enabled, the robot's translational velocities will be + * scaled down. + */ + public void enableTranslationSlowMode() { + this.isTranslationSlowMode = true; + } + + /** + * Disables slow mode for translation. When disabled, the robot's translational velocities will + * not be scaled. + */ + public void disableTranslationSlowMode() { + this.isTranslationSlowMode = false; + } + + /** + * Enables slow mode for rotation. When enabled, the robot's rotational velocity will be scaled + * down. + */ + public void enableRotationSlowMode() { + this.isRotationSlowMode = true; + } + + /** + * Disables slow mode for rotation. When disabled, the robot's rotational velocity will not be + * scaled. + */ + public void disableRotationSlowMode() { + this.isRotationSlowMode = false; + } + /** * Sets the swerve modules in the x-stance orientation. In this orientation the wheels are aligned - * to make an 'X'. This makes it more difficult for other robots to push the robot, which is - * useful when shooting. + * to make an 'X'. This prevents the robot from rolling on an inclined surface and makes it more + * difficult for other robots to push the robot, which is useful when shooting. */ public void setXStance() { chassisSpeeds = new ChassisSpeeds(0.0, 0.0, 0.0); @@ -484,6 +619,31 @@ public void setXStance() { } } + /** + * Puts the drivetrain into the x-stance orientation. In this orientation the wheels are aligned + * to make an 'X'. This prevents the robot from rolling on an inclined surface and makes it more + * difficult for other robots to push the robot, which is useful when shooting. The robot cannot + * be driven until x-stance is disabled. + */ + public void enableXstance() { + this.driveMode = DriveMode.X; + this.setXStance(); + } + + /** Disables x-stance, allowing the robot to be driven. */ + public void disableXstance() { + this.driveMode = DriveMode.NORMAL; + } + + /** + * Returns true if the robot is in the x-stance orientation. + * + * @return true if the robot is in the x-stance orientation + */ + public boolean isXstance() { + return this.driveMode == DriveMode.X; + } + /** * Sets the robot's center of gravity about which it will rotate. The origin is at the center of * the robot. The positive x direction is forward; the positive y direction, left. @@ -491,13 +651,13 @@ public void setXStance() { * @param x the x coordinate of the robot's center of gravity (in meters) * @param y the y coordinate of the robot's center of gravity (in meters) */ - public void setCenterGrav(double x, double y) { + public void setCenterGravity(double x, double y) { this.centerGravity = new Translation2d(x, y); } /** Resets the robot's center of gravity about which it will rotate to the center of the robot. */ - public void resetCenterGrav() { - setCenterGrav(0.0, 0.0); + public void resetCenterGravity() { + setCenterGravity(0.0, 0.0); } /** @@ -519,27 +679,16 @@ public double getVelocityY() { } /** - * Puts the drivetrain into the x-stance orientation. In this orientation the wheels are aligned - * to make an 'X'. This makes it more difficult for other robots to push the robot, which is - * useful when shooting. The robot cannot be driven until x-stance is disabled. - */ - public void enableXstance() { - this.driveMode = DriveMode.X; - this.setXStance(); - } - - /** Disables the x-stance, allowing the robot to be driven. */ - public void disableXstance() { - this.driveMode = DriveMode.NORMAL; - } - - /** - * Returns true if the robot is in the x-stance orientation. + * Returns the average current of the swerve module drive motors in amps. * - * @return true if the robot is in the x-stance orientation + * @return the average current of the swerve module drive motors in amps */ - public boolean isXstance() { - return this.driveMode == DriveMode.X; + public double getAverageDriveCurrent() { + double totalCurrent = 0.0; + for (SwerveModule module : swerveModules) { + totalCurrent += Math.abs(module.getDriveCurrent()); + } + return totalCurrent / swerveModules.length; } /** @@ -569,16 +718,24 @@ public PIDController getAutoThetaController() { return autoThetaController; } - /** Runs forwards at the commanded voltage. */ + /** + * Runs forwards at the commanded voltage. + * + * @param volts the commanded voltage + */ public void runCharacterizationVolts(double volts) { driveMode = DriveMode.CHARACTERIZATION; characterizationVoltage = volts; // invoke drive which will set the characterization voltage to each module - drive(0, 0, 0, true); + drive(0, 0, 0, true, false); } - /** Returns the average drive velocity in meters/sec. */ + /** + * Returns the average drive velocity in meters/sec. + * + * @return the average drive velocity in meters/sec + */ public double getCharacterizationVelocity() { double driveVelocityAverage = 0.0; for (SwerveModule swerveModule : swerveModules) { @@ -587,6 +744,59 @@ public double getCharacterizationVelocity() { return driveVelocityAverage / 4.0; } + /** + * Enables or disables the move-to-pose feature. Refer to the MoveToPose command class for more + * information. + * + * @param state true to enable, false to disable + */ + public void enableMoveToPose(boolean state) { + this.isMoveToPoseEnabled = state; + } + + /** + * Returns true if the move-to-pose feature is enabled; false otherwise. + * + * @return true if the move-to-pose feature is enabled; false otherwise + */ + public boolean isMoveToPoseEnabled() { + return this.isMoveToPoseEnabled; + } + + /** + * If the robot is enabled and brake mode is not enabled, enable it. If the robot is disabled, has + * stopped moving for the specified period of time, and brake mode is enabled, disable it. + */ + private void updateBrakeMode() { + if (DriverStation.isEnabled() && !brakeMode) { + brakeMode = true; + setBrakeMode(true); + brakeModeTimer.restart(); + + } else { + boolean stillMoving = false; + for (SwerveModule mod : swerveModules) { + if (Math.abs(mod.getState().speedMetersPerSecond) + > RobotConfig.getInstance().getRobotMaxCoastVelocity()) { + stillMoving = true; + brakeModeTimer.restart(); + } + } + + if (brakeMode && !stillMoving && brakeModeTimer.hasElapsed(BREAK_MODE_DELAY_SEC)) { + brakeMode = false; + setBrakeMode(false); + } + } + } + + private void setBrakeMode(boolean enable) { + for (SwerveModule mod : swerveModules) { + mod.setAngleBrakeMode(enable); + mod.setDriveBrakeMode(enable); + } + } + private enum DriveMode { NORMAL, X, diff --git a/src/main/java/frc/robot/subsystems/subsystem/Subsystem.java b/src/main/java/frc/robot/subsystems/subsystem/Subsystem.java new file mode 100644 index 00000000..152fc173 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/subsystem/Subsystem.java @@ -0,0 +1,95 @@ +package frc.robot.subsystems.subsystem; + +import static frc.robot.subsystems.subsystem.SubsystemConstants.*; + +import edu.wpi.first.wpilibj.shuffleboard.Shuffleboard; +import edu.wpi.first.wpilibj.shuffleboard.ShuffleboardTab; +import edu.wpi.first.wpilibj2.command.SubsystemBase; +import frc.lib.team6328.util.TunableNumber; +import org.littletonrobotics.junction.Logger; + +/** + * Models a generic subsystem for a rotational mechanism. The other subsystems defined in this + * library aren't good examples for typical robot subsystems. This class can serve as an example or + * be used for quick prototyping. + */ +public class Subsystem extends SubsystemBase { + + // these Tunables are convenient when testing as they provide direct control of the subsystem's + // motor + private final TunableNumber motorPower = new TunableNumber("Subsystem/power", 0.0); + private final TunableNumber motorCurrent = new TunableNumber("Subsystem/current", 0.0); + private final TunableNumber motorPosition = new TunableNumber("Subsystem/position", 0.0); + + private final SubsystemIOInputsAutoLogged inputs = new SubsystemIOInputsAutoLogged(); + private SubsystemIO io; + + /** + * Create a new subsystem with its associated hardware interface object. + * + * @param io the hardware interface object for this subsystem + */ + public Subsystem(SubsystemIO io) { + + this.io = io; + + // Create a Shuffleboard tab for this subsystem if testing is enabled. Add additional indicators + // and controls as needed. + if (TESTING) { + ShuffleboardTab tab = Shuffleboard.getTab(SUBSYSTEM_NAME); + tab.add(SUBSYSTEM_NAME, this); + } + } + + /** + * The subsystem's periodic method needs to update and process the inputs from the hardware + * interface object. + */ + @Override + public void periodic() { + io.updateInputs(inputs); + Logger.getInstance().processInputs("Subsystem", inputs); + + // when testing, set the motor power, current, or position based on the Tunables (if non-zero) + if (TESTING) { + if (motorPower.get() != 0) { + this.setMotorPower(motorPower.get()); + } + + if (motorCurrent.get() != 0) { + this.setMotorCurrent(motorCurrent.get()); + } + + if (motorPosition.get() != 0) { + this.setMotorPosition(motorPosition.get()); + } + } + } + + /** + * Set the motor power to the specified percentage of maximum power. + * + * @param power the percentage of maximum power to set the motor to + */ + public void setMotorPower(double power) { + io.setMotorPower(power); + } + + /** + * Set the motor current to the specified value in amps. + * + * @param power the current to set the motor to in amps + */ + public void setMotorCurrent(double power) { + io.setMotorCurrent(power); + } + + /** + * Set the motor position to the specified value in degrees. + * + * @param position the position to set the motor to in degrees + */ + public void setMotorPosition(double position) { + io.setMotorPosition(position, POSITION_FEEDFORWARD); + } +} diff --git a/src/main/java/frc/robot/subsystems/subsystem/SubsystemConstants.java b/src/main/java/frc/robot/subsystems/subsystem/SubsystemConstants.java new file mode 100644 index 00000000..592e87fb --- /dev/null +++ b/src/main/java/frc/robot/subsystems/subsystem/SubsystemConstants.java @@ -0,0 +1,36 @@ +package frc.robot.subsystems.subsystem; + +import com.ctre.phoenix.motorcontrol.StatorCurrentLimitConfiguration; + +public class SubsystemConstants { + + private static final String CONSTRUCTOR_EXCEPTION = "constant class"; + + private SubsystemConstants() { + throw new IllegalStateException(CONSTRUCTOR_EXCEPTION); + } + + public static final boolean DEBUGGING = true; + public static final boolean TESTING = true; + public static final String SUBSYSTEM_NAME = "Subsystem"; + + public static final int MOTOR_CAN_ID = 6; + public static final double GEAR_RATIO = 100.0; + public static final boolean MOTOR_INVERTED = false; + + public static final double POSITION_PID_F = 0.0; + public static final double POSITION_PID_P = 0.0; + public static final double POSITION_PID_I = 0; + public static final double POSITION_PID_D = 0; + public static final double POSITION_PID_I_ZONE = 0; + public static final double POSITION_PID_PEAK_OUTPUT = 1.0; + public static final double POSITION_FEEDFORWARD = 0; + + public static final double CURRENT_LIMIT = 40; + + public static final StatorCurrentLimitConfiguration CURRENT_LIMIT_CONFIG = + new StatorCurrentLimitConfiguration(true, CURRENT_LIMIT, 50, 0.5); + + public static final int TIMEOUT_MS = 30; + public static final int SLOT_INDEX = 0; +} diff --git a/src/main/java/frc/robot/subsystems/subsystem/SubsystemIO.java b/src/main/java/frc/robot/subsystems/subsystem/SubsystemIO.java new file mode 100644 index 00000000..493825f5 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/subsystem/SubsystemIO.java @@ -0,0 +1,46 @@ +package frc.robot.subsystems.subsystem; + +import org.littletonrobotics.junction.AutoLog; + +/** Generic subsystem hardware interface. */ +public interface SubsystemIO { + /** Contains all of the input data received from hardware. */ + @AutoLog + public static class SubsystemIOInputs { + double positionDeg = 0.0; + double velocityRPM = 0.0; + double closedLoopError = 0.0; + double appliedVoltage = 0.0; + double setpoint = 0.0; + double power = 0.0; + String controlMode = ""; + double[] statorCurrentAmps = new double[] {}; + double[] tempCelsius = new double[] {}; + double[] supplyCurrentAmps = new double[] {}; + } + + /** Updates the set of loggable inputs. */ + public default void updateInputs(SubsystemIOInputs inputs) {} + + /** + * Set the motor power to the specified percentage of maximum power. + * + * @param power the percentage of maximum power to set the motor to + */ + public default void setMotorPower(double power) {} + + /** + * Set the motor current to the specified value in amps. + * + * @param power the current to set the motor to in amps + */ + public default void setMotorCurrent(double current) {} + + /** + * Set the motor position to the specified value in degrees. + * + * @param position the position to set the motor to in degrees + * @param arbitraryFeedForward the arbitrary feed forward as a percentage of maximum power + */ + public default void setMotorPosition(double position, double arbitraryFeedForward) {} +} diff --git a/src/main/java/frc/robot/subsystems/subsystem/SubsystemIOTalonFX.java b/src/main/java/frc/robot/subsystems/subsystem/SubsystemIOTalonFX.java new file mode 100644 index 00000000..0a4240f6 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/subsystem/SubsystemIOTalonFX.java @@ -0,0 +1,118 @@ +package frc.robot.subsystems.subsystem; + +import static frc.robot.subsystems.subsystem.SubsystemConstants.*; + +import com.ctre.phoenix.motorcontrol.ControlMode; +import com.ctre.phoenix.motorcontrol.DemandType; +import com.ctre.phoenix.motorcontrol.NeutralMode; +import com.ctre.phoenix.motorcontrol.TalonFXControlMode; +import com.ctre.phoenix.motorcontrol.can.TalonFX; +import frc.lib.team254.drivers.TalonFXFactory; +import frc.lib.team3061.RobotConfig; +import frc.lib.team3061.swerve.Conversions; +import frc.lib.team6328.util.TunableNumber; + +/** TalonFX implementation of the generic SubsystemIO */ +public class SubsystemIOTalonFX implements SubsystemIO { + private TalonFX motor; + + private final TunableNumber kP = new TunableNumber("Subsystem/kP", POSITION_PID_P); + private final TunableNumber kI = new TunableNumber("Subsystem/kI", POSITION_PID_I); + private final TunableNumber kD = new TunableNumber("Subsystem/kD", POSITION_PID_D); + private final TunableNumber kF = new TunableNumber("Subsystem/kF", POSITION_PID_F); + private final TunableNumber kIz = new TunableNumber("Subsystem/kIz", POSITION_PID_I_ZONE); + private final TunableNumber kPeakOutput = + new TunableNumber("Subsystem/kPeakOutput", POSITION_PID_PEAK_OUTPUT); + + /** Create a TalonFX-specific generic SubsystemIO */ + public SubsystemIOTalonFX() { + configMotor(MOTOR_CAN_ID); + } + + /** + * Update the inputs based on the current state of the TalonFX motor controller. + * + * @param inputs the inputs object to update + */ + @Override + public void updateInputs(SubsystemIOInputs inputs) { + inputs.positionDeg = Conversions.falconToDegrees(motor.getSelectedSensorPosition(), GEAR_RATIO); + inputs.velocityRPM = Conversions.falconToRPM(motor.getSelectedSensorVelocity(), GEAR_RATIO); + inputs.closedLoopError = motor.getClosedLoopError(); + inputs.appliedVoltage = motor.getMotorOutputVoltage(); + inputs.setpoint = motor.getClosedLoopTarget(); + inputs.power = motor.getMotorOutputPercent(); + inputs.controlMode = motor.getControlMode().toString(); + inputs.statorCurrentAmps = new double[] {motor.getStatorCurrent()}; + inputs.tempCelsius = new double[] {motor.getTemperature()}; + inputs.supplyCurrentAmps = new double[] {motor.getSupplyCurrent()}; + + // update configuration if tunables have changed + if (kP.hasChanged() + || kI.hasChanged() + || kD.hasChanged() + || kF.hasChanged() + || kIz.hasChanged() + || kPeakOutput.hasChanged()) { + motor.config_kF(0, kF.get()); + motor.config_kP(0, kP.get()); + motor.config_kI(0, kI.get()); + motor.config_kD(0, kD.get()); + motor.config_IntegralZone(0, kIz.get()); + motor.configClosedLoopPeakOutput(0, kPeakOutput.get()); + } + } + + /** + * Set the motor power to the specified percentage of maximum power. + * + * @param power the percentage of maximum power to set the motor to + */ + @Override + public void setMotorPower(double power) { + motor.set(ControlMode.PercentOutput, power); + } + + /** + * Set the motor current to the specified value in amps. + * + * @param power the current to set the motor to in amps + */ + @Override + public void setMotorCurrent(double current) { + motor.set(ControlMode.Current, current); + } + + /** + * Set the motor position to the specified value in degrees. + * + * @param position the position to set the motor to in degrees + * @param arbitraryFeedForward the arbitrary feed forward as a percentage of maximum power + */ + @Override + public void setMotorPosition(double position, double arbitraryFeedForward) { + motor.set( + TalonFXControlMode.Position, + Conversions.degreesToFalcon(position, GEAR_RATIO), + DemandType.ArbitraryFeedForward, + arbitraryFeedForward); + } + + private void configMotor(int motorID) { + TalonFXFactory.Configuration motorConfig = new TalonFXFactory.Configuration(); + motorConfig.INVERTED = MOTOR_INVERTED; + motorConfig.BRUSHLESS_CURRENT_STATUS_FRAME_RATE_MS = 9; + motorConfig.NEUTRAL_MODE = NeutralMode.Brake; + motorConfig.STATOR_CURRENT_LIMIT = CURRENT_LIMIT_CONFIG; + motorConfig.SLOT0_KP = kP.get(); + motorConfig.SLOT0_KI = kI.get(); + motorConfig.SLOT0_KD = kD.get(); + motorConfig.SLOT0_KF = kF.get(); + motorConfig.NEUTRAL_DEADBAND = 0.001; + motor = + TalonFXFactory.createTalon(motorID, RobotConfig.getInstance().getCANBusName(), motorConfig); + motor.setSelectedSensorPosition(0); + motor.configClosedLoopPeakOutput(0, kPeakOutput.get()); + motor.config_IntegralZone(0, kIz.get()); + } +} diff --git a/vendordeps/AdvantageKit.json b/vendordeps/AdvantageKit.json index cae0375b..fa08705a 100644 --- a/vendordeps/AdvantageKit.json +++ b/vendordeps/AdvantageKit.json @@ -1,7 +1,7 @@ { "fileName": "AdvantageKit.json", "name": "AdvantageKit", - "version": "2.1.2", + "version": "2.2.4", "uuid": "d820cc26-74e3-11ec-90d6-0242ac120003", "mavenUrls": [], "jsonUrl": "https://github.com/Mechanical-Advantage/AdvantageKit/releases/latest/download/AdvantageKit.json", @@ -9,24 +9,24 @@ { "groupId": "org.littletonrobotics.akit.junction", "artifactId": "wpilib-shim", - "version": "2.1.2" + "version": "2.2.4" }, { "groupId": "org.littletonrobotics.akit.junction", "artifactId": "junction-core", - "version": "2.1.2" + "version": "2.2.4" }, { "groupId": "org.littletonrobotics.akit.conduit", "artifactId": "conduit-api", - "version": "2.1.2" + "version": "2.2.4" } ], "jniDependencies": [ { "groupId": "org.littletonrobotics.akit.conduit", "artifactId": "conduit-wpilibio", - "version": "2.1.2", + "version": "2.2.4", "skipInvalidPlatforms": false, "isJar": false, "validPlatforms": [ diff --git a/vendordeps/PathplannerLib.json b/vendordeps/PathplannerLib.json index 53c27617..8e61586b 100644 --- a/vendordeps/PathplannerLib.json +++ b/vendordeps/PathplannerLib.json @@ -1,7 +1,7 @@ { "fileName": "PathplannerLib.json", "name": "PathplannerLib", - "version": "2023.4.1", + "version": "2023.4.4", "uuid": "1b42324f-17c6-4875-8e77-1c312bc8c786", "mavenUrls": [ "https://3015rangerrobotics.github.io/pathplannerlib/repo" @@ -11,7 +11,7 @@ { "groupId": "com.pathplanner.lib", "artifactId": "PathplannerLib-java", - "version": "2023.4.1" + "version": "2023.4.4" } ], "jniDependencies": [], @@ -19,7 +19,7 @@ { "groupId": "com.pathplanner.lib", "artifactId": "PathplannerLib-cpp", - "version": "2023.4.1", + "version": "2023.4.4", "libName": "PathplannerLib", "headerClassifier": "headers", "sharedLibrary": false, diff --git a/vendordeps/Phoenix.json b/vendordeps/Phoenix.json index 55bc0990..04bb6597 100644 --- a/vendordeps/Phoenix.json +++ b/vendordeps/Phoenix.json @@ -1,7 +1,7 @@ { "fileName": "Phoenix.json", "name": "CTRE-Phoenix (v5)", - "version": "5.30.4", + "version": "5.30.4+23.0.11", "frcYear": 2023, "uuid": "ab676553-b602-441f-a38d-f1296eff6537", "mavenUrls": [ @@ -50,7 +50,7 @@ { "groupId": "com.ctre.phoenixpro", "artifactId": "tools", - "version": "23.0.5", + "version": "23.0.11", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -63,7 +63,7 @@ { "groupId": "com.ctre.phoenixpro.sim", "artifactId": "tools-sim", - "version": "23.0.5", + "version": "23.0.11", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -76,7 +76,7 @@ { "groupId": "com.ctre.phoenixpro.sim", "artifactId": "simTalonSRX", - "version": "23.0.5", + "version": "23.0.11", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -89,7 +89,7 @@ { "groupId": "com.ctre.phoenixpro.sim", "artifactId": "simTalonFX", - "version": "23.0.5", + "version": "23.0.11", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -102,7 +102,7 @@ { "groupId": "com.ctre.phoenixpro.sim", "artifactId": "simVictorSPX", - "version": "23.0.5", + "version": "23.0.11", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -115,7 +115,7 @@ { "groupId": "com.ctre.phoenixpro.sim", "artifactId": "simPigeonIMU", - "version": "23.0.5", + "version": "23.0.11", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -128,7 +128,7 @@ { "groupId": "com.ctre.phoenixpro.sim", "artifactId": "simCANCoder", - "version": "23.0.5", + "version": "23.0.11", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -141,7 +141,7 @@ { "groupId": "com.ctre.phoenixpro.sim", "artifactId": "simProTalonFX", - "version": "23.0.5", + "version": "23.0.11", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -154,7 +154,7 @@ { "groupId": "com.ctre.phoenixpro.sim", "artifactId": "simProCANcoder", - "version": "23.0.5", + "version": "23.0.11", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -167,7 +167,7 @@ { "groupId": "com.ctre.phoenixpro.sim", "artifactId": "simProPigeon2", - "version": "23.0.5", + "version": "23.0.11", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -227,7 +227,7 @@ { "groupId": "com.ctre.phoenixpro", "artifactId": "tools", - "version": "23.0.5", + "version": "23.0.11", "libName": "CTRE_PhoenixTools", "headerClassifier": "headers", "sharedLibrary": true, @@ -287,7 +287,7 @@ { "groupId": "com.ctre.phoenixpro.sim", "artifactId": "tools-sim", - "version": "23.0.5", + "version": "23.0.11", "libName": "CTRE_PhoenixTools_Sim", "headerClassifier": "headers", "sharedLibrary": true, @@ -302,7 +302,7 @@ { "groupId": "com.ctre.phoenixpro.sim", "artifactId": "simTalonSRX", - "version": "23.0.5", + "version": "23.0.11", "libName": "CTRE_SimTalonSRX", "headerClassifier": "headers", "sharedLibrary": true, @@ -317,7 +317,7 @@ { "groupId": "com.ctre.phoenixpro.sim", "artifactId": "simTalonFX", - "version": "23.0.5", + "version": "23.0.11", "libName": "CTRE_SimTalonFX", "headerClassifier": "headers", "sharedLibrary": true, @@ -332,7 +332,7 @@ { "groupId": "com.ctre.phoenixpro.sim", "artifactId": "simVictorSPX", - "version": "23.0.5", + "version": "23.0.11", "libName": "CTRE_SimVictorSPX", "headerClassifier": "headers", "sharedLibrary": true, @@ -347,7 +347,7 @@ { "groupId": "com.ctre.phoenixpro.sim", "artifactId": "simPigeonIMU", - "version": "23.0.5", + "version": "23.0.11", "libName": "CTRE_SimPigeonIMU", "headerClassifier": "headers", "sharedLibrary": true, @@ -362,7 +362,7 @@ { "groupId": "com.ctre.phoenixpro.sim", "artifactId": "simCANCoder", - "version": "23.0.5", + "version": "23.0.11", "libName": "CTRE_SimCANCoder", "headerClassifier": "headers", "sharedLibrary": true, @@ -377,7 +377,7 @@ { "groupId": "com.ctre.phoenixpro.sim", "artifactId": "simProTalonFX", - "version": "23.0.5", + "version": "23.0.11", "libName": "CTRE_SimProTalonFX", "headerClassifier": "headers", "sharedLibrary": true, @@ -392,7 +392,7 @@ { "groupId": "com.ctre.phoenixpro.sim", "artifactId": "simProCANcoder", - "version": "23.0.5", + "version": "23.0.11", "libName": "CTRE_SimProCANcoder", "headerClassifier": "headers", "sharedLibrary": true, @@ -407,7 +407,7 @@ { "groupId": "com.ctre.phoenixpro.sim", "artifactId": "simProPigeon2", - "version": "23.0.5", + "version": "23.0.11", "libName": "CTRE_SimProPigeon2", "headerClassifier": "headers", "sharedLibrary": true, diff --git a/vendordeps/photonlib.json b/vendordeps/photonlib.json index 4f378c49..dad31057 100644 --- a/vendordeps/photonlib.json +++ b/vendordeps/photonlib.json @@ -1,7 +1,7 @@ { "fileName": "photonlib.json", "name": "photonlib", - "version": "v2023.3.0", + "version": "v2023.4.2", "uuid": "515fe07e-bfc6-11fa-b3de-0242ac130004 ", "mavenUrls": [ "https://maven.photonvision.org/repository/internal", @@ -13,7 +13,7 @@ { "groupId": "org.photonvision", "artifactId": "PhotonLib-cpp", - "version": "v2023.3.0", + "version": "v2023.4.2", "libName": "Photon", "headerClassifier": "headers", "sharedLibrary": true, @@ -30,12 +30,12 @@ { "groupId": "org.photonvision", "artifactId": "PhotonLib-java", - "version": "v2023.3.0" + "version": "v2023.4.2" }, { "groupId": "org.photonvision", "artifactId": "PhotonTargeting-java", - "version": "v2023.3.0" + "version": "v2023.4.2" } ] } \ No newline at end of file