From 186bcc7b8833792dac3ce2fbabeca2007f5881df Mon Sep 17 00:00:00 2001 From: wilsonwatson Date: Sun, 4 Jan 2026 16:53:12 -0600 Subject: [PATCH 1/5] bring in from jetsi repo --- .vscode/settings.json | 10 +- build.gradle | 3 +- gradle.properties | 1 + gradlew | 0 limelight pipelines/2022_Shop_Targeting.vpr | 68 -- shuffleboard config/shuffleboard.json | 868 ------------------ src/main/java/frc/lib/math/Conversions.java | 94 -- src/main/java/frc/robot/Constants.java | 217 ++++- src/main/java/frc/robot/Robot.java | 65 +- src/main/java/frc/robot/RobotContainer.java | 105 ++- src/main/java/frc/robot/autos/.keep | 0 src/main/java/frc/robot/commands/.keep | 0 src/main/java/frc/robot/modules/.keep | 0 .../robot/subsystems/drive/Drivetrain.java | 27 - .../robot/subsystems/drive/DrivetrainIO.java | 25 - .../subsystems/drive/DrivetrainReal.java | 26 - .../frc/robot/subsystems/swerve/Swerve.java | 418 +++++++++ .../frc/robot/subsystems/swerve/SwerveIO.java | 26 + .../robot/subsystems/swerve/SwerveReal.java | 28 + .../robot/subsystems/swerve/SwerveSim.java | 71 ++ .../robot/subsystems/swerve/gyro/GyroIO.java | 31 + .../subsystems/swerve/gyro/GyroNavX2.java | 41 + .../robot/subsystems/swerve/gyro/GyroSim.java | 26 + .../swerve/mod/ModuleConstants.java | 34 + .../subsystems/swerve/mod/SwerveModule.java | 110 +++ .../subsystems/swerve/mod/SwerveModuleIO.java | 66 ++ .../swerve/mod/SwerveModuleReal.java | 299 ++++++ .../swerve/mod/SwerveModuleSim.java | 142 +++ .../subsystems/swerve/util/MoveToPose.java | 191 ++++ .../swerve/util/PhoenixOdometryThread.java | 233 +++++ .../swerve/util/SwerveArcOdometry.java | 190 ++++ .../swerve/util/SwerveRateLimiter.java | 224 +++++ .../subsystems/swerve/util/SwerveState.java | 320 +++++++ .../swerve/util/TeleopControls.java | 71 ++ .../swerve/util/TuningCommands.java | 221 +++++ .../subsystems/vision/CameraConstants.java | 64 ++ .../frc/robot/subsystems/vision/Vision.java | 128 +++ .../frc/robot/subsystems/vision/VisionIO.java | 92 ++ .../robot/subsystems/vision/VisionReal.java | 31 + .../robot/subsystems/vision/VisionSim.java | 48 + .../java/frc/robot/util/AllianceFlipUtil.java | 48 + src/main/java/frc/robot/util/DeviceDebug.java | 145 +++ .../java/frc/robot/util/GenerateEmptyIO.java | 16 + .../java/frc/robot/util/PhoenixSignals.java | 108 +++ .../java/frc/robot/util/SparkSignals.java | 158 ++++ src/main/java/frc/robot/util/Tuples.java | 565 ++++++++++++ .../frc/robot/util/typestate/AltMethod.java | 15 + .../frc/robot/util/typestate/InitField.java | 12 + .../robot/util/typestate/OptionalField.java | 12 + .../robot/util/typestate/RequiredField.java | 8 + .../util/typestate/TypeStateBuilder.java | 19 + src/main/java/frc/robot/viz/RobotViz.java | 87 ++ vendordeps/ChoreoLib-2025.0.3.json | 44 - vendordeps/ChoreoLib2025.json | 44 + vendordeps/ReduxLib-2025.0.1.json | 72 ++ vendordeps/Studica-2025.0.1.json | 71 ++ vendordeps/photonlib.json | 71 ++ 57 files changed, 4841 insertions(+), 1268 deletions(-) create mode 100644 gradle.properties mode change 100644 => 100755 gradlew delete mode 100644 limelight pipelines/2022_Shop_Targeting.vpr delete mode 100644 shuffleboard config/shuffleboard.json delete mode 100644 src/main/java/frc/lib/math/Conversions.java delete mode 100644 src/main/java/frc/robot/autos/.keep delete mode 100644 src/main/java/frc/robot/commands/.keep delete mode 100644 src/main/java/frc/robot/modules/.keep delete mode 100644 src/main/java/frc/robot/subsystems/drive/Drivetrain.java delete mode 100644 src/main/java/frc/robot/subsystems/drive/DrivetrainIO.java delete mode 100644 src/main/java/frc/robot/subsystems/drive/DrivetrainReal.java create mode 100644 src/main/java/frc/robot/subsystems/swerve/Swerve.java create mode 100644 src/main/java/frc/robot/subsystems/swerve/SwerveIO.java create mode 100644 src/main/java/frc/robot/subsystems/swerve/SwerveReal.java create mode 100644 src/main/java/frc/robot/subsystems/swerve/SwerveSim.java create mode 100644 src/main/java/frc/robot/subsystems/swerve/gyro/GyroIO.java create mode 100644 src/main/java/frc/robot/subsystems/swerve/gyro/GyroNavX2.java create mode 100644 src/main/java/frc/robot/subsystems/swerve/gyro/GyroSim.java create mode 100644 src/main/java/frc/robot/subsystems/swerve/mod/ModuleConstants.java create mode 100644 src/main/java/frc/robot/subsystems/swerve/mod/SwerveModule.java create mode 100644 src/main/java/frc/robot/subsystems/swerve/mod/SwerveModuleIO.java create mode 100644 src/main/java/frc/robot/subsystems/swerve/mod/SwerveModuleReal.java create mode 100644 src/main/java/frc/robot/subsystems/swerve/mod/SwerveModuleSim.java create mode 100644 src/main/java/frc/robot/subsystems/swerve/util/MoveToPose.java create mode 100644 src/main/java/frc/robot/subsystems/swerve/util/PhoenixOdometryThread.java create mode 100644 src/main/java/frc/robot/subsystems/swerve/util/SwerveArcOdometry.java create mode 100644 src/main/java/frc/robot/subsystems/swerve/util/SwerveRateLimiter.java create mode 100644 src/main/java/frc/robot/subsystems/swerve/util/SwerveState.java create mode 100644 src/main/java/frc/robot/subsystems/swerve/util/TeleopControls.java create mode 100644 src/main/java/frc/robot/subsystems/swerve/util/TuningCommands.java create mode 100644 src/main/java/frc/robot/subsystems/vision/CameraConstants.java create mode 100644 src/main/java/frc/robot/subsystems/vision/Vision.java create mode 100644 src/main/java/frc/robot/subsystems/vision/VisionIO.java create mode 100644 src/main/java/frc/robot/subsystems/vision/VisionReal.java create mode 100644 src/main/java/frc/robot/subsystems/vision/VisionSim.java create mode 100644 src/main/java/frc/robot/util/AllianceFlipUtil.java create mode 100644 src/main/java/frc/robot/util/DeviceDebug.java create mode 100644 src/main/java/frc/robot/util/GenerateEmptyIO.java create mode 100644 src/main/java/frc/robot/util/PhoenixSignals.java create mode 100644 src/main/java/frc/robot/util/SparkSignals.java create mode 100644 src/main/java/frc/robot/util/Tuples.java create mode 100644 src/main/java/frc/robot/util/typestate/AltMethod.java create mode 100644 src/main/java/frc/robot/util/typestate/InitField.java create mode 100644 src/main/java/frc/robot/util/typestate/OptionalField.java create mode 100644 src/main/java/frc/robot/util/typestate/RequiredField.java create mode 100644 src/main/java/frc/robot/util/typestate/TypeStateBuilder.java create mode 100644 src/main/java/frc/robot/viz/RobotViz.java delete mode 100644 vendordeps/ChoreoLib-2025.0.3.json create mode 100644 vendordeps/ChoreoLib2025.json create mode 100644 vendordeps/ReduxLib-2025.0.1.json create mode 100644 vendordeps/Studica-2025.0.1.json create mode 100644 vendordeps/photonlib.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 6cf194b..5c330e4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,9 +18,11 @@ { "name": "WPIlibUnitTests", "workingDirectory": "${workspaceFolder}/build/jni/release", - "vmargs": [ "-Djava.library.path=${workspaceFolder}/build/jni/release" ], + "vmargs": [ + "-Djava.library.path=${workspaceFolder}/build/jni/release" + ], "env": { - "LD_LIBRARY_PATH": "${workspaceFolder}/build/jni/release" , + "LD_LIBRARY_PATH": "${workspaceFolder}/build/jni/release", "DYLD_LIBRARY_PATH": "${workspaceFolder}/build/jni/release" } }, @@ -67,5 +69,7 @@ }, "editor.formatOnSave": true, "editor.formatOnPaste": true, - "terminal.integrated.defaultProfile.windows": "Git Bash" + "terminal.integrated.defaultProfile.windows": "Git Bash", + "java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx16G -Xms100m -Xlog:disable", + "java.compile.nullAnalysis.mode": "disabled" } diff --git a/build.gradle b/build.gradle index 4e5ecde..7ae8be7 100644 --- a/build.gradle +++ b/build.gradle @@ -82,7 +82,8 @@ dependencies { nativeRelease wpi.java.vendor.jniRelease(wpi.platforms.desktop) simulationRelease wpi.sim.enableRelease() - annotationProcessor 'com.github.Frc5572:RobotTools:v1.1.1' + annotationProcessor 'com.github.Frc5572:RobotTools:io-gen-SNAPSHOT' + implementation 'org.jspecify:jspecify:1.0.0' implementation 'org.apache.httpcomponents:httpclient:4.5.14' implementation 'org.apache.httpcomponents:httpmime:4.5.14' diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..c7df913 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +org.gradle.jvmargs=-Xms512m -Xmx8g diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/limelight pipelines/2022_Shop_Targeting.vpr b/limelight pipelines/2022_Shop_Targeting.vpr deleted file mode 100644 index d59cb87..0000000 --- a/limelight pipelines/2022_Shop_Targeting.vpr +++ /dev/null @@ -1,68 +0,0 @@ -area_max:100 -area_min:0.0026873856000000015 -area_similarity:26 -aspect_max:20 -aspect_min:0 -black_level:40 -blue_balance:2500 -calibration_type:0 -contour_grouping:3 -contour_sort_final:0 -convexity_max:100 -convexity_min:80 -corner_approx:5.000000 -crop_x_max:1 -crop_x_min:-1 -crop_y_max:1.000000 -crop_y_min:-1.000000 -cross_a_a:1 -cross_a_x:0 -cross_a_y:0 -cross_b_a:1 -cross_b_x:0 -cross_b_y:0 -desc:Shop_Targeting -desired_contour_region:0 -dilation_steps:1 -direction_filter:0 -dual_close_sort_origin:0 -erosion_steps:0 -exposure:3 -force_convex:1 -hue_max:94 -hue_min:22 -image_flip:0 -image_source:0 -img_to_show:0 -intersection_filter:3 -invert_hue:0 -multigroup_max:5 -multigroup_min:2 -multigroup_rejector:1 -pipeline_led_enabled:1 -pipeline_led_power:100 -pipeline_res:0 -pipeline_type:0 -red_balance:2500 -roi_x:0.000000 -roi_y:0.000000 -sat_max:255 -sat_min:83 -send_corners:0 -send_raw_contours:0 -solve3d:0 -solve3d_algo:0 -solve3d_bindtarget:1 -solve3d_conf:0.990000 -solve3d_error:8 -solve3d_guess:0 -solve3d_iterations:50 -solve3d_precies:1 -solve3d_precise:0 -solve3d_zoffset:0.000000 -val_max:220 -val_min:18 -x_outlier_miqr:1 -y_max:1.000000 -y_min:-1.000000 -y_outlier_miqr:2.5 diff --git a/shuffleboard config/shuffleboard.json b/shuffleboard config/shuffleboard.json deleted file mode 100644 index 631b51d..0000000 --- a/shuffleboard config/shuffleboard.json +++ /dev/null @@ -1,868 +0,0 @@ -{ - "tabPane": [ - { - "title": "SmartDashboard", - "autoPopulate": true, - "autoPopulatePrefix": "SmartDashboard/", - "widgetPane": { - "gridSize": 128.0, - "showGrid": true, - "hgap": 16.0, - "vgap": 16.0, - "titleType": 0, - "tiles": { - "0,0": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/limelight_PipelineName", - "_title": "limelight_PipelineName" - } - }, - "1,0": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/Mod 2 Integrated", - "_title": "Mod 2 Integrated" - } - }, - "2,0": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "ComboBox Chooser", - "_source0": "network_table:///SmartDashboard/Choose Auto: ", - "_title": "Choose Auto: " - } - }, - "3,0": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/Mod 0 Velocity", - "_title": "Mod 0 Velocity" - } - }, - "4,0": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/Mod 3 Cancoder", - "_title": "Mod 3 Cancoder" - } - }, - "5,0": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/Mod 0 Cancoder", - "_title": "Mod 0 Cancoder" - } - }, - "6,0": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/Mod 0 Integrated", - "_title": "Mod 0 Integrated" - } - }, - "7,0": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Boolean Box", - "_source0": "network_table:///SmartDashboard/Magazine Switch", - "_title": "Magazine Switch", - "Colors/Color when true": "#7CFC00FF", - "Colors/Color when false": "#8B0000FF" - } - }, - "8,0": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/Mod 3 Integrated", - "_title": "Mod 3 Integrated" - } - }, - "9,0": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/Mod 1 Velocity", - "_title": "Mod 1 Velocity" - } - }, - "10,0": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/limelight_Interface", - "_title": "limelight_Interface" - } - }, - "11,0": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/Mod 1 Cancoder", - "_title": "Mod 1 Cancoder" - } - }, - "12,0": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/Mod 1 Integrated", - "_title": "Mod 1 Integrated" - } - }, - "0,1": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/Mod 2 Velocity", - "_title": "Mod 2 Velocity" - } - }, - "1,1": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/Mod 2 Cancoder", - "_title": "Mod 2 Cancoder" - } - }, - "2,1": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/limelight_Stream", - "_title": "limelight_Stream" - } - }, - "3,1": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/Mod 3 Velocity", - "_title": "Mod 3 Velocity" - } - }, - "4,1": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/SetPoint (RPM)", - "_title": "SetPoint (RPM)" - } - }, - "5,1": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/DB/Slider 0", - "_title": "DB/Slider 0" - } - }, - "6,1": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/DB/Slider 1", - "_title": "DB/Slider 1" - } - }, - "7,1": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/DB/Slider 2", - "_title": "DB/Slider 2" - } - }, - "8,1": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/DB/Slider 3", - "_title": "DB/Slider 3" - } - }, - "9,1": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/Auto Selector", - "_title": "Auto Selector" - } - }, - "10,1": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Boolean Box", - "_source0": "network_table:///SmartDashboard/DB/Button 0", - "_title": "DB/Button 0", - "Colors/Color when true": "#7CFC00FF", - "Colors/Color when false": "#8B0000FF" - } - }, - "11,1": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Boolean Box", - "_source0": "network_table:///SmartDashboard/DB/Button 1", - "_title": "DB/Button 1", - "Colors/Color when true": "#7CFC00FF", - "Colors/Color when false": "#8B0000FF" - } - }, - "12,1": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Boolean Box", - "_source0": "network_table:///SmartDashboard/DB/Button 2", - "_title": "DB/Button 2", - "Colors/Color when true": "#7CFC00FF", - "Colors/Color when false": "#8B0000FF" - } - }, - "0,2": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Boolean Box", - "_source0": "network_table:///SmartDashboard/DB/Button 3", - "_title": "DB/Button 3", - "Colors/Color when true": "#7CFC00FF", - "Colors/Color when false": "#8B0000FF" - } - }, - "1,2": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/DB/String 0", - "_title": "DB/String 0" - } - }, - "2,2": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/DB/String 1", - "_title": "DB/String 1" - } - }, - "3,2": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/DB/String 2", - "_title": "DB/String 2" - } - }, - "4,2": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/DB/String 3", - "_title": "DB/String 3" - } - }, - "5,2": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/DB/String 4", - "_title": "DB/String 4" - } - }, - "6,2": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/DB/String 5", - "_title": "DB/String 5" - } - }, - "7,2": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/DB/String 6", - "_title": "DB/String 6" - } - }, - "8,2": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/DB/String 7", - "_title": "DB/String 7" - } - }, - "9,2": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/DB/String 8", - "_title": "DB/String 8" - } - }, - "10,2": { - "size": [ - 1, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///SmartDashboard/DB/String 9", - "_title": "DB/String 9" - } - } - } - } - }, - { - "title": "LiveWindow", - "autoPopulate": true, - "autoPopulatePrefix": "LiveWindow/", - "widgetPane": { - "gridSize": 128.0, - "showGrid": true, - "hgap": 16.0, - "vgap": 16.0, - "titleType": 0, - "tiles": { - "0,0": { - "size": [ - 2, - 4 - ], - "content": { - "_type": "Subsystem Layout", - "_source0": "network_table:///LiveWindow/Ungrouped", - "_title": "Ungrouped", - "Layout/Label position": "BOTTOM", - "_children": [ - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/PIDController[16]", - "_title": "PIDController[16]" - }, - { - "_type": "Text View", - "_source0": "network_table:///LiveWindow/Ungrouped/DoubleSolenoid[1,2]/Value", - "_title": "DoubleSolenoid[1,2]/Value" - }, - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/PIDController[25]", - "_title": "PIDController[25]" - }, - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/PIDController[13]", - "_title": "PIDController[13]" - }, - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/PIDController[17]", - "_title": "PIDController[17]" - }, - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/PIDController[14]", - "_title": "PIDController[14]" - }, - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/PIDController[22]", - "_title": "PIDController[22]" - }, - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/PIDController[26]", - "_title": "PIDController[26]" - }, - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/PIDController[6]", - "_title": "PIDController[6]" - }, - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/PIDController[23]", - "_title": "PIDController[23]" - }, - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/PIDController[7]", - "_title": "PIDController[7]" - }, - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/PIDController[18]", - "_title": "PIDController[18]" - }, - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/PIDController[8]", - "_title": "PIDController[8]" - }, - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/PIDController[11]", - "_title": "PIDController[11]" - }, - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/PIDController[19]", - "_title": "PIDController[19]" - }, - { - "_type": "Gyro", - "_source0": "network_table:///LiveWindow/Ungrouped/navX-Sensor[4]", - "_title": "navX-Sensor[4]", - "Visuals/Major tick spacing": 45.0, - "Visuals/Starting angle": 180.0, - "Visuals/Show tick mark ring": true, - "Visuals/Counter clockwise": false - }, - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/PIDController[10]", - "_title": "PIDController[10]" - }, - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/PIDController[20]", - "_title": "PIDController[20]" - }, - { - "_type": "Boolean Box", - "_source0": "network_table:///LiveWindow/Ungrouped/DigitalInput[1]/Value", - "_title": "DigitalInput[1]/Value", - "Colors/Color when true": "#7CFC00FF", - "Colors/Color when false": "#8B0000FF" - }, - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/PIDController[5]", - "_title": "PIDController[5]" - }, - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/PIDController[15]", - "_title": "PIDController[15]" - }, - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/PIDController[24]", - "_title": "PIDController[24]" - }, - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/PIDController[4]", - "_title": "PIDController[4]" - }, - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/PIDController[9]", - "_title": "PIDController[9]" - }, - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/PIDController[12]", - "_title": "PIDController[12]" - }, - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/PIDController[21]", - "_title": "PIDController[21]" - }, - { - "_type": "Text View", - "_source0": "network_table:///LiveWindow/Ungrouped/DoubleSolenoid[1,0]/Value", - "_title": "DoubleSolenoid[1,0]/Value" - }, - { - "_type": "Motor Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/Talon FX [11]", - "_title": "LiveWindow/Ungrouped/Talon FX [11]", - "Visuals/Orientation": "HORIZONTAL" - }, - { - "_type": "Motor Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/Talon FX [12]", - "_title": "LiveWindow/Ungrouped/Talon FX [12]", - "Visuals/Orientation": "HORIZONTAL" - }, - { - "_type": "Motor Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/Talon FX [9]", - "_title": "LiveWindow/Ungrouped/Talon FX [9]", - "Visuals/Orientation": "HORIZONTAL" - }, - { - "_type": "Motor Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/MotorControllerGroup[2]", - "_title": "LiveWindow/Ungrouped/MotorControllerGroup[2]", - "Visuals/Orientation": "HORIZONTAL" - }, - { - "_type": "Motor Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/MotorControllerGroup[1]", - "_title": "LiveWindow/Ungrouped/MotorControllerGroup[1]", - "Visuals/Orientation": "HORIZONTAL" - }, - { - "_type": "Motor Controller", - "_source0": "network_table:///LiveWindow/Ungrouped/Talon FX [10]", - "_title": "LiveWindow/Ungrouped/Talon FX [10]", - "Visuals/Orientation": "HORIZONTAL" - } - ] - } - }, - "2,0": { - "size": [ - 2, - 4 - ], - "content": { - "_type": "Subsystem Layout", - "_source0": "network_table:///LiveWindow/OutsideClimber", - "_title": "OutsideClimber", - "Layout/Label position": "BOTTOM", - "_children": [] - } - }, - "4,0": { - "size": [ - 2, - 4 - ], - "content": { - "_type": "Subsystem Layout", - "_source0": "network_table:///LiveWindow/InsideClimber", - "_title": "InsideClimber", - "Layout/Label position": "BOTTOM", - "_children": [] - } - }, - "6,0": { - "size": [ - 2, - 4 - ], - "content": { - "_type": "Subsystem Layout", - "_source0": "network_table:///LiveWindow/OuterMagazine", - "_title": "OuterMagazine", - "Layout/Label position": "BOTTOM", - "_children": [ - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/OuterMagazine/PID Controller", - "_title": "PID Controller" - } - ] - } - }, - "8,0": { - "size": [ - 2, - 4 - ], - "content": { - "_type": "Subsystem Layout", - "_source0": "network_table:///LiveWindow/LEDs", - "_title": "LEDs", - "Layout/Label position": "BOTTOM", - "_children": [] - } - }, - "10,0": { - "size": [ - 2, - 4 - ], - "content": { - "_type": "Subsystem Layout", - "_source0": "network_table:///LiveWindow/Intake", - "_title": "Intake", - "Layout/Label position": "BOTTOM", - "_children": [] - } - }, - "0,4": { - "size": [ - 2, - 4 - ], - "content": { - "_type": "Subsystem Layout", - "_source0": "network_table:///LiveWindow/InnerMagazine", - "_title": "InnerMagazine", - "Layout/Label position": "BOTTOM", - "_children": [ - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/InnerMagazine/PID Controller", - "_title": "PID Controller" - } - ] - } - }, - "2,4": { - "size": [ - 2, - 4 - ], - "content": { - "_type": "Subsystem Layout", - "_source0": "network_table:///LiveWindow/Turret", - "_title": "Turret", - "Layout/Label position": "BOTTOM", - "_children": [] - } - }, - "4,4": { - "size": [ - 2, - 4 - ], - "content": { - "_type": "Subsystem Layout", - "_source0": "network_table:///LiveWindow/Swerve", - "_title": "Swerve", - "Layout/Label position": "BOTTOM", - "_children": [] - } - }, - "6,4": { - "size": [ - 2, - 4 - ], - "content": { - "_type": "Subsystem Layout", - "_source0": "network_table:///LiveWindow/Shooter", - "_title": "Shooter", - "Layout/Label position": "BOTTOM", - "_children": [ - { - "_type": "PID Controller", - "_source0": "network_table:///LiveWindow/Shooter/PID Controller", - "_title": "PID Controller" - } - ] - } - } - } - } - }, - { - "title": "Tab 3", - "autoPopulate": false, - "autoPopulatePrefix": "", - "widgetPane": { - "gridSize": 128.0, - "showGrid": true, - "hgap": 16.0, - "vgap": 16.0, - "titleType": 0, - "tiles": { - "0,1": { - "size": [ - 7, - 4 - ], - "content": { - "_type": "Camera Stream", - "_source0": "camera_server://Magazine Camera", - "_title": "Magazine Camera", - "Crosshair/Show crosshair": true, - "Crosshair/Crosshair color": "#FFFFFFFF", - "Controls/Show controls": false, - "Controls/Rotation": "NONE", - "compression": -1.0, - "fps": -1, - "imageWidth": -1, - "imageHeight": -1 - } - }, - "7,1": { - "size": [ - 6, - 4 - ], - "content": { - "_type": "Camera Stream", - "_source0": "camera_server://limelight", - "_title": "limelight", - "Crosshair/Show crosshair": true, - "Crosshair/Crosshair color": "#FFFFFFFF", - "Controls/Show controls": false, - "Controls/Rotation": "NONE", - "compression": -1.0, - "fps": -1, - "imageWidth": -1, - "imageHeight": -1 - } - }, - "0,0": { - "size": [ - 2, - 1 - ], - "content": { - "_type": "ComboBox Chooser", - "_source0": "network_table:///SmartDashboard/Choose Auto: ", - "_title": "SmartDashboard/Choose Auto: " - } - }, - "2,0": { - "size": [ - 2, - 1 - ], - "content": { - "_type": "Boolean Box", - "_source0": "network_table:///SmartDashboard/Magazine Switch", - "_title": "SmartDashboard/Magazine Switch", - "Colors/Color when true": "#7CFC00FF", - "Colors/Color when false": "#8B0000FF" - } - }, - "4,0": { - "size": [ - 2, - 1 - ], - "content": { - "_type": "Text View", - "_source0": "network_table:///LiveWindow/Ungrouped/navX-Sensor[4]/Value", - "_title": "LiveWindow/Ungrouped/navX-Sensor[4]/Value" - } - } - } - } - } - ], - "windowGeometry": { - "x": -8.0, - "y": -8.0, - "width": 1936.0, - "height": 1056.0 - } -} diff --git a/src/main/java/frc/lib/math/Conversions.java b/src/main/java/frc/lib/math/Conversions.java deleted file mode 100644 index 7871d53..0000000 --- a/src/main/java/frc/lib/math/Conversions.java +++ /dev/null @@ -1,94 +0,0 @@ -package frc.lib.math; - -/** - * Mathematical conversions for swerve calculations - */ -public class Conversions { - - /** - * @param counts Falcon Counts - * @param gearRatio Gear Ratio between Falcon and Mechanism - * @return Degrees of Rotation of Mechanism falconToDegrees - */ - public static double falconToDegrees(double counts, double gearRatio) { - return counts * (360.0 / (gearRatio * 2048.0)); - } - - /** - * @param degrees Degrees of rotation of Mechanism - * @param gearRatio Gear Ratio between Falcon and Mechanism - * @return Falcon Counts degreesToFalcon - */ - public static double degreesToFalcon(double degrees, double gearRatio) { - double ticks = degrees / (360.0 / (gearRatio * 2048.0)); - return ticks; - } - - /** - * @param velocityCounts Falcon Velocity Counts - * @param gearRatio Gear Ratio between Falcon and Mechanism (set to 1 for Falcon RPM) - * @return RPM of Mechanism - */ - public static double falconToRPM(double velocityCounts, double gearRatio) { - double motorRPM = velocityCounts * (600.0 / 2048.0); - double mechRPM = motorRPM / gearRatio; - return mechRPM; - } - - /** - * @param rpm RPM of mechanism - * @param gearRatio Gear Ratio between Falcon and Mechanism (set to 1 for Falcon RPM) - * @return RPM of Mechanism - */ - public static double rpmToFalcon(double rpm, double gearRatio) { - double motorRPM = rpm * gearRatio; - double sensorCounts = motorRPM * (2048.0 / 600.0); - return sensorCounts; - } - - /** - * @param counts Falcon Counts - * @param gearRatio Gear Ratio between Falcon and Mechanism - * @param circumference Circumference of Wheel - * @return Degrees of Rotation of Mechanism falconToDegrees - */ - public static double falconToMeters(double counts, double gearRatio, double circumference) { - return counts * circumference / (gearRatio * 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); - double wheelMPS = (wheelRPM * circumference) / 60; - return wheelMPS; - } - - /** - * @param velocity Velocity MPS - * @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 mpsToFalcon(double velocity, double circumference, double gearRatio) { - double wheelRPM = ((velocity * 60) / circumference); - double wheelVelocity = rpmToFalcon(wheelRPM, gearRatio); - return wheelVelocity; - } - - /** - * Normalize angle to between 0 to 360 - * - * @param goal initial angle - * @return normalized angle - */ - public static double reduceTo0_360(double goal) { - return goal % 360; - } - -} diff --git a/src/main/java/frc/robot/Constants.java b/src/main/java/frc/robot/Constants.java index e4c1815..5c5e13f 100644 --- a/src/main/java/frc/robot/Constants.java +++ b/src/main/java/frc/robot/Constants.java @@ -1,33 +1,218 @@ package frc.robot; +import static edu.wpi.first.units.Units.Inches; +import com.ctre.phoenix6.signals.InvertedValue; +import com.ctre.phoenix6.signals.NeutralModeValue; +import com.ctre.phoenix6.signals.SensorDirectionValue; +import com.studica.frc.AHRS.NavXComType; +import edu.wpi.first.apriltag.AprilTagFieldLayout; +import edu.wpi.first.apriltag.AprilTagFields; +import edu.wpi.first.math.geometry.Rotation2d; +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.kinematics.SwerveDriveKinematics; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.units.measure.Distance; +import frc.robot.subsystems.swerve.mod.ModuleConstants; +import frc.robot.subsystems.swerve.mod.ModuleConstantsBuilder; +import frc.robot.subsystems.vision.CameraConstants; +import frc.robot.subsystems.vision.CameraConstantsBuilder; + /** * Constants file. */ public final class Constants { - /** - * Stick Deadband - */ - public static final double stickDeadband = 0.1; - /** - * Driver ID - */ - public static final int driverID = 0; - /** - * Operator ID - */ - public static final int operatorID = 1; + + /** Constants for driver controls */ + public static class DriverControls { + /** Driverstation controller Index */ + public static final int controllerId = 0; + /** Stick axis controls less than this amount are treated as 0. */ + public static final double stickDeadband = 0.1; + + /** Maximum Translational speed (in m/s) */ + public static final double driverTranslationalMaxSpeed = 3.0; + /** Maximum Rotational speed (in rad/s) */ + public static final double driverRotationalMaxSpeed = 4.0; + } /** - * Motor CAN id's. + * MoveToPos constants. */ - public static final class Motors { + public static class SwerveTransformPID { + public static final double translationP = 3.5; + public static final double translationI = 0.0; + public static final double translationD = 0.0; + public static final double rotationP = 3.0; + public static final double rotationI = 0.0; + public static final double rotationD = 0.0; + + public static final double maxAngularVelocity = 9.0; + public static final double maxAngularAcceleration = 9 * 5; } /** - * Pneumatics CAN id constants. + * Swerve Constants */ - public static final class Pneumatics { + public static final class Swerve { + /** If true, motors and absolute encoders are on canivore loop. Otherwise on rio. */ + public static final boolean isCanviore = false; + + public static final NavXComType navXID = NavXComType.kMXP_SPI; + public static final boolean invertGyro = true; + public static final boolean isFieldRelative = true; + public static final boolean isOpenLoop = false; + + /* Drivetrain Constants */ + public static final double trackWidth = Units.inchesToMeters(23.75); + public static final double wheelBase = Units.inchesToMeters(17.75); + public static final Distance wheelDiameter = Inches.of(3.8); + public static final Distance wheelCircumference = wheelDiameter.times(Math.PI); + public static final Distance wheelRadius = wheelDiameter.div(2); + + public static final Distance bumperFront = Inches.of(15); + public static final Distance bumperRight = Inches.of(15); + + public static final Translation2d[] swerveTranslations = + new Translation2d[] {new Translation2d(wheelBase / 2.0, trackWidth / 2.0), + new Translation2d(wheelBase / 2.0, -trackWidth / 2.0), + new Translation2d(-wheelBase / 2.0, trackWidth / 2.0), + new Translation2d(-wheelBase / 2.0, -trackWidth / 2.0)}; + + /* + * Swerve Kinematics No need to ever change this unless you are not doing a traditional + * rectangular/square 4 module swerve + */ + public static final SwerveDriveKinematics swerveKinematics = + new SwerveDriveKinematics(swerveTranslations); + + /* Module Gear Ratios */ + public static final double driveGearRatio = (8.14 / 1.0); // MK4i L1 + public static final double angleGearRatio = ((150.0 / 7.0) / 1.0); // (150 / 7) : 1 + + /* Motor Inverts */ + public static final InvertedValue angleMotorInvert = InvertedValue.Clockwise_Positive; + public static final InvertedValue driveMotorInvert = + InvertedValue.CounterClockwise_Positive; + + /* Angle Encoder Invert */ + public static final SensorDirectionValue cancoderInvert = + SensorDirectionValue.CounterClockwise_Positive; + + /* Swerve Current Limiting */ + public static final int angleCurrentLimit = 25; + public static final int angleCurrentLowerLimit = 40; + public static final double angleCurrentLowerTimeThreshold = 0.1; + public static final boolean angleEnableCurrentLimit = true; + + public static final int driveCurrentLimit = 35; + public static final int driveCurrentLowerLimit = 60; + public static final double driveCurrentLowerTimeThreshold = 0.1; + public static final boolean driveEnableCurrentLimit = true; + + /* + * These values are used by the drive falcon to ramp in open loop and closed loop driving. + * We found a small open loop ramp (0.25) helps with wear, tipping, etc + */ + public static final double openLoopRamp = 0.25; + public static final double closedLoopRamp = 0.0; + + /* Angle Motor PID Values */ + public static final double angleKP = 100.0; + public static final double angleKI = 0.0; + public static final double angleKD = 0.0; + + /* Drive Motor PID Values */ + public static final double driveKP = 0.12; + public static final double driveKI = 0.0; + public static final double driveKD = 0.0; + public static final double driveKF = 0.0; + + /* Drive Motor Characterization Values From SYSID */ + public static final double driveKS = 0.32; + public static final double driveKV = 1.51; + public static final double driveKA = 0.27; + + /* Swerve Profiling Values */ + /** Meters per Second */ + public static final double maxSpeed = 3.0; + public static final double autoMaxSpeed = 3.0; + /** Radians per Second */ + public static final double maxAngularVelocity = 4.0; + + /* Neutral Modes */ + public static final NeutralModeValue angleNeutralMode = NeutralModeValue.Coast; + public static final NeutralModeValue driveNeutralMode = NeutralModeValue.Brake; + + public static final double odometryFrequency = 100.0; + + /* Teleop limits */ + public static final double forwardLimit = 10.0; + public static final double forwardTiltLimit = 1000.0; + public static final double leftTiltLimit = 1000.0; + public static final double rightTiltLimit = 1000.0; + public static final double backTiltLimit = 1000.0; + public static final double skidLimit = 1000.0; + + /* Module Specific Constants */ + + // @anchor:moduleConstants + // @formatter:off + public static final ModuleConstants[] modulesConstants = new ModuleConstants[] { + // Front Left Module + new ModuleConstantsBuilder() + .driveMotorId(6) + .angleMotorId(51) + .canCoderId(4) + .angleOffset(Rotation2d.fromRotations(-0.496826)) + .finish(), + // Front Right Module + new ModuleConstantsBuilder() + .driveMotorId(2) + .angleMotorId(40) + .canCoderId(2) + .angleOffset(Rotation2d.fromRotations(0.405518 + 0.5)) + .finish(), + // Back Left Module + new ModuleConstantsBuilder() + .driveMotorId(3) + .angleMotorId(9) + .canCoderId(1) + .angleOffset(Rotation2d.fromRotations(0.348145)) + .finish(), + // Back Right Module + new ModuleConstantsBuilder() + .driveMotorId(10) + .angleMotorId(8) + .canCoderId(10) + .angleOffset(Rotation2d.fromRotations(0.317627 + 0.5)) + .finish(), + }; + // @formatter:on } + /** Vision Constants */ + public static final class Vision { + public static final AprilTagFieldLayout fieldLayout = + AprilTagFieldLayout.loadField(AprilTagFields.k2025ReefscapeAndyMark); + // @formatter:off + public static final CameraConstants[] cameraConstants = new CameraConstants[] { + new CameraConstantsBuilder() + .name("cam0") + .height(800) + .width(1280) + .horizontalFieldOfView(80) + .simFps(20) + .simLatency(0.3) + .simLatencyStdDev(0.02) + .calibrationErrorMean(0.8) + .calibrationErrorStdDev(0.08) + .robotToCamera(new Transform3d(0, 0, 0, new Rotation3d(Units.degreesToRadians(180), 0, 0))) + .translationError(0.02) + .finish(), + }; + // @formatter:on + } } diff --git a/src/main/java/frc/robot/Robot.java b/src/main/java/frc/robot/Robot.java index b22af27..0e2484d 100644 --- a/src/main/java/frc/robot/Robot.java +++ b/src/main/java/frc/robot/Robot.java @@ -14,15 +14,15 @@ import org.littletonrobotics.junction.networktables.NT4Publisher; import org.littletonrobotics.junction.wpilog.WPILOGReader; import org.littletonrobotics.junction.wpilog.WPILOGWriter; -import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj.Timer; import edu.wpi.first.wpilibj2.command.CommandScheduler; +import frc.robot.util.PhoenixSignals; /** * Runs tasks on Roborio in this file. */ public class Robot extends LoggedRobot { private RobotContainer robotContainer; - private Command autoChooser; /** * Robnot Run type @@ -37,6 +37,7 @@ public static enum RobotRunType { } public RobotRunType robotRunType = RobotRunType.kReal; + private Timer gcTimer = new Timer(); // private Ultrasonic ultrasonic = new Ultrasonic(); /** @@ -63,8 +64,6 @@ public void robotInit() { break; } - - if (isReal()) { Logger.addDataReceiver(new WPILOGWriter("/media/sda1")); // Log to a USB stick Logger.addDataReceiver(new NT4Publisher()); // Publish data to NetworkTables @@ -95,27 +94,21 @@ public void robotInit() { // Instantiate our RobotContainer. This will perform all our button bindings, // and put our autonomous chooser on the dashboard. robotContainer = new RobotContainer(robotRunType); - } - /** - * This function 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. - * - *

- * This runs after the mode specific periodic functions, but before LiveWindow and - * SmartDashboard integrated updating. - */ + gcTimer.start(); + } @Override public void robotPeriodic() { - // Runs the Scheduler. This is responsible for polling buttons, adding newly-scheduled - // commands, - // running already-scheduled commands, removing finished or interrupted commands, and - // running - // subsystem periodic() methods. This must be called from the robot's periodic block in - // order for - // anything in the Command-based framework to work. + PhoenixSignals.refreshAll(); + CommandScheduler.getInstance().run(); + + robotContainer.periodic(); + + if (gcTimer.advanceIfElapsed(5)) { + System.gc(); + } } @Override @@ -124,48 +117,24 @@ public void disabledInit() {} @Override public void disabledPeriodic() {} - /** - * This autonomous runs the autonomous command selected by your {@link RobotContainer} class. - */ @Override - public void autonomousInit() { - robotContainer.getAutonomousCommand().schedule(); - autoChooser = robotContainer.getAutonomousCommand(); - - // schedule the autonomous command (example) - if (autoChooser != null) { - autoChooser.schedule(); - } - } + public void autonomousInit() {} - /** This function is called periodically during autonomous. */ @Override public void autonomousPeriodic() {} @Override - public void teleopInit() { - if (autoChooser != null) { - autoChooser.cancel(); - } - } + public void teleopInit() {} - /** This function is called periodically during operator control. */ @Override - public void teleopPeriodic() { - // vision.update(); - } + public void teleopPeriodic() {} @Override - public void testInit() { - // Cancels all running commands at the start of test mode. - CommandScheduler.getInstance().cancelAll(); - } + public void testInit() {} - /** This function is called periodically during test mode. */ @Override public void testPeriodic() {} - private static final String environmentVariable = "AKIT_LOG_PATH"; private static final String advantageScopeFileName = "akit-log-path.txt"; diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java index 2756f2f..5bce154 100644 --- a/src/main/java/frc/robot/RobotContainer.java +++ b/src/main/java/frc/robot/RobotContainer.java @@ -1,17 +1,28 @@ package frc.robot; -import edu.wpi.first.wpilibj.GenericHID; -import edu.wpi.first.wpilibj.XboxController; -import edu.wpi.first.wpilibj.smartdashboard.SendableChooser; -import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.InstantCommand; -import edu.wpi.first.wpilibj2.command.WaitCommand; +import org.ironmaple.simulation.SimulatedArena; +import org.jspecify.annotations.NullMarked; +import org.littletonrobotics.junction.Logger; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.wpilibj2.command.button.CommandXboxController; import frc.robot.Robot.RobotRunType; -import frc.robot.subsystems.drive.Drivetrain; -import frc.robot.subsystems.drive.DrivetrainIO; -import frc.robot.subsystems.drive.DrivetrainReal; +import frc.robot.subsystems.swerve.Swerve; +import frc.robot.subsystems.swerve.SwerveIOEmpty; +import frc.robot.subsystems.swerve.SwerveReal; +import frc.robot.subsystems.swerve.SwerveSim; +import frc.robot.subsystems.swerve.gyro.GyroIOEmpty; +import frc.robot.subsystems.swerve.gyro.GyroNavX2; +import frc.robot.subsystems.swerve.mod.SwerveModuleIOEmpty; +import frc.robot.subsystems.swerve.mod.SwerveModuleReal; +import frc.robot.subsystems.swerve.util.TeleopControls; +import frc.robot.subsystems.vision.Vision; +import frc.robot.subsystems.vision.VisionIOEmpty; +import frc.robot.subsystems.vision.VisionReal; +import frc.robot.subsystems.vision.VisionSim; +import frc.robot.util.DeviceDebug; +import frc.robot.viz.RobotViz; + /** * This class is where the bulk of the robot should be declared. Since Command-based is a @@ -19,60 +30,58 @@ * periodic methods (other than the scheduler calls). Instead, the structure of the robot (including * subsystems, commands, and button mappings) should be declared here. */ -public class RobotContainer { - /* Controllers */ - private final CommandXboxController driver = new CommandXboxController(Constants.driverID); - private final CommandXboxController operator = new CommandXboxController(Constants.operatorID); +@NullMarked +public final class RobotContainer { - // Initialize AutoChooser Sendable - private final SendableChooser autoChooser = new SendableChooser<>(); + /* Controllers */ + public final CommandXboxController driver = + new CommandXboxController(Constants.DriverControls.controllerId); /* Subsystems */ - private Drivetrain drivetrain; + private final Swerve swerve; + private final Vision vision; + + private final SwerveSim sim; + private final RobotViz viz; /** - * The container for the robot. Contains subsystems, OI devices, and commands. */ public RobotContainer(RobotRunType runtimeType) { - SmartDashboard.putData("Choose Auto: ", autoChooser); - autoChooser.setDefaultOption("Wait 1 Second", "wait"); switch (runtimeType) { case kReal: - drivetrain = new Drivetrain(new DrivetrainReal()); + sim = null; + swerve = new Swerve(SwerveReal::new, GyroNavX2::new, SwerveModuleReal::new); + vision = new Vision(swerve.state, new VisionReal()); break; case kSimulation: - // drivetrain = new Drivetrain(new DrivetrainSim() {}); + sim = new SwerveSim(new Pose2d(2.0, 2.0, Rotation2d.kZero)); + swerve = new Swerve(sim::simProvider, sim::gyroProvider, sim::moduleProvider); + vision = new Vision(swerve.state, new VisionSim(sim)); break; default: - drivetrain = new Drivetrain(new DrivetrainIO() {}); + sim = null; + swerve = new Swerve(SwerveIOEmpty::new, GyroIOEmpty::new, SwerveModuleIOEmpty::new); + vision = new Vision(swerve.state, new VisionIOEmpty()); } - // Configure the button bindings - configureButtonBindings(); - } + viz = new RobotViz(sim, swerve); - /** - * Use this method to define your button->command mappings. Buttons can be created by - * instantiating a {@link GenericHID} or one of its subclasses - * ({@link edu.wpi.first.wpilibj.Joystick} or {@link XboxController}), and then passing it to a - * {@link edu.wpi.first.wpilibj2.command.button.JoystickButton}. - */ - private void configureButtonBindings() {} + DeviceDebug.initialize(); - /** - * Gets the user's selected autonomous command. - * - * @return Returns autonomous command selected. - */ - public Command getAutonomousCommand() { - Command autocommand; - String stuff = autoChooser.getSelected(); - switch (stuff) { - case "wait": - autocommand = new WaitCommand(1.0); - break; - default: - autocommand = new InstantCommand(); + swerve.setDefaultCommand(swerve.driveUserRelative(TeleopControls.teleopControls( + () -> -driver.getLeftY(), () -> -driver.getLeftX(), () -> -driver.getRightX()))); + + driver.y().onTrue(swerve.setFieldRelativeOffset()); + } + + /** Runs once per 0.02 seconds after subsystems and commands. */ + public void periodic() { + if (sim != null) { + SimulatedArena.getInstance().simulationPeriodic(); + Logger.recordOutput("FieldSimulation/Algae", + SimulatedArena.getInstance().getGamePiecesArrayByType("Algae")); + Logger.recordOutput("FieldSimulation/Coral", + SimulatedArena.getInstance().getGamePiecesArrayByType("Coral")); } - return autocommand; + viz.periodic(); } } diff --git a/src/main/java/frc/robot/autos/.keep b/src/main/java/frc/robot/autos/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/frc/robot/commands/.keep b/src/main/java/frc/robot/commands/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/frc/robot/modules/.keep b/src/main/java/frc/robot/modules/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/frc/robot/subsystems/drive/Drivetrain.java b/src/main/java/frc/robot/subsystems/drive/Drivetrain.java deleted file mode 100644 index 88e6569..0000000 --- a/src/main/java/frc/robot/subsystems/drive/Drivetrain.java +++ /dev/null @@ -1,27 +0,0 @@ -package frc.robot.subsystems.drive; - -import org.littletonrobotics.junction.Logger; -import edu.wpi.first.wpilibj2.command.SubsystemBase; - -/** - * Drivetrain subsystem. - */ - -public class Drivetrain extends SubsystemBase { - private DrivetrainIO io; - private DrivetrainIOInputsAutoLogged inputs = new DrivetrainIOInputsAutoLogged(); - - /** - * Create Wrist Intake Subsystem - */ - public Drivetrain(DrivetrainIO io) { - this.io = io; - } - - @Override - public void periodic() { - io.updateInputs(inputs); - Logger.processInputs("Drivetrain", inputs); - } -} - diff --git a/src/main/java/frc/robot/subsystems/drive/DrivetrainIO.java b/src/main/java/frc/robot/subsystems/drive/DrivetrainIO.java deleted file mode 100644 index 842e1aa..0000000 --- a/src/main/java/frc/robot/subsystems/drive/DrivetrainIO.java +++ /dev/null @@ -1,25 +0,0 @@ -package frc.robot.subsystems.drive; - -import org.littletonrobotics.junction.AutoLog; -import edu.wpi.first.math.geometry.Rotation2d; - -/** - * DrivetrainIO interface - */ -public interface DrivetrainIO { - /** - * Drivetrain IO - */ - @AutoLog - public static class DrivetrainIOInputs { - public Rotation2d gyroYaw = new Rotation2d(); - } - - /** Updates the set of loggable inputs. */ - public default void updateInputs(DrivetrainIOInputs inputs) {} - - /** Run the motor at the specified voltage. */ - public default void setDriveVoltage(double lvolts, double rvolts) {} - - -} diff --git a/src/main/java/frc/robot/subsystems/drive/DrivetrainReal.java b/src/main/java/frc/robot/subsystems/drive/DrivetrainReal.java deleted file mode 100644 index f8f17d1..0000000 --- a/src/main/java/frc/robot/subsystems/drive/DrivetrainReal.java +++ /dev/null @@ -1,26 +0,0 @@ -package frc.robot.subsystems.drive; - -import edu.wpi.first.math.geometry.Rotation2d; - -/** - * DrivetrainReal - */ -public class DrivetrainReal implements DrivetrainIO { - - - /** - * Drivetrain Real - */ - public DrivetrainReal() {} - - @Override - public void updateInputs(DrivetrainIOInputs inputs) { - inputs.gyroYaw = Rotation2d.fromDegrees(0); - } - - /** - * Drive Voltage - */ - public void setDriveVoltage(double lvolts, double rvolts) {} - -} diff --git a/src/main/java/frc/robot/subsystems/swerve/Swerve.java b/src/main/java/frc/robot/subsystems/swerve/Swerve.java new file mode 100644 index 0000000..3b1ff21 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/swerve/Swerve.java @@ -0,0 +1,418 @@ +package frc.robot.subsystems.swerve; + +import java.util.Arrays; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.IntStream; +import org.jspecify.annotations.NullMarked; +import org.littletonrobotics.junction.Logger; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.kinematics.ChassisSpeeds; +import edu.wpi.first.math.kinematics.SwerveDriveKinematics; +import edu.wpi.first.math.kinematics.SwerveModulePosition; +import edu.wpi.first.math.kinematics.SwerveModuleState; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; +import edu.wpi.first.wpilibj2.command.SubsystemBase; +import frc.robot.Constants; +import frc.robot.subsystems.swerve.gyro.GyroIO; +import frc.robot.subsystems.swerve.gyro.GyroInputsAutoLogged; +import frc.robot.subsystems.swerve.mod.SwerveModule; +import frc.robot.subsystems.swerve.mod.SwerveModuleIO; +import frc.robot.subsystems.swerve.util.MoveToPoseBuilder; +import frc.robot.subsystems.swerve.util.PhoenixOdometryThread; +import frc.robot.subsystems.swerve.util.SwerveRateLimiter; +import frc.robot.subsystems.swerve.util.SwerveState; +import frc.robot.subsystems.swerve.util.TuningCommands; +import frc.robot.util.AllianceFlipUtil; + +/** + * Primary swerve drivetrain subsystem. + * + *

+ * This subsystem owns and coordinates all components required to control and estimate the state of + * a swerve drive, including: + *

+ * + *

Threading model

Odometry-related sensor signals are updated on a dedicated background + * thread. Access to these signals and derived state is synchronized using a shared + * {@code odometryLock} to ensure consistency across the estimator and modules. + * + *

Pose estimation

Wheel encoder and gyro data are integrated at high rate to produce + * odometry updates, which are then fused with delayed vision measurements inside + * {@link SwerveState}. The resulting pose estimate is the authoritative source of robot position + * for autonomous and field-relative driving. + * + *

Driving model

All drive commands ultimately resolve to robot-relative + * {@link ChassisSpeeds}. These speeds are passed through a {@link SwerveRateLimiter} before being + * discretized and converted to per-module states. + * + *

+ * This class exposes convenience commands for robot-relative, field-relative, and user-relative + * driving, as well as trajectory-style pose targeting and characterization routines. + */ +@NullMarked +public final class Swerve extends SubsystemBase { + + private final Lock odometryLock = new ReentrantLock(); + private final PhoenixOdometryThread odometryThread; + private final SwerveModule[] modules; + private final GyroIO gyro; + private final GyroInputsAutoLogged gyroInputs = new GyroInputsAutoLogged(); + private final SwerveIO io; + private final SwerveInputsAutoLogged inputs = new SwerveInputsAutoLogged(); + + private final SwerveRateLimiter limiter = new SwerveRateLimiter(); + + public final SwerveState state; + + /** + * Constructs the swerve subsystem and initializes all hardware interfaces, estimator state, and + * background odometry processing. + * + *

+ * The provided factories are invoked with a shared {@link PhoenixOdometryThread} instance to + * allow sensors and motors to register high-frequency signals for synchronized sampling. + * + * @param swerveIo factory for creating the drivetrain-level IO implementation + * @param gyroIo factory for creating the gyro IO implementation + * @param moduleIoFn factory for creating per-module IO implementations + */ + public Swerve(Function swerveIo, + Function gyroIo, + BiFunction moduleIoFn) { + super("Swerve"); + this.odometryThread = new PhoenixOdometryThread(this.odometryLock); + this.gyro = gyroIo.apply(this.odometryThread); + this.modules = IntStream.range(0, Constants.Swerve.modulesConstants.length) + .mapToObj(i -> new SwerveModule(i, moduleIoFn.apply(i, this.odometryThread))) + .toArray(SwerveModule[]::new); + this.io = swerveIo.apply(odometryThread); + this.odometryThread.start(); + this.odometryLock.lock(); + SwerveModulePosition[] initPositions = new SwerveModulePosition[modules.length]; + try { + Arrays.stream(modules).map(mod -> { + mod.updateInputs(); + return mod.getPosition(); + }).toArray(_i -> initPositions); + } finally { + this.odometryLock.unlock(); + } + this.state = new SwerveState(initPositions, this.gyroInputs.yaw); + } + + @Override + public void periodic() { + this.odometryLock.lock(); + + for (int i = 0; i < modules.length; i++) { + this.modules[i].updateInputs(); + } + + this.gyro.updateInputs(this.gyroInputs); + Logger.processInputs("Swerve/Gyro", this.gyroInputs); + + this.io.updateInputs(this.inputs); + Logger.processInputs("Swerve/Timestamps", this.inputs); + + this.odometryLock.unlock(); + + for (int i = 0; i < modules.length; i++) { + this.modules[i].periodic(); + } + + SwerveModuleState[] wheelStates = new SwerveModuleState[modules.length]; + for (int j = 0; j < modules.length; j++) { + wheelStates[j] = modules[j].getState(); + } + ChassisSpeeds currentSpeeds = + Constants.Swerve.swerveKinematics.toChassisSpeeds(wheelStates); + double[] sampleTimestamps = this.inputs.timestamps; + double gyroPerSample = currentSpeeds.omegaRadiansPerSecond / sampleTimestamps.length; + SwerveModulePosition[] wheelPositions = new SwerveModulePosition[modules.length]; + for (int i = 0; i < sampleTimestamps.length; i++) { + for (int j = 0; j < modules.length; j++) { + wheelPositions[j] = modules[j].getOdometryPosition(i); + } + state.addOdometryObservation(wheelPositions, + gyroInputs.connected ? Rotation2d.fromRadians(gyroInputs.yawRads[i]) + : state.getGlobalPoseEstimate().getRotation() + .plus(Rotation2d.fromRadians((gyroPerSample + 1) * i)), + sampleTimestamps[i]); + } + limiter.update(currentSpeeds); + state.updateSpeeds(currentSpeeds); + + Logger.recordOutput("Swerve/GlobalPoseEstimate", state.getGlobalPoseEstimate()); + } + + /** + * Drives the robot using robot-relative chassis speeds. + * + *

+ * Supplied speeds are passed through the {@link SwerveRateLimiter} before being applied to the + * drivetrain. + * + * @param driveSpeeds supplier of robot-relative chassis speeds + * @return a command that drives the robot while scheduled + */ + public Command driveRobotRelative(Supplier driveSpeeds) { + return this.run(() -> { + ChassisSpeeds speeds = driveSpeeds.get(); + speeds = limiter.limit(speeds); + setModuleStates(speeds); + }); + } + + /** + * Drives the robot using a user-defined field reference heading. + * + *

+ * The supplied field-relative speeds are transformed into robot-relative speeds using the + * user-controlled heading offset. + * + * @param driveSpeeds supplier of field-relative chassis speeds + * @return a command that drives the robot while scheduled + */ + public Command driveUserRelative(Supplier driveSpeeds) { + return driveRobotRelative(() -> ChassisSpeeds.fromFieldRelativeSpeeds(driveSpeeds.get(), + getUserRelativeHeading())); + } + + /** + * Drives the robot using the estimated global field heading. + * + *

+ * The supplied field-relative speeds are transformed into robot-relative speeds using the + * current pose estimate from {@link SwerveState}. + * + * @param driveSpeeds supplier of field-relative chassis speeds + * @return a command that drives the robot while scheduled + */ + public Command driveFieldRelative(Supplier driveSpeeds) { + return driveRobotRelative(() -> ChassisSpeeds.fromFieldRelativeSpeeds(driveSpeeds.get(), + state.getGlobalPoseEstimate().getRotation())); + } + + /** + * Immediately sets the robot's pose to a new value, updating both the odometry estimator and + * the underlying simulation state (if any). + * + *

+ * This is useful when you need to forcibly override the robot's pose, for example during + * testing or at the start of autonomous in simulation. + * + *

+ * If you only want to update the odometry/estimator without affecting any simulation state, use + * {@link SwerveState#resetPose(Pose2d)} instead. + * + * @param newPose a supplier that provides the new robot pose in field coordinates + * @return a command that applies the pose override once when scheduled + */ + public Command overridePose(Supplier newPose) { + return Commands.runOnce(() -> { + Pose2d newPose_ = newPose.get(); + io.resetPose(newPose_); + state.resetPose(newPose_); + }); + } + + /** + * Creates a command builder for driving the robot to a target global pose. + * + *

+ * The generated command uses a holonomic controller and the current pose estimate to compute + * chassis speeds, which are rate-limited and applied to the drivetrain. + * + * @return a {@link MoveToPoseBuilder} for configuring pose targets + */ + public MoveToPoseBuilder moveToPose() { + return new MoveToPoseBuilder(this, (speeds) -> { + speeds = limiter.limit(speeds); + setModuleStates(speeds); + }); + } + + /** + * Creates a SysId routine for drivetrain feedforward characterization. + * + *

+ * This routine is used to identify kS and kV parameters for the swerve drive by applying + * controlled voltage steps and measuring resulting motion. + * + * @return a command that runs feedforward characterization + */ + public Command feedforwardCharacterization() { + return TuningCommands.feedforwardCharacterization(this, this::runCharacterization, + this::getFFCharacterizationVelocity); + } + + /** + * Creates a SysId routine for wheel radius characterization. + * + *

+ * This routine estimates the effective wheel radius by correlating commanded motion with + * measured yaw change. + * + * @return a command that runs wheel radius characterization + */ + public Command wheelRadiusCharacterization() { + return TuningCommands.wheelRadiusCharacterization(this, this::setModuleStates, + this::getWheelRadiusCharacterizationPositions, () -> this.gyroInputs.yaw); + } + + /** + * Sets a user-defined field-relative heading offset. + * + *

+ * This offset is used by user-relative driving modes to define "forward" independently of the + * robot's pose estimate. + * + * @param knownHeading supplier of the desired field heading + * @return a one-shot command that applies the offset + */ + public Command setFieldRelativeOffset(Supplier knownHeading) { + return Commands.runOnce( + () -> fieldOffset = gyroInputs.yaw.getRotations() - knownHeading.get().getRotations()); + } + + /** + * Sets a user-defined field-relative heading offset. + * + *

+ * This offset is used by user-relative driving modes to define "forward" independently of the + * robot's pose estimate. + * + * @return a one-shot command that sets the offset such that the current direction is "forward" + */ + public Command setFieldRelativeOffset() { + return setFieldRelativeOffset(() -> Rotation2d.kZero); + } + + /** + * Sets a user-defined field-relative heading offset. + * + *

+ * This offset is used by user-relative driving modes to define "forward" independently of the + * robot's pose estimate. + * + * @return a one-shot command that sets the offset such that it agrees with the estimated pose + * (+180 degrees when on red alliance) + */ + public Command resetFieldRelativeOffsetBasedOnPose() { + return setFieldRelativeOffset(() -> state.getGlobalPoseEstimate().getRotation() + .plus(AllianceFlipUtil.shouldFlip() ? Rotation2d.kZero : Rotation2d.k180deg)); + } + + /** + * Creates a command that smoothly brings the drivetrain to a complete stop. + * + *

+ * This command commands zero desired chassis speeds and allows the {@link SwerveRateLimiter} to + * decelerate the robot within configured acceleration, tilt, and skid constraints rather than + * stopping abruptly. + * + *

+ * The command completes once the rate-limited translational speed falls below a small + * threshold, indicating the robot is effectively stationary. After completion, a final + * zero-speed command is issued to ensure all modules are explicitly commanded to stop. + * + *

+ * This is intended for use when transitioning between driving modes, autonomous steps, or + * before actions that require the robot to be fully settled. + * + * @return a command that decelerates and stops the drivetrain + */ + public Command stop() { + return this.driveRobotRelative(ChassisSpeeds::new).until(() -> { + var speeds = limiter.limit(new ChassisSpeeds()); + return Math.hypot(speeds.vxMetersPerSecond, speeds.vyMetersPerSecond) < 0.1; + }).andThen(this.emergencyStop()); + } + + /** + * Creates a command that immediately commands zero chassis speeds to the drivetrain. + * + *

+ * This method bypasses all rate limiting and deceleration constraints and directly commands the + * swerve modules to stop in a single control cycle. As a result, it may cause abrupt + * deceleration, increased state estimation error, and/or loss of traction depending on robot + * speed and surface conditions. + * + *

+ * In most situations, {@link #stop()} should be preferred, as it brings the robot to + * rest in a controlled manner using the {@link SwerveRateLimiter}. + * + *

+ * This command is intended only for exceptional circumstances such as fault handling, disable + * transitions, or safety-critical interruptions where immediate cessation of motion is + * required. + * + * @return a command that immediately commands zero chassis speeds + */ + public Command emergencyStop() { + return this.runOnce(() -> setModuleStates(new ChassisSpeeds())); + } + + private void runCharacterization(double output) { + for (SwerveModule module : modules) { + module.runCharacterization(output); + } + } + + private double getFFCharacterizationVelocity() { + double output = 0.0; + for (SwerveModule module : modules) { + output += module.getFFCharacterizationVelocity() / modules.length; + } + return output; + } + + private double[] getWheelRadiusCharacterizationPositions() { + double[] values = new double[modules.length]; + for (int i = 0; i < modules.length; i++) { + values[i] = modules[i].getWheelRadiusCharacterizationPosition(); + } + return values; + } + + private double fieldOffset = 0.0; + + /** + * Returns the current user-relative heading used for driving. + * + *

+ * This heading is derived from the gyro yaw and a manually controlled field offset, independent + * of the pose estimator. + * + * @return user-relative field heading + */ + public Rotation2d getUserRelativeHeading() { + return Rotation2d.fromRotations(gyroInputs.yaw.getRotations() - fieldOffset); + } + + private void setModuleStates(ChassisSpeeds chassisSpeeds) { + ChassisSpeeds targetSpeeds = ChassisSpeeds.discretize(chassisSpeeds, 0.02); + SwerveModuleState[] swerveModuleStates = + Constants.Swerve.swerveKinematics.toSwerveModuleStates(targetSpeeds); + setModuleStates(swerveModuleStates); + } + + private void setModuleStates(SwerveModuleState[] desiredStates) { + SwerveDriveKinematics.desaturateWheelSpeeds(desiredStates, Constants.Swerve.maxSpeed); + for (int i = 0; i < modules.length; i++) { + modules[i].setDesiredState(desiredStates[i]); + } + } +} diff --git a/src/main/java/frc/robot/subsystems/swerve/SwerveIO.java b/src/main/java/frc/robot/subsystems/swerve/SwerveIO.java new file mode 100644 index 0000000..18853dd --- /dev/null +++ b/src/main/java/frc/robot/subsystems/swerve/SwerveIO.java @@ -0,0 +1,26 @@ +package frc.robot.subsystems.swerve; + +import org.jspecify.annotations.NullMarked; +import org.littletonrobotics.junction.AutoLog; +import edu.wpi.first.math.geometry.Pose2d; +import frc.robot.subsystems.swerve.util.PhoenixOdometryThread; +import frc.robot.util.GenerateEmptyIO; + +/** IO for swerve timestamps */ +@GenerateEmptyIO(PhoenixOdometryThread.class) +@NullMarked +public interface SwerveIO { + + /** Inputs for swerve timestamps */ + @AutoLog + public static class SwerveInputs { + public double[] timestamps = new double[0]; + } + + /** Update inputs */ + public void updateInputs(SwerveInputs inputs); + + /** Set the ground truth pose. Only useful in sim. */ + public void resetPose(Pose2d pose); + +} diff --git a/src/main/java/frc/robot/subsystems/swerve/SwerveReal.java b/src/main/java/frc/robot/subsystems/swerve/SwerveReal.java new file mode 100644 index 0000000..a97c481 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/swerve/SwerveReal.java @@ -0,0 +1,28 @@ +package frc.robot.subsystems.swerve; + +import java.util.Queue; +import org.jspecify.annotations.NullMarked; +import edu.wpi.first.math.geometry.Pose2d; +import frc.robot.subsystems.swerve.util.PhoenixOdometryThread; + +/** Real Swerve Implementation */ +@NullMarked +public final class SwerveReal implements SwerveIO { + + private final Queue timestampQueue; + + /** Real Swerve Implementation */ + public SwerveReal(PhoenixOdometryThread odometryThread) { + this.timestampQueue = odometryThread.makeTimestampQueue(); + } + + @Override + public void updateInputs(SwerveInputs inputs) { + inputs.timestamps = this.timestampQueue.stream().mapToDouble(x -> x).toArray(); + this.timestampQueue.clear(); + } + + @Override + public void resetPose(Pose2d pose) {} + +} diff --git a/src/main/java/frc/robot/subsystems/swerve/SwerveSim.java b/src/main/java/frc/robot/subsystems/swerve/SwerveSim.java new file mode 100644 index 0000000..036bcf0 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/swerve/SwerveSim.java @@ -0,0 +1,71 @@ +package frc.robot.subsystems.swerve; + +import static edu.wpi.first.units.Units.KilogramSquareMeters; +import static edu.wpi.first.units.Units.Pounds; +import static edu.wpi.first.units.Units.Volts; +import org.ironmaple.simulation.SimulatedArena; +import org.ironmaple.simulation.drivesims.COTS; +import org.ironmaple.simulation.drivesims.SwerveDriveSimulation; +import org.ironmaple.simulation.drivesims.configs.DriveTrainSimulationConfig; +import org.ironmaple.simulation.drivesims.configs.SwerveModuleSimulationConfig; +import org.jspecify.annotations.NullMarked; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.system.plant.DCMotor; +import edu.wpi.first.wpilibj.Timer; +import frc.robot.Constants; +import frc.robot.subsystems.swerve.gyro.GyroIO; +import frc.robot.subsystems.swerve.gyro.GyroSim; +import frc.robot.subsystems.swerve.mod.SwerveModuleIO; +import frc.robot.subsystems.swerve.mod.SwerveModuleSim; +import frc.robot.subsystems.swerve.util.PhoenixOdometryThread; + +/** Simulation implementation for swerve */ +@NullMarked +public final class SwerveSim implements SwerveIO { + + public final SwerveDriveSimulation mapleSim; + + /** Simulation implementation for swerve */ + public SwerveSim(Pose2d initialPose) { + this.mapleSim = new SwerveDriveSimulation( + DriveTrainSimulationConfig.Default().withGyro(COTS.ofNav2X()) + .withRobotMass(Pounds.of(150)) + .withCustomModuleTranslations(Constants.Swerve.swerveTranslations) + .withBumperSize(Constants.Swerve.bumperFront.times(2), + Constants.Swerve.bumperRight.times(2)) + .withSwerveModule(new SwerveModuleSimulationConfig(DCMotor.getKrakenX60(1), + DCMotor.getKrakenX60(1), Constants.Swerve.driveGearRatio, + Constants.Swerve.angleGearRatio, Volts.of(0.15), Volts.of(0.35), + Constants.Swerve.wheelRadius, KilogramSquareMeters.of(0.02), 1.2)), + initialPose); + SimulatedArena.getInstance().addDriveTrainSimulation(this.mapleSim); + } + + + /** Supplier passed into Swerve constructor */ + public SwerveSim simProvider(PhoenixOdometryThread odometryThread) { + return this; + } + + /** Supplier passed into Swerve constructor */ + public GyroIO gyroProvider(PhoenixOdometryThread odometryThread) { + return new GyroSim(this.mapleSim.getGyroSimulation()); + } + + /** Supplier passed into Swerve constructor */ + public SwerveModuleIO moduleProvider(int index, PhoenixOdometryThread odometryThread) { + return new SwerveModuleSim(index, this.mapleSim.getModules()[index]); + } + + @Override + public void updateInputs(SwerveInputs inputs) { + inputs.timestamps = new double[] {Timer.getTimestamp()}; + } + + + @Override + public void resetPose(Pose2d pose) { + mapleSim.setSimulationWorldPose(pose); + } + +} diff --git a/src/main/java/frc/robot/subsystems/swerve/gyro/GyroIO.java b/src/main/java/frc/robot/subsystems/swerve/gyro/GyroIO.java new file mode 100644 index 0000000..1e8c935 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/swerve/gyro/GyroIO.java @@ -0,0 +1,31 @@ +package frc.robot.subsystems.swerve.gyro; + +import org.jspecify.annotations.NullMarked; +import org.littletonrobotics.junction.AutoLog; +import edu.wpi.first.math.geometry.Rotation2d; +import frc.robot.subsystems.swerve.util.PhoenixOdometryThread; +import frc.robot.util.GenerateEmptyIO; + +/** IO for gyro */ +@GenerateEmptyIO({PhoenixOdometryThread.class}) +@NullMarked +public interface GyroIO { + + /** Inputs for gyro */ + @AutoLog + public static class GyroInputs { + public boolean connected; + public Rotation2d yaw = Rotation2d.kZero; + public double yawVelocityRadPerSec; + public Rotation2d pitch = Rotation2d.kZero; + public double pitchVelocityRadPerSec; + public Rotation2d roll = Rotation2d.kZero; + public double rollVelocityRadPerSec; + + public double[] yawRads = new double[0]; + } + + /** Update inputs */ + public void updateInputs(GyroInputs inputs); + +} diff --git a/src/main/java/frc/robot/subsystems/swerve/gyro/GyroNavX2.java b/src/main/java/frc/robot/subsystems/swerve/gyro/GyroNavX2.java new file mode 100644 index 0000000..486e812 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/swerve/gyro/GyroNavX2.java @@ -0,0 +1,41 @@ +package frc.robot.subsystems.swerve.gyro; + +import java.util.Queue; +import org.jspecify.annotations.NullMarked; +import com.studica.frc.AHRS; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.util.Units; +import frc.robot.Constants; +import frc.robot.subsystems.swerve.util.PhoenixOdometryThread; + +/** NavX2 implementation for Gyro */ +@NullMarked +public class GyroNavX2 implements GyroIO { + + private AHRS gyro = new AHRS(Constants.Swerve.navXID, (int) Constants.Swerve.odometryFrequency); + + private final Queue yawQueue; + + /** NavX2 implementation for Gyro */ + public GyroNavX2(PhoenixOdometryThread odometryThread) { + this.yawQueue = odometryThread.registerSignal(() -> Units.degreesToRadians(gyro.getYaw())); + } + + @Override + public void updateInputs(GyroInputs inputs) { + inputs.connected = gyro.isConnected(); + + inputs.yaw = Rotation2d.fromDegrees(gyro.getYaw()); + inputs.yawVelocityRadPerSec = Units.degreesToRadians(gyro.getRawGyroZ()); + + inputs.pitch = Rotation2d.fromDegrees(gyro.getPitch()); + inputs.pitchVelocityRadPerSec = Units.degreesToRadians(gyro.getRawGyroX()); + + inputs.roll = Rotation2d.fromDegrees(gyro.getRoll()); + inputs.rollVelocityRadPerSec = Units.degreesToRadians(gyro.getRawGyroY()); + + inputs.yawRads = yawQueue.stream().mapToDouble(x -> x).toArray(); + yawQueue.clear(); + } + +} diff --git a/src/main/java/frc/robot/subsystems/swerve/gyro/GyroSim.java b/src/main/java/frc/robot/subsystems/swerve/gyro/GyroSim.java new file mode 100644 index 0000000..5d3f62c --- /dev/null +++ b/src/main/java/frc/robot/subsystems/swerve/gyro/GyroSim.java @@ -0,0 +1,26 @@ +package frc.robot.subsystems.swerve.gyro; + +import static edu.wpi.first.units.Units.RadiansPerSecond; +import org.ironmaple.simulation.drivesims.GyroSimulation; +import org.jspecify.annotations.NullMarked; + +/** Simulation implementation for gyro */ +@NullMarked +public class GyroSim implements GyroIO { + + private final GyroSimulation gyro; + + /** Simulation implementation for gyro */ + public GyroSim(GyroSimulation gyro) { + this.gyro = gyro; + } + + @Override + public void updateInputs(GyroInputs inputs) { + inputs.yaw = gyro.getGyroReading(); + inputs.yawVelocityRadPerSec = gyro.getMeasuredAngularVelocity().in(RadiansPerSecond); + inputs.yawRads = new double[] {inputs.yaw.getRadians()}; + inputs.connected = true; + } + +} diff --git a/src/main/java/frc/robot/subsystems/swerve/mod/ModuleConstants.java b/src/main/java/frc/robot/subsystems/swerve/mod/ModuleConstants.java new file mode 100644 index 0000000..4a79045 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/swerve/mod/ModuleConstants.java @@ -0,0 +1,34 @@ +package frc.robot.subsystems.swerve.mod; + +import org.jspecify.annotations.NullMarked; +import edu.wpi.first.math.geometry.Rotation2d; +import frc.robot.util.typestate.RequiredField; +import frc.robot.util.typestate.TypeStateBuilder; + +/** Per-module constants */ +@NullMarked +public class ModuleConstants { + + /** CAN ID for the drive motor */ + public final int driveMotorId; + + /** CAN ID for the angle motor */ + public final int angleMotorId; + + /** CAN ID for the CANCoder */ + public final int canCoderId; + + /** Reported angle when wheel is straight */ + public final Rotation2d angleOffset; + + /** Per-module constants */ + @TypeStateBuilder("ModuleConstantsBuilder") + public ModuleConstants(@RequiredField int driveMotorId, @RequiredField int angleMotorId, + @RequiredField int canCoderId, @RequiredField Rotation2d angleOffset) { + this.driveMotorId = driveMotorId; + this.angleMotorId = angleMotorId; + this.canCoderId = canCoderId; + this.angleOffset = angleOffset; + } + +} diff --git a/src/main/java/frc/robot/subsystems/swerve/mod/SwerveModule.java b/src/main/java/frc/robot/subsystems/swerve/mod/SwerveModule.java new file mode 100644 index 0000000..357c6e7 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/swerve/mod/SwerveModule.java @@ -0,0 +1,110 @@ +package frc.robot.subsystems.swerve.mod; + +import static edu.wpi.first.units.Units.Meters; +import org.jspecify.annotations.NullMarked; +import org.littletonrobotics.junction.Logger; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.kinematics.SwerveModulePosition; +import edu.wpi.first.math.kinematics.SwerveModuleState; +import edu.wpi.first.math.util.Units; +import frc.robot.Constants; + +/** Swerve Module */ +@NullMarked +public class SwerveModule { + + private final String inputsName; + private final SwerveModuleIO io; + private final SwerveModuleInputsAutoLogged inputs = new SwerveModuleInputsAutoLogged(); + + /** + * Swerve Module + * + * @param moduleId index into the motor constants + * @param io IO implementation + */ + public SwerveModule(int moduleId, SwerveModuleIO io) { + this.inputsName = "Swerve/Module" + moduleId; + this.io = io; + } + + /** + * Update inputs for a Swerve Module. + */ + public void updateInputs() { + this.io.updateInputs(this.inputs); + Logger.processInputs(this.inputsName, this.inputs); + } + + /** + * Set the desired state of the Swerve Module + * + * @param desiredState The desired {@link SwerveModuleState} for the module + */ + public void setDesiredState(SwerveModuleState desiredState) { + desiredState.optimize(getState().angle); + this.io.runAnglePosition(desiredState.angle); + this.io.runDriveVelocity( + desiredState.speedMetersPerSecond / Constants.Swerve.wheelRadius.in(Meters), 0); + } + + /** Get the current Swerve Module State */ + public SwerveModuleState getState() { + return new SwerveModuleState( + inputs.driveVelocityRadPerSec * Constants.Swerve.wheelRadius.in(Meters), + inputs.anglePosition); + } + + /** Get the current Swerve Module Position */ + public SwerveModulePosition getPosition() { + return new SwerveModulePosition( + inputs.drivePositionRad * Constants.Swerve.wheelRadius.in(Meters), + inputs.anglePosition); + } + + /** Run motor at given voltage with angle motor pointing forward */ + public void runCharacterization(double output) { + io.runDriveOpenLoop(output); + io.runAnglePosition(Rotation2d.kZero); + } + + /** Returns the module position in radians. */ + public double getWheelRadiusCharacterizationPosition() { + return inputs.drivePositionRad; + } + + /** Returns the module velocity in rotations/sec (Phoenix native units). */ + public double getFFCharacterizationVelocity() { + return Units.radiansToRotations(inputs.driveVelocityRadPerSec); + } + + /** Set drive motor brake mode */ + public void setBrakeMode(boolean enabled) { + this.io.setDriveBrakeMode(enabled); + } + + /** Set angle motor brake mode */ + public void setFreeSpin(boolean enabled) { + this.io.setAngleBrakeMode(!enabled); + } + + private SwerveModulePosition[] odometryPositions = new SwerveModulePosition[0]; + + /** Periodic function (doesn't update inputs) */ + public void periodic() { + int sampleCount = inputs.odometryDrivePositionsRad.length; + odometryPositions = new SwerveModulePosition[sampleCount]; + for (int i = 0; i < sampleCount; i++) { + double positionMeters = + inputs.odometryDrivePositionsRad[i] * Constants.Swerve.wheelRadius.in(Meters); + Rotation2d angle = inputs.odometryAnglePositions[i]; + odometryPositions[i] = new SwerveModulePosition(positionMeters, angle); + } + } + + /** Get swerve module position at timestamp index i */ + public SwerveModulePosition getOdometryPosition(int i) { + return odometryPositions[i]; + } + +} diff --git a/src/main/java/frc/robot/subsystems/swerve/mod/SwerveModuleIO.java b/src/main/java/frc/robot/subsystems/swerve/mod/SwerveModuleIO.java new file mode 100644 index 0000000..e97b9d2 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/swerve/mod/SwerveModuleIO.java @@ -0,0 +1,66 @@ +package frc.robot.subsystems.swerve.mod; + +import org.jspecify.annotations.NullMarked; +import org.littletonrobotics.junction.AutoLog; +import edu.wpi.first.math.geometry.Rotation2d; +import frc.robot.subsystems.swerve.util.PhoenixOdometryThread; +import frc.robot.util.GenerateEmptyIO; + +/** IO for swerve module */ +@GenerateEmptyIO({int.class, PhoenixOdometryThread.class}) +@NullMarked +public interface SwerveModuleIO { + + /** Inputs for swerve module */ + @AutoLog + public static class SwerveModuleInputs { + public boolean driveConnected; + public double drivePositionRad; + public double driveVelocityRadPerSec; + public double driveAppliedVolts; + public double driveSupplyCurrentAmps; + public double driveStatorCurrentAmps; + + public boolean angleConnected; + public Rotation2d anglePosition = Rotation2d.kZero; + public double angleVelocityRadPerSec; + public double angleAppliedVolts; + public double angleSupplyCurrentAmps; + public double angleStatorCurrentAmps; + + public boolean absoluteAngleConnected; + public Rotation2d angleAbsolutePosition = Rotation2d.kZero; + + public double[] odometryDrivePositionsRad = new double[0]; + public double[] odometryDriveVelocityRadsPerSec = new double[0]; + public Rotation2d[] odometryAnglePositions = new Rotation2d[0]; + } + + /** Update inputs */ + public void updateInputs(SwerveModuleInputs inputs); + + /** Set voltage on drive motor */ + public void runDriveOpenLoop(double output); + + /** Set voltage on angle motor */ + public void runAngleOpenLoop(double output); + + /** Set velocity setpoint */ + public void runDriveVelocity(double velocityRadPerSec, double feedforward); + + /** Set angle setpoint */ + public void runAnglePosition(Rotation2d rotation); + + /** Set PID constants for drive motor */ + public void setDrivePID(double kP, double kI, double kD, double kS, double kV, double kA); + + /** Set PID constants for angle motor */ + public void setAnglePID(double kP, double kI, double kD); + + /** Set drive brake mode */ + public void setDriveBrakeMode(boolean enabled); + + /** Set angle brake mode */ + public void setAngleBrakeMode(boolean enabled); + +} diff --git a/src/main/java/frc/robot/subsystems/swerve/mod/SwerveModuleReal.java b/src/main/java/frc/robot/subsystems/swerve/mod/SwerveModuleReal.java new file mode 100644 index 0000000..dd79368 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/swerve/mod/SwerveModuleReal.java @@ -0,0 +1,299 @@ +package frc.robot.subsystems.swerve.mod; + +import java.util.Queue; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import org.jspecify.annotations.NullMarked; +import com.ctre.phoenix6.BaseStatusSignal; +import com.ctre.phoenix6.StatusSignal; +import com.ctre.phoenix6.configs.CANcoderConfiguration; +import com.ctre.phoenix6.configs.TalonFXConfiguration; +import com.ctre.phoenix6.controls.PositionVoltage; +import com.ctre.phoenix6.controls.VelocityVoltage; +import com.ctre.phoenix6.controls.VoltageOut; +import com.ctre.phoenix6.hardware.CANcoder; +import com.ctre.phoenix6.hardware.ParentDevice; +import com.ctre.phoenix6.hardware.TalonFX; +import com.ctre.phoenix6.signals.FeedbackSensorSourceValue; +import com.ctre.phoenix6.signals.NeutralModeValue; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.units.measure.Angle; +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.units.measure.Current; +import edu.wpi.first.units.measure.Voltage; +import frc.robot.Constants; +import frc.robot.subsystems.swerve.util.PhoenixOdometryThread; +import frc.robot.util.DeviceDebug; +import frc.robot.util.PhoenixSignals; + +/** Real swerve module implementation (assumes two TalonFXs) */ +@NullMarked +public class SwerveModuleReal implements SwerveModuleIO { + + // Drive Motor and Signals + private final TalonFX driveMotor; + private final TalonFXConfiguration driveConfig = new TalonFXConfiguration(); + + private final StatusSignal drivePosition; + private final Queue drivePositionQueue; + private final StatusSignal driveVelocity; + private final StatusSignal driveAppliedVolts; + private final StatusSignal driveSupplyCurrentAmps; + private final StatusSignal driveStatorCurrentAmps; + + // Angle Motor and Signals + private final TalonFX angleMotor; + private final TalonFXConfiguration angleConfig = new TalonFXConfiguration(); + + private final StatusSignal anglePosition; + private final Queue anglePositionQueue; + private final StatusSignal angleVelocity; + private final StatusSignal angleAppliedVolts; + private final StatusSignal angleSupplyCurrentAmps; + private final StatusSignal angleStatorCurrentAmps; + + // Absolute Encoder Signals + private final CANcoder absoluteEncoder; + private final CANcoderConfiguration absoluteConfig = new CANcoderConfiguration(); + private final StatusSignal absolutePosition; + + // Absolute Offset + private final Rotation2d angleOffset; + + /** Real swerve module implementation */ + public SwerveModuleReal(int index, PhoenixOdometryThread odometryThread) { + boolean isCanivore = Constants.Swerve.isCanviore; + + String loop = isCanivore ? "*" : ""; + driveMotor = new TalonFX(Constants.Swerve.modulesConstants[index].driveMotorId, loop); + angleMotor = new TalonFX(Constants.Swerve.modulesConstants[index].angleMotorId, loop); + absoluteEncoder = new CANcoder(Constants.Swerve.modulesConstants[index].canCoderId, loop); + angleOffset = Constants.Swerve.modulesConstants[index].angleOffset; + + // Create drive motor signals + drivePosition = driveMotor.getPosition(); + drivePositionQueue = odometryThread.registerSignal(driveMotor.getPosition().clone()); + driveVelocity = driveMotor.getVelocity(); + driveAppliedVolts = driveMotor.getMotorVoltage(); + driveSupplyCurrentAmps = driveMotor.getSupplyCurrent(); + driveStatorCurrentAmps = driveMotor.getStatorCurrent(); + + // Create angle motor signals + anglePosition = angleMotor.getPosition(); + anglePositionQueue = odometryThread.registerSignal(angleMotor.getPosition().clone()); + angleVelocity = angleMotor.getVelocity(); + angleAppliedVolts = angleMotor.getMotorVoltage(); + angleSupplyCurrentAmps = angleMotor.getSupplyCurrent(); + angleStatorCurrentAmps = angleMotor.getStatorCurrent(); + + // Create absolute encoder signals + absolutePosition = absoluteEncoder.getAbsolutePosition(); + + configDriveMotor(); + configAngleMotor(); + configAngleEncoder(); + + // Configure periodic frames + BaseStatusSignal.setUpdateFrequencyForAll(Constants.Swerve.odometryFrequency, drivePosition, + anglePosition); + BaseStatusSignal.setUpdateFrequencyForAll(50.0, driveVelocity, driveAppliedVolts, + driveSupplyCurrentAmps, driveStatorCurrentAmps, angleVelocity, angleAppliedVolts, + angleSupplyCurrentAmps, angleStatorCurrentAmps); + PhoenixSignals.tryUntilOk(5, () -> ParentDevice.optimizeBusUtilizationForAll(driveMotor, + angleMotor, absoluteEncoder)); + + // Register signals for refresh + PhoenixSignals.registerSignals(isCanivore, drivePosition, driveVelocity, driveAppliedVolts, + driveSupplyCurrentAmps, driveStatorCurrentAmps, anglePosition, angleVelocity, + angleAppliedVolts, angleSupplyCurrentAmps, angleStatorCurrentAmps, absolutePosition); + + DeviceDebug.register("SwerveModule_" + index + "_Drive", driveMotor); + DeviceDebug.register("SwerveModule_" + index + "_Angle", angleMotor); + DeviceDebug.register("SwerveModule_" + index + "_Encoder", absoluteEncoder); + } + + private void configDriveMotor() { + /* Drive Motor Config */ + /* Motor Inverts and Neutral Mode */ + driveConfig.MotorOutput.Inverted = Constants.Swerve.driveMotorInvert; + driveConfig.MotorOutput.NeutralMode = Constants.Swerve.driveNeutralMode; + + /* Gear Ratio Config */ + driveConfig.Feedback.SensorToMechanismRatio = Constants.Swerve.driveGearRatio; + + /* Current Limiting */ + driveConfig.CurrentLimits.SupplyCurrentLimitEnable = + Constants.Swerve.driveEnableCurrentLimit; + driveConfig.CurrentLimits.SupplyCurrentLimit = Constants.Swerve.driveCurrentLimit; + driveConfig.CurrentLimits.SupplyCurrentLowerLimit = Constants.Swerve.driveCurrentLowerLimit; + driveConfig.CurrentLimits.SupplyCurrentLowerTime = + Constants.Swerve.driveCurrentLowerTimeThreshold; + + /* PID Config */ + driveConfig.Slot0.kP = Constants.Swerve.driveKP; + driveConfig.Slot0.kI = Constants.Swerve.driveKI; + driveConfig.Slot0.kD = Constants.Swerve.driveKD; + driveConfig.Slot0.kS = Constants.Swerve.driveKS; + driveConfig.Slot0.kV = Constants.Swerve.driveKV; + driveConfig.Slot0.kA = Constants.Swerve.driveKA; + + /* Open and Closed Loop Ramping */ + driveConfig.OpenLoopRamps.DutyCycleOpenLoopRampPeriod = Constants.Swerve.openLoopRamp; + driveConfig.OpenLoopRamps.VoltageOpenLoopRampPeriod = Constants.Swerve.openLoopRamp; + + driveConfig.ClosedLoopRamps.DutyCycleClosedLoopRampPeriod = Constants.Swerve.closedLoopRamp; + driveConfig.ClosedLoopRamps.VoltageClosedLoopRampPeriod = Constants.Swerve.closedLoopRamp; + + PhoenixSignals.tryUntilOk(5, () -> driveMotor.getConfigurator().apply(driveConfig, 0.25)); + PhoenixSignals.tryUntilOk(5, () -> driveMotor.getConfigurator().setPosition(0.0, 0.25)); + } + + private void configAngleMotor() { + /* Angle Motor Config */ + /* Motor Inverts and Neutral Mode */ + angleConfig.MotorOutput.Inverted = Constants.Swerve.angleMotorInvert; + angleConfig.MotorOutput.NeutralMode = Constants.Swerve.angleNeutralMode; + + /* Gear Ratio and Wrapping Config */ + angleConfig.Feedback.FeedbackRemoteSensorID = absoluteEncoder.getDeviceID(); + angleConfig.Feedback.FeedbackSensorSource = FeedbackSensorSourceValue.FusedCANcoder; + angleConfig.Feedback.SensorToMechanismRatio = 1.0; + angleConfig.Feedback.RotorToSensorRatio = Constants.Swerve.angleGearRatio; + angleConfig.ClosedLoopGeneral.ContinuousWrap = true; + + /* Current Limiting */ + angleConfig.CurrentLimits.SupplyCurrentLimitEnable = + Constants.Swerve.angleEnableCurrentLimit; + angleConfig.CurrentLimits.SupplyCurrentLimit = Constants.Swerve.angleCurrentLimit; + angleConfig.CurrentLimits.SupplyCurrentLowerLimit = Constants.Swerve.angleCurrentLowerLimit; + angleConfig.CurrentLimits.SupplyCurrentLowerTime = + Constants.Swerve.angleCurrentLowerTimeThreshold; + + /* PID Config */ + angleConfig.Slot0.kP = Constants.Swerve.angleKP; + angleConfig.Slot0.kI = Constants.Swerve.angleKI; + angleConfig.Slot0.kD = Constants.Swerve.angleKD; + + PhoenixSignals.tryUntilOk(5, () -> angleMotor.getConfigurator().apply(angleConfig)); + } + + private void configAngleEncoder() { + /* Angle Encoder Config */ + absoluteConfig.MagnetSensor.SensorDirection = Constants.Swerve.cancoderInvert; + absoluteConfig.MagnetSensor.AbsoluteSensorDiscontinuityPoint = 0.5; + absoluteConfig.MagnetSensor.MagnetOffset = -angleOffset.getRotations(); + + PhoenixSignals.tryUntilOk(5, + () -> absoluteEncoder.getConfigurator().apply(absoluteConfig, 0.25)); + } + + @Override + public void updateInputs(SwerveModuleInputs inputs) { + // Drive Motor inputs + inputs.driveConnected = BaseStatusSignal.isAllGood(drivePosition, driveVelocity, + driveAppliedVolts, driveSupplyCurrentAmps, driveStatorCurrentAmps); + inputs.drivePositionRad = Units.rotationsToRadians(drivePosition.getValueAsDouble()); + inputs.driveVelocityRadPerSec = Units.rotationsToRadians(driveVelocity.getValueAsDouble()); + inputs.driveAppliedVolts = driveAppliedVolts.getValueAsDouble(); + inputs.driveSupplyCurrentAmps = driveSupplyCurrentAmps.getValueAsDouble(); + inputs.driveStatorCurrentAmps = driveStatorCurrentAmps.getValueAsDouble(); + + // Angle Motor inputs + inputs.angleConnected = BaseStatusSignal.isAllGood(anglePosition, angleVelocity, + angleAppliedVolts, angleSupplyCurrentAmps, angleStatorCurrentAmps); + inputs.anglePosition = Rotation2d.fromRotations(anglePosition.getValueAsDouble()); + inputs.angleVelocityRadPerSec = Units.rotationsToRadians(angleVelocity.getValueAsDouble()); + inputs.angleAppliedVolts = angleAppliedVolts.getValueAsDouble(); + inputs.angleSupplyCurrentAmps = angleSupplyCurrentAmps.getValueAsDouble(); + inputs.angleStatorCurrentAmps = angleStatorCurrentAmps.getValueAsDouble(); + + // Encoder inputs + inputs.absoluteAngleConnected = BaseStatusSignal.isAllGood(absolutePosition); + inputs.angleAbsolutePosition = + Rotation2d.fromRotations(absolutePosition.getValueAsDouble()); + + // Odometry inputs + inputs.odometryDrivePositionsRad = + drivePositionQueue.stream().mapToDouble(Units::rotationsToRadians).toArray(); + inputs.odometryAnglePositions = + anglePositionQueue.stream().map(Rotation2d::fromRotations).toArray(Rotation2d[]::new); + drivePositionQueue.clear(); + anglePositionQueue.clear(); + } + + private final VoltageOut driveVoltage = new VoltageOut(0.0); + + @Override + public void runDriveOpenLoop(double output) { + driveMotor.setControl(driveVoltage.withOutput(output)); + } + + private final VoltageOut angleVoltage = new VoltageOut(0.0); + + @Override + public void runAngleOpenLoop(double output) { + angleMotor.setControl(angleVoltage.withOutput(output)); + } + + private final VelocityVoltage driveVelocityVoltage = new VelocityVoltage(0.0); + + @Override + public void runDriveVelocity(double velocityRadPerSec, double feedforward) { + driveMotor.setControl( + driveVelocityVoltage.withVelocity(velocityRadPerSec).withFeedForward(feedforward)); + } + + private final PositionVoltage anglePositionVoltage = new PositionVoltage(0.0); + + @Override + public void runAnglePosition(Rotation2d rotation) { + angleMotor.setControl(anglePositionVoltage.withPosition(rotation.getRotations())); + } + + @Override + public void setDrivePID(double kP, double kI, double kD, double kS, double kV, double kA) { + driveConfig.Slot0.kP = kP; + driveConfig.Slot0.kI = kI; + driveConfig.Slot0.kD = kD; + driveConfig.Slot0.kS = kS; + driveConfig.Slot0.kV = kV; + driveConfig.Slot0.kA = kA; + PhoenixSignals.tryUntilOk(5, () -> driveMotor.getConfigurator().apply(driveConfig, 0.25)); + } + + @Override + public void setAnglePID(double kP, double kI, double kD) { + angleConfig.Slot0.kP = kP; + angleConfig.Slot0.kI = kI; + angleConfig.Slot0.kD = kD; + PhoenixSignals.tryUntilOk(5, () -> angleMotor.getConfigurator().apply(angleConfig, 0.25)); + } + + private static final Executor brakeModeExecutor = Executors.newFixedThreadPool(8); + + @Override + public void setDriveBrakeMode(boolean enabled) { + brakeModeExecutor.execute(() -> { + synchronized (driveConfig) { + driveConfig.MotorOutput.NeutralMode = + enabled ? NeutralModeValue.Brake : NeutralModeValue.Coast; + PhoenixSignals.tryUntilOk(5, + () -> driveMotor.getConfigurator().apply(driveConfig, 0.25)); + } + }); + } + + @Override + public void setAngleBrakeMode(boolean enabled) { + brakeModeExecutor.execute(() -> { + synchronized (angleConfig) { + angleConfig.MotorOutput.NeutralMode = + enabled ? NeutralModeValue.Brake : NeutralModeValue.Coast; + PhoenixSignals.tryUntilOk(5, + () -> angleMotor.getConfigurator().apply(angleConfig, 0.25)); + } + }); + } + +} diff --git a/src/main/java/frc/robot/subsystems/swerve/mod/SwerveModuleSim.java b/src/main/java/frc/robot/subsystems/swerve/mod/SwerveModuleSim.java new file mode 100644 index 0000000..5763a8c --- /dev/null +++ b/src/main/java/frc/robot/subsystems/swerve/mod/SwerveModuleSim.java @@ -0,0 +1,142 @@ +package frc.robot.subsystems.swerve.mod; + +import static edu.wpi.first.units.Units.Amps; +import static edu.wpi.first.units.Units.Radians; +import static edu.wpi.first.units.Units.RadiansPerSecond; +import org.ironmaple.simulation.drivesims.SwerveModuleSimulation; +import org.ironmaple.simulation.motorsims.SimulatedMotorController; +import org.jspecify.annotations.NullMarked; +import org.littletonrobotics.junction.Logger; +import edu.wpi.first.math.controller.PIDController; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.units.Units; +import frc.robot.Constants; + +/** Simulation implementation for Swerve Module */ +@NullMarked +public class SwerveModuleSim implements SwerveModuleIO { + + private final SwerveModuleSimulation moduleSimulation; + private final SimulatedMotorController.GenericMotorController driveMotor; + private final SimulatedMotorController.GenericMotorController turnMotor; + + private final int id; + + private boolean driveClosedLoop = false; + private boolean turnClosedLoop = false; + private final PIDController driveController; + private final PIDController turnController; + private double driveFFVolts = 0.0; + private double driveAppliedVolts = 0.0; + private double turnAppliedVolts = 0.0; + + private double kV = 1.0; + private double targetVelocity = 0.0; + + /** Simulation implementation for Swerve Module */ + public SwerveModuleSim(int index, SwerveModuleSimulation modSim) { + this.id = index; + this.moduleSimulation = modSim; + this.driveMotor = moduleSimulation.useGenericMotorControllerForDrive() + .withCurrentLimit(Amps.of(Constants.Swerve.driveCurrentLimit)); + this.turnMotor = + moduleSimulation.useGenericControllerForSteer().withCurrentLimit(Amps.of(20)); + + this.driveController = new PIDController(0.5, 0.0, 0.0); + this.turnController = new PIDController(8.0, 0.0, 0.0); + + // Enable wrapping for turn PID + turnController.enableContinuousInput(-Math.PI, Math.PI); + } + + @Override + public void updateInputs(SwerveModuleInputs inputs) { + if (driveClosedLoop) { + if (Math.abs(driveController.getSetpoint()) < 0.01) { + driveAppliedVolts = 0.0; + driveController.reset(); + } else { + driveAppliedVolts = driveFFVolts + + driveController.calculate( + moduleSimulation.getDriveWheelFinalSpeed().in(Units.RadiansPerSecond)) + + kV * targetVelocity; + } + } else { + driveController.reset(); + } + if (turnClosedLoop) { + turnAppliedVolts = + turnController.calculate(moduleSimulation.getSteerAbsoluteFacing().getRadians()); + } else { + turnController.reset(); + } + + Logger.recordOutput("driveVoltage" + id, driveAppliedVolts); + Logger.recordOutput("angleVoltage" + id, turnAppliedVolts); + + driveMotor.requestVoltage(Units.Volts.of(driveAppliedVolts)); + turnMotor.requestVoltage(Units.Volts.of(turnAppliedVolts)); + + inputs.driveConnected = true; + inputs.angleConnected = true; + inputs.absoluteAngleConnected = true; + + inputs.drivePositionRad = moduleSimulation.getDriveWheelFinalPosition().in(Radians); + inputs.driveVelocityRadPerSec = + moduleSimulation.getDriveWheelFinalSpeed().in(RadiansPerSecond); + inputs.angleAbsolutePosition = moduleSimulation.getSteerAbsoluteFacing(); + inputs.anglePosition = inputs.angleAbsolutePosition; + inputs.angleVelocityRadPerSec = + moduleSimulation.getSteerAbsoluteEncoderSpeed().in(RadiansPerSecond); + + inputs.odometryDrivePositionsRad = new double[] {inputs.drivePositionRad}; + inputs.odometryDriveVelocityRadsPerSec = new double[] {inputs.driveVelocityRadPerSec}; + inputs.odometryAnglePositions = new Rotation2d[] {inputs.anglePosition}; + } + + @Override + public void runDriveOpenLoop(double output) { + driveClosedLoop = false; + driveAppliedVolts = output; + } + + @Override + public void runAngleOpenLoop(double output) { + turnClosedLoop = false; + turnAppliedVolts = output; + } + + @Override + public void runDriveVelocity(double velocityRadPerSec, double feedforward) { + driveClosedLoop = true; + driveController.setSetpoint(velocityRadPerSec); + targetVelocity = velocityRadPerSec; + } + + @Override + public void runAnglePosition(Rotation2d rotation) { + turnClosedLoop = true; + turnController.setSetpoint(rotation.getRadians()); + } + + @Override + public void setDrivePID(double kP, double kI, double kD, double kS, double kV, double kA) { + // Changing PID not handled in sim + } + + @Override + public void setAnglePID(double kP, double kI, double kD) { + // Changing PID not handled in sim + } + + @Override + public void setDriveBrakeMode(boolean enabled) { + // Brake mode not handled in sim + } + + @Override + public void setAngleBrakeMode(boolean enabled) { + // Brake mode not handled in sim + } + +} diff --git a/src/main/java/frc/robot/subsystems/swerve/util/MoveToPose.java b/src/main/java/frc/robot/subsystems/swerve/util/MoveToPose.java new file mode 100644 index 0000000..53adf77 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/swerve/util/MoveToPose.java @@ -0,0 +1,191 @@ +package frc.robot.subsystems.swerve.util; + +import java.util.function.Consumer; +import java.util.function.DoubleSupplier; +import java.util.function.Supplier; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import choreo.auto.AutoRoutine; +import edu.wpi.first.math.controller.HolonomicDriveController; +import edu.wpi.first.math.controller.PIDController; +import edu.wpi.first.math.controller.ProfiledPIDController; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.kinematics.ChassisSpeeds; +import edu.wpi.first.math.trajectory.TrapezoidProfile.Constraints; +import edu.wpi.first.wpilibj.event.EventLoop; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.CommandScheduler; +import edu.wpi.first.wpilibj2.command.button.Trigger; +import frc.robot.Constants; +import frc.robot.subsystems.swerve.Swerve; +import frc.robot.util.AllianceFlipUtil; +import frc.robot.util.typestate.AltMethod; +import frc.robot.util.typestate.InitField; +import frc.robot.util.typestate.OptionalField; +import frc.robot.util.typestate.RequiredField; +import frc.robot.util.typestate.TypeStateBuilder; + +/** + * Command that drives a swerve drivetrain to a specified field-relative pose using a + * {@link HolonomicDriveController}. + * + *

+ * This command continuously computes robot-relative chassis speeds that reduce translational and + * rotational error to a target {@link Pose2d}. The command finishes once the robot is within the + * specified translation and rotation tolerances. + * + *

+ * The target pose may be supplied dynamically (e.g., from vision or an autonomous routine). An + * optional alliance-based pose flip can be applied automatically for red alliance operation. + * + *

+ * Output chassis speeds are robot-relative and are clamped to a configurable maximum translational + * speed. + * + *

+ * This command is typically created via {@code Swerve.moveToPose(...)} and should not be + * instantiated directly. + */ +@NullMarked +public class MoveToPose extends Command { + + private final AutoRoutine autoRoutine; + private final EventLoop eventLoop; + + private final Swerve swerve; + private final Consumer robotRelativeConsumer; + private final Supplier pose2dSupplier; + private final DoubleSupplier maxSpeedSupplier; + private final boolean flipForRed; + private final double translationTolerance; + private final double rotationTolerance; + + private boolean isActive = false; + private boolean isCompleted = false; + + /** + * Constructs a command that drives the robot to a target pose. + * + *

+ * Note: This constructor is intended for use by generated builders. Prefer + * {@code Swerve.moveToPose(...)} when creating this command. + * + * @param swerve drivetrain subsystem providing pose estimation + * @param robotRelativeConsumer consumer that accepts robot-relative chassis speeds + * @param target supplier providing the desired field-relative target pose + * @param autoRoutine optional autonomous routine used to scope triggers + * @param maxSpeed supplier providing the maximum allowed translational speed (m/s) + * @param flipForRed whether to mirror the target pose for red alliance + * @param translationTolerance allowable positional error (meters) + * @param rotationTolerance allowable angular error (radians) + */ + @TypeStateBuilder("MoveToPoseBuilder") + public MoveToPose(@InitField Swerve swerve, + @InitField Consumer robotRelativeConsumer, + @RequiredField(alt = @AltMethod(type = Pose2d.class, parameter_name = "targetConst", + value = "() -> targetConst")) Supplier target, + @OptionalField("null") @Nullable AutoRoutine autoRoutine, + @OptionalField(value = "() -> frc.robot.Constants.Swerve.autoMaxSpeed", + alt = @AltMethod(type = double.class, parameter_name = "maxSpeedConst", + value = "() -> maxSpeedConst")) DoubleSupplier maxSpeed, + @OptionalField("true") boolean flipForRed, + @OptionalField("0.5") double translationTolerance, + @OptionalField("edu.wpi.first.math.util.Units.degreesToRadians(5)") double rotationTolerance) { + this.autoRoutine = autoRoutine; + if (autoRoutine == null) { + this.eventLoop = CommandScheduler.getInstance().getDefaultButtonLoop(); + } else { + this.eventLoop = autoRoutine.loop(); + } + this.swerve = swerve; + this.robotRelativeConsumer = robotRelativeConsumer; + this.pose2dSupplier = target; + this.maxSpeedSupplier = maxSpeed; + this.flipForRed = flipForRed; + this.translationTolerance = translationTolerance; + this.rotationTolerance = rotationTolerance; + } + + /** + * Returns a trigger that is {@code true} while this command is actively running. + * + *

+ * If associated with an {@link AutoRoutine}, the trigger is also gated by the routine's active + * state. + * + * @return trigger indicating whether the command is currently active + */ + public Trigger active() { + if (autoRoutine != null) { + return new Trigger(eventLoop, + () -> this.isActive && autoRoutine.active().getAsBoolean()); + } + return new Trigger(eventLoop, () -> this.isActive); + } + + /** + * Returns a trigger that becomes {@code true} once the command has completed successfully. + * + *

+ * The trigger remains {@code true} until the command is restarted. + * + * @return trigger indicating completion of the command + */ + public Trigger done() { + return new Trigger(eventLoop, () -> this.isCompleted); + } + + @Override + public void initialize() { + isActive = true; + isCompleted = false; + } + + private static final HolonomicDriveController holonomicDriveController = + new HolonomicDriveController(new PIDController(Constants.SwerveTransformPID.translationP, + Constants.SwerveTransformPID.translationI, Constants.SwerveTransformPID.translationD), + new PIDController(Constants.SwerveTransformPID.translationP, + Constants.SwerveTransformPID.translationI, + Constants.SwerveTransformPID.translationD), + new ProfiledPIDController(Constants.SwerveTransformPID.rotationP, + Constants.SwerveTransformPID.rotationI, Constants.SwerveTransformPID.rotationD, + new Constraints(Constants.SwerveTransformPID.maxAngularVelocity, + Constants.SwerveTransformPID.maxAngularAcceleration))); + + private Pose2d target = Pose2d.kZero; + + @Override + public void execute() { + target = this.pose2dSupplier.get(); + if (flipForRed) { + target = AllianceFlipUtil.apply(target); + } + ChassisSpeeds ctrlEffort = holonomicDriveController + .calculate(swerve.state.getGlobalPoseEstimate(), target, 0, target.getRotation()); + double speed = Math.hypot(ctrlEffort.vxMetersPerSecond, ctrlEffort.vyMetersPerSecond); + double maxSpeed = this.maxSpeedSupplier.getAsDouble(); + if (speed > maxSpeed) { + double mul = maxSpeed / speed; + ctrlEffort.vxMetersPerSecond *= mul; + ctrlEffort.vyMetersPerSecond *= mul; + } + this.robotRelativeConsumer.accept(ctrlEffort); + } + + @Override + public void end(boolean interrupted) { + isActive = false; + isCompleted = !interrupted; + } + + @Override + public boolean isFinished() { + Pose2d poseError = Pose2d.kZero.plus(target.minus(swerve.state.getGlobalPoseEstimate())); + final var eTranslate = poseError.getTranslation(); + final var eRotate = poseError.getRotation(); + return Math.abs(eTranslate.getX()) < translationTolerance + && Math.abs(eTranslate.getY()) < translationTolerance + && Math.abs(eRotate.getDegrees()) < rotationTolerance; + } + +} diff --git a/src/main/java/frc/robot/subsystems/swerve/util/PhoenixOdometryThread.java b/src/main/java/frc/robot/subsystems/swerve/util/PhoenixOdometryThread.java new file mode 100644 index 0000000..5e0accc --- /dev/null +++ b/src/main/java/frc/robot/subsystems/swerve/util/PhoenixOdometryThread.java @@ -0,0 +1,233 @@ +package frc.robot.subsystems.swerve.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.DoubleSupplier; +import org.jspecify.annotations.NullMarked; +import com.ctre.phoenix6.BaseStatusSignal; +import com.ctre.phoenix6.CANBus; +import com.ctre.phoenix6.StatusSignal; +import edu.wpi.first.wpilibj.RobotController; +import frc.robot.Constants; + +/** + * Background thread responsible for sampling odometry-related signals at a higher frequency than + * the main robot loop. + * + *

+ * This thread is primarily intended to reduce latency and aliasing when collecting drivetrain + * sensor data (e.g., wheel encoders, gyro rates) used for odometry and state estimation. + * + *

+ * Two categories of signals may be registered: + *

+ * + *

+ * Each registered signal is associated with a thread-safe {@link Queue} into which samples are + * pushed. Consumers are expected to poll these queues from the main loop while holding the provided + * odometry lock. + * + *

+ * If running on a CAN-FD network, Phoenix signals are synchronized using + * {@link BaseStatusSignal#waitForAll(double, BaseStatusSignal...)}, allowing all signals to be + * sampled at nearly the same timestamp. On non-CAN-FD networks, the thread sleeps for the + * configured odometry period and explicitly refreshes signals. + * + *

+ * The thread runs as a daemon and will terminate automatically when the JVM exits. + */ +@NullMarked +public class PhoenixOdometryThread extends Thread { + + /** Lock shared with odometry consumers to ensure consistent sampling */ + private final Lock odometryLock; + + /** Lock protecting signal registration and Phoenix signal refresh */ + private final Lock signalsLock = new ReentrantLock(); + + /** Registered Phoenix status signals */ + private BaseStatusSignal[] phoenixSignals; + /** Registered non-Phoenix signal suppliers */ + private final List genericSignals = new ArrayList<>(); + /** Output queues corresponding to Phoenix signals */ + private final List> phoenixQueues = new ArrayList<>(); + /** Output queues corresponding to generic signals */ + private final List> genericQueues = new ArrayList<>(); + /** Queues receiving timestamps associated with each sample */ + private final List> timestampQueues = new ArrayList<>(); + + /** Whether the active CAN bus supports CAN-FD synchronization */ + private final boolean isCANFD = new CANBus("*").isNetworkFD(); + + /** + * Creates a new odometry sampling thread. + * + * @param odometryLock lock shared with consumers to ensure that sampled values and timestamps + * are read atomically + */ + public PhoenixOdometryThread(Lock odometryLock) { + this.odometryLock = odometryLock; + this.setName("PhoenixOdometryThread"); + this.setDaemon(true); + } + + /** + * Starts the thread if at least one signal has been registered. + * + *

+ * This prevents spawning an idle background thread when no signals are being sampled. + */ + @Override + public synchronized void start() { + // Only start the thread if stuff is actually registered. + if (phoenixQueues.size() > 0 || genericQueues.size() > 0) { + super.start(); + } + } + + /** + * Registers a Phoenix {@link StatusSignal} to be sampled by the thread. + * + *

+ * The returned queue will receive one value per odometry tick. + * + *

+ * This method is thread-safe and may be called before the thread starts. + * + * @param signal Phoenix status signal to sample + * @return queue containing sampled signal values + */ + public Queue registerSignal(StatusSignal signal) { + Queue queue = new ArrayBlockingQueue<>(20); + signalsLock.lock(); + odometryLock.lock(); + try { + BaseStatusSignal[] newSignals = new BaseStatusSignal[phoenixSignals.length + 1]; + System.arraycopy(phoenixSignals, 0, newSignals, 0, phoenixSignals.length); + newSignals[phoenixSignals.length] = signal; + phoenixSignals = newSignals; + phoenixQueues.add(queue); + } finally { + signalsLock.unlock(); + odometryLock.unlock(); + } + return queue; + } + + /** + * Registers a generic value supplier to be sampled at the odometry rate. + * + *

+ * This is useful for signals not managed by Phoenix (e.g., WPILib sensors or computed values). + * + * @param signal supplier producing the value to sample + * @return queue containing sampled values + */ + public Queue registerSignal(DoubleSupplier signal) { + Queue queue = new ArrayBlockingQueue<>(20); + signalsLock.lock(); + odometryLock.lock(); + try { + genericSignals.add(signal); + genericQueues.add(queue); + } finally { + signalsLock.unlock(); + odometryLock.unlock(); + } + return queue; + } + + /** + * Creates a queue that receives timestamps corresponding to each sampling cycle. + * + *

+ * The timestamp represents FPGA time in seconds, adjusted for average Phoenix signal latency + * when applicable. + * + * @return queue of sample timestamps (seconds) + */ + public Queue makeTimestampQueue() { + Queue queue = new ArrayBlockingQueue<>(20); + odometryLock.lock(); + try { + timestampQueues.add(queue); + } finally { + odometryLock.unlock(); + } + return queue; + } + + /** + * Main sampling loop. + * + *

+ * The thread runs indefinitely, sampling all registered signals at + * {@code Constants.Swerve.odometryFrequency}. + * + *

+ * Sampling behavior differs depending on CAN capabilities: + *

+ * + *

+ * All sampled values and timestamps are enqueued atomically under the odometry lock to ensure + * consistency for consumers. + */ + @Override + public void run() { + while (true) { + signalsLock.lock(); + try { + if (isCANFD && phoenixSignals.length > 0) { + BaseStatusSignal.waitForAll(2.0 / Constants.Swerve.odometryFrequency, + phoenixSignals); + } else { + Thread.sleep((long) (1000.0 / Constants.Swerve.odometryFrequency)); + if (phoenixSignals.length > 0) { + BaseStatusSignal.refreshAll(phoenixSignals); + } + } + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + signalsLock.unlock(); + } + + odometryLock.lock(); + try { + double timestamp = RobotController.getFPGATime() / 1e6; + double totalLatency = 0.0; + for (BaseStatusSignal signal : phoenixSignals) { + totalLatency += signal.getTimestamp().getLatency(); + } + if (phoenixSignals.length > 0) { + timestamp -= totalLatency / phoenixSignals.length; + } + + for (int i = 0; i < phoenixSignals.length; i++) { + phoenixQueues.get(i).offer(phoenixSignals[i].getValueAsDouble()); + } + for (int i = 0; i < genericSignals.size(); i++) { + genericQueues.get(i).offer(genericSignals.get(i).getAsDouble()); + } + for (int i = 0; i < timestampQueues.size(); i++) { + timestampQueues.get(i).offer(timestamp); + } + } finally { + odometryLock.unlock(); + } + } + } + +} diff --git a/src/main/java/frc/robot/subsystems/swerve/util/SwerveArcOdometry.java b/src/main/java/frc/robot/subsystems/swerve/util/SwerveArcOdometry.java new file mode 100644 index 0000000..c3e85e6 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/swerve/util/SwerveArcOdometry.java @@ -0,0 +1,190 @@ +package frc.robot.subsystems.swerve.util; + +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Transform2d; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.kinematics.SwerveDriveKinematics; +import edu.wpi.first.math.kinematics.SwerveDriveOdometry; +import edu.wpi.first.math.kinematics.SwerveModulePosition; + +/** + * Swerve drive odometry implementation that integrates wheel motion using circular arc geometry + * rather than straight-line approximations. + * + *

+ * Inspired by Team 1690, this odometry model treats each swerve module's motion between updates as + * an arc defined by the change in wheel angle and distance traveled. This more accurately + * represents real swerve motion, especially during simultaneous translation and rotation, where + * straight-line assumptions can introduce measurable integration error. + * + *

+ * Compared to {@link SwerveDriveOdometry}, which assumes each module travels in a straight line + * over a timestep, this class: + *

+ * + *

+ * The robot heading is still sourced directly from the gyro, while translation is derived purely + * from module motion. + * + *

+ * This class is intended as a drop-in replacement for {@link SwerveDriveOdometry} when improved + * accuracy is desired at the cost of slightly increased computation. + */ +public final class SwerveArcOdometry extends SwerveDriveOdometry { + private final int numberOfModules; + private final Translation2d[] robotRelativeModuleOffsets; + + private Pose2d robotPose = Pose2d.kZero; + private final SwerveModulePosition[] previousWheelPositions; + + /** + * Constructs a new arc-based swerve odometry instance. + * + * @param kinematics the swerve drive kinematics describing module locations + * @param gyroAngle the initial robot heading from the gyro + * @param modulePositions the initial positions of each swerve module + */ + public SwerveArcOdometry(SwerveDriveKinematics kinematics, Rotation2d gyroAngle, + SwerveModulePosition[] modulePositions) { + super(kinematics, gyroAngle, modulePositions); + numberOfModules = kinematics.getModules().length; + robotRelativeModuleOffsets = kinematics.getModules(); + + previousWheelPositions = new SwerveModulePosition[numberOfModules]; + for (int i = 0; i < numberOfModules; i++) { + previousWheelPositions[i] = new SwerveModulePosition(); + } + } + + /** + * Computes the 2D displacement of a swerve module by integrating its motion along a circular + * arc. + * + *

+ * The arc is defined by the change in module steering angle and the change in wheel distance + * between two samples. If the steering angle does not change, the motion is treated as a + * straight line for efficiency. + * + *

+ * This calculation assumes the module follows a constant-curvature path over the timestep, + * which more closely matches real swerve behavior during rotation. + * + * @param previousWheelPosition the module position at the previous update + * @param currentWheelPosition the module position at the current update + * @return the module's displacement in the robot-relative frame + */ + private static Translation2d getModuleDisplacement(SwerveModulePosition previousWheelPosition, + SwerveModulePosition currentWheelPosition) { + // First, calculate difference between previous and current angles and distances + double angleDifferenceRadians = + currentWheelPosition.angle.getRadians() - previousWheelPosition.angle.getRadians(); + double arcLength = + currentWheelPosition.distanceMeters - previousWheelPosition.distanceMeters; + + // *If angle difference is 0 then we can just use a straight line instead of an arc + if (angleDifferenceRadians == 0) { + return new Translation2d(arcLength, currentWheelPosition.angle); + } + + // Next, calculate radius. Positive = left turn, negative = right turn + double radius = (arcLength / angleDifferenceRadians); + + // Then, calculate the center point of the circle that the arc is a part of, using the + // previous + // angle. The previous module translation is (0, 0) because we don't care where it starts, + // only + // the displacement. It is also always perpendicular to the previous angle, with + // positive/negative radius indicating which side of the module that the circle center will + // be + // on + double circleCenterX = -radius * previousWheelPosition.angle.getSin(); + double circleCenterY = radius * previousWheelPosition.angle.getCos(); + + // Finally, calculate the current module translation on the arc and return it as module + // displacement + double displacementX = circleCenterX + radius * currentWheelPosition.angle.getSin(); + double displacementY = circleCenterY - radius * currentWheelPosition.angle.getCos(); + + return new Translation2d(displacementX, displacementY); + } + + @Override + public Pose2d update(Rotation2d currentGyroAngle, + SwerveModulePosition[] currentWheelPositions) { + // First, get the field relative module poses of the previous robot pose, and apply robot + // relative module offsets + Pose2d[] fieldRelativeModulePosesOfPreviousPose = new Pose2d[numberOfModules]; + for (int i = 0; i < numberOfModules; i++) { + fieldRelativeModulePosesOfPreviousPose[i] = robotPose + .transformBy(new Transform2d(robotRelativeModuleOffsets[i], Rotation2d.kZero)); + } + + // Also get the module displacements from the previous wheel positions to the current wheel + // positions + Translation2d[] moduleDisplacements = new Translation2d[numberOfModules]; + for (int i = 0; i < numberOfModules; i++) { + moduleDisplacements[i] = + getModuleDisplacement(previousWheelPositions[i], currentWheelPositions[i]); + } + + // Next, add the module displacements to the field relative module poses + Translation2d[] fieldRelativeModuleDisplacements = new Translation2d[numberOfModules]; + for (int i = 0; i < numberOfModules; i++) { + fieldRelativeModuleDisplacements[i] = fieldRelativeModulePosesOfPreviousPose[i] + .transformBy(new Transform2d(moduleDisplacements[i], Rotation2d.kZero)) + .getTranslation(); + } + + // Finally, average the module displacements and return the new pose + Translation2d sumOfFieldRelativeModuleDisplacements = new Translation2d(); + for (int i = 0; i < numberOfModules; i++) { + sumOfFieldRelativeModuleDisplacements = + sumOfFieldRelativeModuleDisplacements.plus(fieldRelativeModuleDisplacements[i]); + } + double updatedPoseX = sumOfFieldRelativeModuleDisplacements.getX() / numberOfModules; + double updatedPoseY = sumOfFieldRelativeModuleDisplacements.getY() / numberOfModules; + var updatedPose = new Pose2d(updatedPoseX, updatedPoseY, currentGyroAngle); + + // After calculations, but before the next loop, update the previous pose & wheel positions + // to + // the current ones + robotPose = updatedPose; + for (int i = 0; i < numberOfModules; i++) { + previousWheelPositions[i] = currentWheelPositions[i]; + } + + return updatedPose; + } + + @Override + public void resetPosition(Rotation2d gyroAngle, SwerveModulePosition[] wheelPositions, + Pose2d poseMeters) { + robotPose = new Pose2d(poseMeters.getTranslation(), gyroAngle); + } + + @Override + public void resetPose(Pose2d poseMeters) { + robotPose = poseMeters; + } + + @Override + public void resetTranslation(Translation2d translation) { + robotPose = new Pose2d(translation, robotPose.getRotation()); + } + + @Override + public void resetRotation(Rotation2d rotation) { + robotPose = new Pose2d(robotPose.getTranslation(), rotation); + } + + @Override + public Pose2d getPoseMeters() { + return robotPose; + } + +} diff --git a/src/main/java/frc/robot/subsystems/swerve/util/SwerveRateLimiter.java b/src/main/java/frc/robot/subsystems/swerve/util/SwerveRateLimiter.java new file mode 100644 index 0000000..540a1b0 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/swerve/util/SwerveRateLimiter.java @@ -0,0 +1,224 @@ +package frc.robot.subsystems.swerve.util; + +import java.util.EnumSet; +import org.ejml.data.DMatrix3; +import org.ejml.dense.fixed.CommonOps_DDF3; +import org.jspecify.annotations.NullMarked; +import org.littletonrobotics.junction.Logger; +import edu.wpi.first.math.geometry.Twist2d; +import edu.wpi.first.math.kinematics.ChassisSpeeds; +import edu.wpi.first.networktables.NetworkTableEvent.Kind; +import edu.wpi.first.networktables.NetworkTableInstance; +import frc.robot.Constants; + +/** + * Applies rate limiting to robot-relative swerve chassis commands to ensure physically achievable, + * stable, and predictable motion. + * + *

+ * This limiter constrains translational acceleration using a multi-stage process inspired by Team + * 1690's swerve control methodology: + *

+ * + *

+ * The limiter operates in discrete time (20 ms control loop) and should be called once per + * cycle. The current robot velocity must be provided via {@link #update(ChassisSpeeds)} before + * calling {@link #limit(ChassisSpeeds)}. + * + *

+ * All limits are exposed via NetworkTables under {@code /SwerveRateLimiter/*} and may be tuned at + * runtime. Diagnostic values are published to AdvantageKit logs for visualization and debugging. + * + *

+ * Rotational velocity ({@code omegaRadiansPerSecond}) is passed through unchanged and is not + * subject to rate limiting. + * + *

+ * Usage pattern: + * + *

{@code
+ * limiter.update(currentRobotRelativeSpeeds);
+ * ChassisSpeeds safeSpeeds = limiter.limit(desiredSpeeds);
+ * }
+ * + *

+ * Based on concepts presented by FRC Team 1690: + * + * https://www.youtube.com/watch?v=vUtVXz7ebEE + */ +@NullMarked +public class SwerveRateLimiter { + + private double forwardLimit = Constants.Swerve.forwardLimit; + private double forwardTiltLimit = Constants.Swerve.forwardTiltLimit; + private double leftTiltLimit = Constants.Swerve.leftTiltLimit; + private double rightTiltLimit = Constants.Swerve.rightTiltLimit; + private double backTiltLimit = Constants.Swerve.backTiltLimit; + private double skidLimit = Constants.Swerve.skidLimit; + + /** + * Creates a new {@code SwerveRateLimiter} and publishes all acceleration limits to + * NetworkTables for live tuning. + * + *

+ * Any updates received via NetworkTables will immediately modify the corresponding limit used + * by the rate limiter. + */ + public SwerveRateLimiter() { + final NetworkTableInstance ntInstance = NetworkTableInstance.getDefault(); + try { + for (var item : SwerveRateLimiter.class.getDeclaredFields()) { + if (item.getType().equals(double.class)) { + var topic = ntInstance.getDoubleTopic("/SwerveRateLimiter/" + item.getName()); + var publisher = topic.publish(); + var value = (double) item.get(this); + publisher.accept(value); + ntInstance.addListener(topic, EnumSet.of(Kind.kValueAll), (ev) -> { + try { + var newValue = ev.valueData.value.getDouble(); + item.set(this, newValue); + } catch (IllegalArgumentException | IllegalAccessException e) { + e.printStackTrace(); + } + }); + } + } + } catch (IllegalArgumentException | IllegalAccessException e) { + e.printStackTrace(); + } + } + + /** + * Updates the current robot-relative chassis velocity used as the basis for acceleration + * limiting. + * + *

+ * This method should be called once per control loop using velocity data from odometry or state + * estimation before calling {@link #limit(ChassisSpeeds)}. + * + * @param robotRelative the current robot-relative chassis speeds + */ + public void update(ChassisSpeeds robotRelative) { + currentVel.a1 = robotRelative.vxMetersPerSecond; + currentVel.a2 = robotRelative.vyMetersPerSecond; + currentVel.a3 = robotRelative.omegaRadiansPerSecond; + } + + private final DMatrix3 currentVel = new DMatrix3(); + private final DMatrix3 wantedVel = new DMatrix3(); + private final DMatrix3 wantedAcc = new DMatrix3(); + + private void publish(String name, DMatrix3 v) { + Logger.recordOutput("SwerveRateLimiter/" + name, new Twist2d(v.a1, v.a2, v.a3)); + } + + private void publish(String name, double v) { + Logger.recordOutput("SwerveRateLimiter/" + name, v); + } + + /** + * Limits a desired robot-relative chassis velocity based on physical acceleration, tilt, and + * skid constraints, producing a safe and achievable {@link ChassisSpeeds}. + * + *

+ * The limiting process operates in discrete time (20 ms) and applies several sequential + * constraints: + *

    + *
  1. Physical acceleration limit: Caps acceleration in the direction of current motion + * based on available traction and remaining headroom to max speed.
  2. + *
  3. Tilt prevention: Restricts directional acceleration components to prevent + * excessive forward, backward, or lateral tilting.
  4. + *
  5. Skid prevention: Limits the overall translational acceleration magnitude to avoid + * wheel slip.
  6. + *
+ * + *

+ * The resulting acceleration is integrated over one control loop period and added to the + * current velocity to produce the final commanded chassis speeds. + * + *

+ * Rotational velocity ({@code omegaRadiansPerSecond}) is passed through unchanged and is not + * subject to acceleration limiting. + * + * @param wantedSpeedsRobotRelative the desired robot-relative chassis speeds (vx, vy, omega) + * @return a new {@link ChassisSpeeds} representing the limited, physically achievable + * robot-relative velocities for the next control step + */ + public ChassisSpeeds limit(ChassisSpeeds wantedSpeedsRobotRelative) { + double currentSpeed = Math.hypot(currentVel.a1, currentVel.a2); + double wantedSpeed = Math.hypot(wantedSpeedsRobotRelative.vxMetersPerSecond, + wantedSpeedsRobotRelative.vyMetersPerSecond); + + publish("currentSpeed", currentSpeed); + publish("wantedSpeed", wantedSpeed); + + wantedVel.a1 = wantedSpeedsRobotRelative.vxMetersPerSecond; + wantedVel.a2 = wantedSpeedsRobotRelative.vyMetersPerSecond; + wantedVel.a3 = wantedSpeedsRobotRelative.omegaRadiansPerSecond; + publish("wantedVel", wantedVel); + + CommonOps_DDF3.subtract(wantedVel, currentVel, wantedAcc); + CommonOps_DDF3.divide(wantedAcc, 0.02); + publish("wantedAccStep0", wantedAcc); + + // Step 1: Robot cannot accelerate indefinitely. Ensure accelerations are achievable + // (sub-max to ensure correctness even under degradation). + double subphysicalAccelerationLimit = + Math.max(1.0 - (currentSpeed / Constants.Swerve.maxSpeed), 0.0); + publish("subphysicalAccelerationLimit", subphysicalAccelerationLimit); + double maxForwardAccel = forwardLimit * subphysicalAccelerationLimit; + publish("maxForwardAccel", maxForwardAccel); + + // get acceleration in direction of current velocity + double wantedAccMagnitude = wantedAcc.a1 * currentVel.a1 / currentSpeed + + wantedAcc.a2 * currentVel.a2 / currentSpeed; + publish("wantedAccMagnitudeStep1", wantedAccMagnitude); + if (wantedAccMagnitude > maxForwardAccel) { + double mul = maxForwardAccel / wantedAccMagnitude; + wantedAcc.a1 *= mul; + wantedAcc.a2 *= mul; + } + publish("wantedAccStep1", wantedAcc); + + // Step 2: Robot may accelerate too fast and result in tilting. Limit directional + // acceleration to prevent this. + if (wantedAcc.a1 > forwardTiltLimit) { + wantedAcc.a1 = forwardLimit; + } + if (wantedAcc.a1 < -backTiltLimit) { + wantedAcc.a1 = -backTiltLimit; + } + if (wantedAcc.a2 > leftTiltLimit) { + wantedAcc.a2 = leftTiltLimit; + } + if (wantedAcc.a2 < -rightTiltLimit) { + wantedAcc.a2 = -rightTiltLimit; + } + publish("wantedAccStep2", wantedAcc); + + // Step 3: Robot accelerating in a different direction may result in skidding. Limit + // magnitude of acceleration to prevent this. + wantedAccMagnitude = Math.hypot(wantedAcc.a1, wantedAcc.a2); + publish("wantedAccMagnitudeStep3", wantedAccMagnitude); + if (wantedAccMagnitude > skidLimit) { + double multiplier = skidLimit / wantedAccMagnitude; + wantedAcc.a1 *= multiplier; + wantedAcc.a2 *= multiplier; + } + publish("wantedAccStep3", wantedAcc); + + CommonOps_DDF3.scale(0.02, wantedAcc); + CommonOps_DDF3.add(currentVel, wantedAcc, wantedVel); + + publish("resultant", wantedVel); + publish("resultantSpeed", Math.hypot(wantedVel.a1, wantedVel.a2)); + + return new ChassisSpeeds(wantedVel.a1, wantedVel.a2, wantedVel.a3); + } + +} diff --git a/src/main/java/frc/robot/subsystems/swerve/util/SwerveState.java b/src/main/java/frc/robot/subsystems/swerve/util/SwerveState.java new file mode 100644 index 0000000..d3a94cb --- /dev/null +++ b/src/main/java/frc/robot/subsystems/swerve/util/SwerveState.java @@ -0,0 +1,320 @@ +package frc.robot.subsystems.swerve.util; + +import static edu.wpi.first.units.Units.Degrees; +import static edu.wpi.first.units.Units.Radians; +import static edu.wpi.first.units.Units.Seconds; +import java.util.List; +import java.util.Optional; +import org.jspecify.annotations.NullMarked; +import org.littletonrobotics.junction.Logger; +import org.photonvision.targeting.PhotonPipelineResult; +import org.photonvision.targeting.PhotonTrackedTarget; +import edu.wpi.first.math.MathUtil; +import edu.wpi.first.math.VecBuilder; +import edu.wpi.first.math.estimator.PoseEstimator; +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.Transform3d; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.math.interpolation.TimeInterpolatableBuffer; +import edu.wpi.first.math.kinematics.ChassisSpeeds; +import edu.wpi.first.math.kinematics.SwerveDriveOdometry; +import edu.wpi.first.math.kinematics.SwerveModulePosition; +import edu.wpi.first.math.util.Units; +import frc.robot.Constants; +import frc.robot.subsystems.swerve.Swerve; +import frc.robot.subsystems.vision.CameraConstants; + +/** + * Maintains and updates the robot's estimated global pose for a swerve drive by fusing wheel + * odometry, gyro measurements, and delayed vision updates. + * + *

+ * This class tracks two related poses: + *

+ * + *

+ * A time-interpolated pose buffer is maintained to allow vision measurements with latency to be + * applied retroactively at the correct timestamp. + * + *

+ * This implementation resembles a lightweight pose estimator rather than a full Kalman filter, + * using configurable standard deviations to weight vision updates against odometry uncertainty. + */ +@NullMarked +public class SwerveState { + + /** Whether the pose estimator has been initialized from vision */ + private boolean initted = false; + + private final PoseEstimator visionAdjustedOdometry; + + private final TimeInterpolatableBuffer rotationBuffer = + TimeInterpolatableBuffer.createBuffer(1.5); + + /** + * Creates a new swerve state estimator. + * + * @param wheelPositions the initial swerve module positions used to seed odometry + */ + public SwerveState(SwerveModulePosition[] wheelPositions, Rotation2d gyroYaw) { + SwerveDriveOdometry swerveOdometry = + new SwerveArcOdometry(Constants.Swerve.swerveKinematics, gyroYaw, wheelPositions); + visionAdjustedOdometry = new PoseEstimator<>(Constants.Swerve.swerveKinematics, + swerveOdometry, VecBuilder.fill(0.1, 0.1, 0.1), VecBuilder.fill(0.9, 0.9, 0.9)); + } + + /** + * Resets the internal pose estimate to a known field pose. + * + *

+ * This method forces the underlying swerve odometry to the specified pose, effectively + * redefining the robot's position on the field. It should be used when the robot pose is known + * with high confidence, such as: + *

+ * + *

+ * This method updates only the pose estimator / odometry state owned by {@code SwerveState}. It + * does not update any associated simulation state or drivetrain model. + * + *

+ * Most code should prefer {@link Swerve#overridePose} when resetting the robot pose, as that + * method ensures both the estimator and any simulated drivetrain pose remain consistent. + * + *

+ * Future odometry and vision updates will be applied relative to this new pose. + * + * @param pose the desired robot pose in field coordinates + */ + public void resetPose(Pose2d pose) { + this.visionAdjustedOdometry.resetPose(pose); + rotationBuffer.clear(); + rotationBuffer.getInternalBuffer().clear(); + } + + private Optional sampleRotationAt(double timestampSeconds) { + if (rotationBuffer.getInternalBuffer().isEmpty()) { + return Optional.empty(); + } + + double oldestOdometryTimestamp = rotationBuffer.getInternalBuffer().firstKey(); + double newestOdometryTimestamp = rotationBuffer.getInternalBuffer().lastKey(); + if (oldestOdometryTimestamp > timestampSeconds) { + return Optional.empty(); + } + timestampSeconds = + MathUtil.clamp(timestampSeconds, oldestOdometryTimestamp, newestOdometryTimestamp); + + return rotationBuffer.getSample(timestampSeconds); + } + + /** + * Updates odometry and pose estimates using swerve module encoders and an optional gyro + * measurement. + * + * @param wheelPositions current swerve module positions + * @param gyroYaw current robot yaw, if available + * @param timestamp measurement timestamp in seconds + */ + public void addOdometryObservation(SwerveModulePosition[] wheelPositions, Rotation2d gyroYaw, + double timestamp) { + rotationBuffer.addSample(timestamp, gyroYaw); + visionAdjustedOdometry.update(gyroYaw, wheelPositions); + } + + private ChassisSpeeds currentSpeeds; + + /** + * Updates the robot's current chassis speeds. + * + *

+ * This method records the most recent robot-relative {@link ChassisSpeeds} command or measured + * velocity. The stored speeds may be used by future pose estimation or vision fusion logic to + * account for motion during sensor latency or to improve prediction accuracy. + * + *

+ * This method does not directly affect the current pose estimate. + * + * @param speeds the current robot-relative chassis speeds + */ + public void updateSpeeds(ChassisSpeeds speeds) { + currentSpeeds = speeds; + } + + /** + * Forcibly initializes the pose estimator using a known robot pose. + * + *

+ * This method sets the internal odometry/estimator state directly and marks the estimator as + * initialized, bypassing the normal vision-based initialization path. It is intended for + * situations where a trusted global pose is already known (for example, at the start of + * autonomous) or when a vision needs to be tested on a non-compliant field (for example, the + * practice field at district events). + * + *

+ * After this method is called, subsequent vision updates will be treated as corrections rather + * than initialization measurements. + * + * @param pose the known robot pose in field coordinates to initialize the estimator with + */ + public void overrideInit(Pose2d pose) { + visionAdjustedOdometry.resetPose(pose); + initted = true; + } + + /** + * Adds a vision measurement using an externally computed camera pose. + * + * @param cameraPose estimated camera pose in field coordinates + * @param robotToCamera transform from robot to camera frame + * @param translationStdDev translation measurement standard deviation (meters) + * @param rotationStdDev rotation measurement standard deviation (radians) + * @param timestamp measurement timestamp in seconds + */ + public void addVisionObservation(Pose3d cameraPose, Transform3d robotToCamera, + double translationStdDev, double rotationStdDev, double timestamp) { + Pose2d robotPose = cameraPose.plus(robotToCamera.inverse()).toPose2d(); + Pose2d before = visionAdjustedOdometry.getEstimatedPosition(); + visionAdjustedOdometry.addVisionMeasurement(robotPose, timestamp, + VecBuilder.fill(translationStdDev, translationStdDev, rotationStdDev)); + Pose2d after = visionAdjustedOdometry.getEstimatedPosition(); + double correction = after.getTranslation().getDistance(before.getTranslation()); + Logger.recordOutput("State/Correction", correction); + Logger.recordOutput("State/VisionRobotPose", robotPose); + } + + /** + * Adds a vision measurement from PhotonVision. + * + *

+ * If the estimator has not yet been initialized, a valid multi-tag estimate will be used to + * seed the global pose. + * + * @param camera camera configuration constants + * @param pipelineResult latest PhotonVision pipeline result + */ + public boolean addVisionObservation(CameraConstants camera, + PhotonPipelineResult pipelineResult) { + var multiTag = pipelineResult.getMultiTagResult(); + if (!initted) { + multiTag.ifPresent(multiTag_ -> { + Transform3d best = multiTag_.estimatedPose.best; + Pose3d cameraPose = + new Pose3d().plus(best).relativeTo(Constants.Vision.fieldLayout.getOrigin()); + Pose3d robotPose = cameraPose.plus(camera.robotToCamera.inverse()); + visionAdjustedOdometry.resetPose(robotPose.toPose2d()); + initted = true; + }); + return initted; + } else { + double translationSpeed = + Math.hypot(currentSpeeds.vxMetersPerSecond, currentSpeeds.vyMetersPerSecond); + double rotationSpeed = Math.abs(currentSpeeds.omegaRadiansPerSecond); + double velocityStdDev = camera.simLatencyStdDev.in(Seconds); + double velocityTranslationError = translationSpeed * velocityStdDev; + double velocityRotationError = rotationSpeed * velocityStdDev; + Logger.recordOutput("State/velocityTranslationError", velocityTranslationError); + Logger.recordOutput("State/velocityRotationError", velocityRotationError); + if (multiTag.isPresent()) { + // Multi Tag + Transform3d best = multiTag.get().estimatedPose.best; + Pose3d cameraPose = + new Pose3d().plus(best).relativeTo(Constants.Vision.fieldLayout.getOrigin()); + double stdDevMultiplier = stdDevMultiplier(pipelineResult.targets, cameraPose); + double translationStdDev = + stdDevMultiplier * velocityTranslationError + camera.translationError; + double rotationStdDev = + stdDevMultiplier * velocityRotationError + camera.rotationError; + Logger.recordOutput("State/stdDevMultipler", stdDevMultiplier); + Logger.recordOutput("State/stdDevTranslation", translationStdDev); + Logger.recordOutput("State/stdDevRotation", rotationStdDev); + addVisionObservation(cameraPose, camera.robotToCamera, translationStdDev, + rotationStdDev, pipelineResult.getTimestampSeconds()); + return true; + } else if (rotationSpeed < Units.degreesToRadians(3)) { + // Single Tag + PhotonTrackedTarget target = pipelineResult.getBestTarget(); + if (target == null) { + return false; + } + Optional yawSample = + sampleRotationAt(pipelineResult.getTimestampSeconds()); + if (!yawSample.isPresent()) { + return false; + } + Optional maybePose = + Constants.Vision.fieldLayout.getTagPose(target.getFiducialId()); + if (!maybePose.isPresent()) { + return false; + } + double distance = target.getBestCameraToTarget().getTranslation().getNorm(); + Rotation3d targetInCameraFrame = new Rotation3d(Radians.of(0.0), + Degrees.of(-target.getPitch()), Degrees.of(-target.getYaw())); + Rotation3d cameraRotationInWorldFrame = + camera.robotToCamera.getRotation().rotateBy(new Rotation3d(yawSample.get())); + Translation3d debugTranslation = + new Pose3d(getGlobalPoseEstimate()).plus(camera.robotToCamera).getTranslation(); + Logger.recordOutput("State/singleTagCameraRotationInWorldFrame", + new Pose3d(debugTranslation, cameraRotationInWorldFrame)); + Rotation3d targetRotationInWorldFrame = + targetInCameraFrame.plus(cameraRotationInWorldFrame); + Logger.recordOutput("State/singleTagTargetRotationInWorldFrame", + new Pose3d(debugTranslation, targetRotationInWorldFrame)); + Translation3d cameraToTargetInWorldFrame = + new Translation3d(distance, targetRotationInWorldFrame); + Logger.recordOutput("State/singleTagTargetVector", new Translation3d[] { + debugTranslation, debugTranslation.plus(cameraToTargetInWorldFrame)}); + Translation2d cameraPosition = maybePose.get().getTranslation() + .minus(cameraToTargetInWorldFrame).toTranslation2d(); + Pose3d cameraPose = new Pose3d(cameraPosition.getX(), cameraPosition.getY(), + camera.robotToCamera.getZ(), cameraRotationInWorldFrame); + Logger.recordOutput("State/singleTagCameraPose", cameraPose); + double stdDevMultiplier = stdDevMultiplier(pipelineResult.targets, cameraPose); + double translationStdDev = + stdDevMultiplier * camera.singleTagError + velocityTranslationError; + Logger.recordOutput("State/stdDevMultipler", stdDevMultiplier); + Logger.recordOutput("State/stdDevTranslation", translationStdDev); + addVisionObservation(cameraPose, camera.robotToCamera, translationStdDev, + Double.POSITIVE_INFINITY, pipelineResult.getTimestampSeconds()); + return true; + } + } + return false; + } + + private static double stdDevMultiplier(List targets, Pose3d cameraPose) { + double totalDistance = 0.0; + int count = 0; + for (var tag : targets) { + var maybeTagPose = Constants.Vision.fieldLayout.getTagPose(tag.getFiducialId()); + if (maybeTagPose.isPresent()) { + var tagPose = maybeTagPose.get(); + totalDistance += tagPose.getTranslation().getDistance(cameraPose.getTranslation()); + count++; + } + } + double avgDistance = totalDistance / count; + double stddev = Math.pow(avgDistance, 2.0) / count; + return stddev; + } + + /** + * Returns the current best estimate of the robot's global field pose. + * + * @return estimated robot pose in field coordinates + */ + public Pose2d getGlobalPoseEstimate() { + return visionAdjustedOdometry.getEstimatedPosition(); + } + +} diff --git a/src/main/java/frc/robot/subsystems/swerve/util/TeleopControls.java b/src/main/java/frc/robot/subsystems/swerve/util/TeleopControls.java new file mode 100644 index 0000000..cc42e61 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/swerve/util/TeleopControls.java @@ -0,0 +1,71 @@ +package frc.robot.subsystems.swerve.util; + +import java.util.function.DoubleSupplier; +import java.util.function.Supplier; +import org.jspecify.annotations.NullMarked; +import edu.wpi.first.math.MathUtil; +import edu.wpi.first.math.kinematics.ChassisSpeeds; +import frc.robot.Constants; + +/** + * Control scheme utilities for teleoperated swerve driving. + * + *

+ * This class converts raw driver input signals (typically joystick axes) into {@link ChassisSpeeds} + * suitable for commanding a swerve drivetrain. Input processing includes deadbanding, non-linear + * shaping, and scaling to configured maximum translational and rotational speeds. + * + *

+ * The resulting chassis speeds are intended to be consumed by higher-level drive commands + * (robot-relative or field-relative) rather than applied directly to hardware. + */ +@NullMarked +public class TeleopControls { + + /** + * Creates a supplier that converts driver inputs into desired chassis speeds. + * + *

+ * The returned {@link Supplier} performs the following processing each time it is evaluated: + *

+ * + *

+ * The produced {@link ChassisSpeeds} are expressed in a pseudo-field-relative frame, + * where: + *

+ * + *

+ * This method does not perform true field-relative conversion; callers are expected to apply + * any required frame transformations using the robot's current heading. + * + * @param forward supplier providing the forward/backward driver input + * @param right supplier providing the left/right driver input + * @param turnCCW supplier providing the counterclockwise rotation input + * @return a supplier that generates processed {@link ChassisSpeeds} for teleop control + */ + public static Supplier teleopControls(DoubleSupplier forward, + DoubleSupplier right, DoubleSupplier turnCCW) { + return () -> { + double xaxis = right.getAsDouble(); + double yaxis = forward.getAsDouble(); + double raxis = turnCCW.getAsDouble(); + yaxis = MathUtil.applyDeadband(yaxis, Constants.DriverControls.stickDeadband); + xaxis = MathUtil.applyDeadband(xaxis, Constants.DriverControls.stickDeadband); + xaxis *= xaxis * Math.signum(xaxis); + yaxis *= yaxis * Math.signum(yaxis); + raxis = (Math.abs(raxis) < Constants.DriverControls.stickDeadband) ? 0 : raxis; + return new ChassisSpeeds(yaxis * Constants.DriverControls.driverTranslationalMaxSpeed, + xaxis * Constants.DriverControls.driverTranslationalMaxSpeed, + raxis * Constants.DriverControls.driverRotationalMaxSpeed); + }; + } + +} diff --git a/src/main/java/frc/robot/subsystems/swerve/util/TuningCommands.java b/src/main/java/frc/robot/subsystems/swerve/util/TuningCommands.java new file mode 100644 index 0000000..aaeb89d --- /dev/null +++ b/src/main/java/frc/robot/subsystems/swerve/util/TuningCommands.java @@ -0,0 +1,221 @@ +package frc.robot.subsystems.swerve.util; + +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.DoubleConsumer; +import java.util.function.DoubleSupplier; +import java.util.function.Supplier; +import org.littletonrobotics.junction.Logger; +import edu.wpi.first.math.filter.SlewRateLimiter; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.kinematics.ChassisSpeeds; +import edu.wpi.first.wpilibj.Timer; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; +import frc.robot.Constants; +import frc.robot.subsystems.swerve.Swerve; + +/** + * Collection of command factories used for drivetrain tuning and characterization. + * + *

+ * This class provides reusable command sequences for empirically determining drivetrain parameters + * such as feedforward gains and effective wheel radius. These commands are intended to be run + * manually during calibration and development, not during normal match operation. + * + *

+ * This class is non-instantiable and only contains static factory methods. + */ +public final class TuningCommands { + + private TuningCommands() {} + + private static final double ffStartDelay = 2.0; + private static final double ffRampRate = 0.1; + private static final double wheelRadiusMaxVelocity = 0.25; + private static final double wheelRadiusRampRate = 0.05; + + /** + * Creates a command to characterize drivetrain feedforward constants. + * + *

+ * This routine performs a quasi-static ramp test by slowly increasing applied voltage while + * measuring the resulting drivetrain velocity. The collected data is fit using linear + * regression to estimate: + * + *

+ * + *

+ * The characterization process consists of: + *

    + *
  1. Orient all modules to face forward
  2. + *
  3. Holding zero output to allow modules to fully orient
  4. + *
  5. Linearly ramping voltage at a fixed rate
  6. + *
  7. Recording velocity and voltage samples each loop
  8. + *
+ * + *

+ * When the command is cancelled, the collected samples are fit and the resulting constants are + * printed to the console and logged. + * + *

+ * This routine should be run on flat carpet with minimal disturbances. + * + * @param swerve the swerve subsystem being characterized + * @param runCharacterization consumer that applies a raw voltage command to the drivetrain + * @param getFFCharacterizationVelocity supplier that returns the current drivetrain velocity + * for sampling + * @return a command that performs feedforward characterization + */ + public static Command feedforwardCharacterization(Swerve swerve, + DoubleConsumer runCharacterization, DoubleSupplier getFFCharacterizationVelocity) { + List velocitySamples = new LinkedList<>(); + List voltageSamples = new LinkedList<>(); + Timer timer = new Timer(); + + return Commands.sequence( + // Reset data + Commands.runOnce(() -> { + velocitySamples.clear(); + voltageSamples.clear(); + }), + + // Allow modules to orient + Commands.run(() -> runCharacterization.accept(0.0), swerve).withTimeout(ffStartDelay), + + // Start timer + Commands.runOnce(timer::restart), + + // Accelerate and gather data + Commands.run(() -> { + double voltage = timer.get() * ffRampRate; + runCharacterization.accept(voltage); + velocitySamples.add(getFFCharacterizationVelocity.getAsDouble()); + voltageSamples.add(voltage); + }, swerve) + + // When cancelled, calculate and print results + .finallyDo(() -> { + int n = velocitySamples.size(); + double sumX = 0.0; + double sumY = 0.0; + double sumXY = 0.0; + double sumX2 = 0.0; + for (int i = 0; i < n; i++) { + sumX += velocitySamples.get(i); + sumY += voltageSamples.get(i); + sumXY += velocitySamples.get(i) * voltageSamples.get(i); + sumX2 += velocitySamples.get(i) * velocitySamples.get(i); + } + double kS = (sumY * sumX2 - sumX * sumXY) / (n * sumX2 - sumX * sumX); + double kV = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX); + + NumberFormat formatter = new DecimalFormat("#0.00000"); + System.out.println("********** Drive FF Characterization Results **********"); + System.out.println("\tkS: " + formatter.format(kS)); + System.out.println("\tkV: " + formatter.format(kV)); + Logger.recordOutput("Sysid/FF/kS", kS); + Logger.recordOutput("Sysid/FF/kV", kV); + })); + } + + /** + * Creates a command to characterize the effective wheel radius of the drivetrain. + * + *

+ * This routine estimates wheel radius by rotating the robot in place and comparing the + * integrated gyro rotation against the average wheel travel. The wheel radius is computed using + * the relationship: + * + *

+     * wheelRadius = (robotRotation * driveBaseRadius) / wheelTravel
+     * 
+ * + *

+ * The command runs two sequences in parallel: + *

+ * + *

+ * Intermediate values and the computed wheel radius are logged continuously for validation and + * offline analysis. + * + *

+ * This routine assumes: + *

+ * + * @param swerve the swerve subsystem being characterized + * @param setModuleStates consumer used to command rotational chassis speeds + * @param getWheelRadiusCharacterizationPositions supplier of wheel position measurements + * @param gyroYaw supplier of the current robot yaw angle + * @return a command that performs wheel radius characterization + */ + public static Command wheelRadiusCharacterization(Swerve swerve, + Consumer setModuleStates, + Supplier getWheelRadiusCharacterizationPositions, Supplier gyroYaw) { + SlewRateLimiter limiter = new SlewRateLimiter(wheelRadiusRampRate); + WheelRadiusCharacterizationState state = new WheelRadiusCharacterizationState(); + + final double driveBaseRadius = + Math.hypot(Constants.Swerve.trackWidth / 2.0, Constants.Swerve.wheelBase / 2.0); + + return Commands.parallel( + // Drive control sequence + Commands.sequence( + // Reset acceleration limiter + Commands.runOnce(() -> limiter.reset(0.0)), + + // Turn in place, accelerating up to full speed + Commands.run(() -> { + double speed = limiter.calculate(wheelRadiusMaxVelocity); + setModuleStates.accept(new ChassisSpeeds(0.0, 0.0, speed)); + }, swerve)), + + // Measurement sequence + Commands.sequence( + // Wait for modules to fully orient before starting measurement + Commands.waitSeconds(1.0), + + // Record starting measurement + Commands.runOnce(() -> { + state.positions = getWheelRadiusCharacterizationPositions.get(); + state.lastAngle = gyroYaw.get(); + state.gyroDelta = 0.0; + }), + + // Update gyro delta + Commands.run(() -> { + var rotation = gyroYaw.get(); + state.gyroDelta += Math.abs(rotation.minus(state.lastAngle).getRadians()); + state.lastAngle = rotation; + + double[] positions = getWheelRadiusCharacterizationPositions.get(); + double wheelDelta = 0.0; + for (int i = 0; i < 4; i++) { + wheelDelta += Math.abs(positions[i] - state.positions[i]) / 4.0; + } + double wheelRadius = (state.gyroDelta * driveBaseRadius) / wheelDelta; + + Logger.recordOutput("Drive/WheelDelta", wheelDelta); + Logger.recordOutput("Drive/WheelRadius", wheelRadius); + }))); + } + + private static class WheelRadiusCharacterizationState { + double[] positions = new double[4]; + Rotation2d lastAngle = Rotation2d.kZero; + double gyroDelta = 0.0; + } +} diff --git a/src/main/java/frc/robot/subsystems/vision/CameraConstants.java b/src/main/java/frc/robot/subsystems/vision/CameraConstants.java new file mode 100644 index 0000000..d309c3f --- /dev/null +++ b/src/main/java/frc/robot/subsystems/vision/CameraConstants.java @@ -0,0 +1,64 @@ +package frc.robot.subsystems.vision; + +import org.jspecify.annotations.NullMarked; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Transform3d; +import edu.wpi.first.units.measure.Frequency; +import edu.wpi.first.units.measure.Time; +import frc.robot.util.typestate.AltMethod; +import frc.robot.util.typestate.OptionalField; +import frc.robot.util.typestate.RequiredField; +import frc.robot.util.typestate.TypeStateBuilder; + +/** Constants for a camera */ +@NullMarked +public class CameraConstants { + + public final String name; + public final int height; + public final int width; + public final Rotation2d horizontalFieldOfView; + public final Frequency simFps; + public final Time simLatency; + public final Time simLatencyStdDev; + public final double calibrationErrorMean; + public final double calibrationErrorStdDev; + public final Transform3d robotToCamera; + public final double translationError; + public final double rotationError; + public final double singleTagError; + + /** Constants for a camera */ + @TypeStateBuilder("CameraConstantsBuilder") + public CameraConstants(@RequiredField String name, @RequiredField int height, + @RequiredField int width, + @RequiredField(alt = @AltMethod(type = double.class, parameter_name = "degrees", + value = "edu.wpi.first.math.geometry.Rotation2d.fromDegrees(degrees)")) Rotation2d horizontalFieldOfView, + @RequiredField(alt = @AltMethod(type = double.class, parameter_name = "hz", + value = "edu.wpi.first.units.Units.Hertz.of(hz)")) Frequency simFps, + @RequiredField(alt = @AltMethod(type = double.class, parameter_name = "seconds", + value = "edu.wpi.first.units.Units.Seconds.of(seconds)")) Time simLatency, + @RequiredField(alt = @AltMethod(type = double.class, parameter_name = "seconds", + value = "edu.wpi.first.units.Units.Seconds.of(seconds)")) Time simLatencyStdDev, + @RequiredField double calibrationErrorMean, @RequiredField double calibrationErrorStdDev, + @RequiredField Transform3d robotToCamera, @OptionalField("0.3") double translationError, + @OptionalField("edu.wpi.first.math.util.Units.degreesToRadians(3)") double rotationError, + @OptionalField("0.2") double singleTagError) { + this.name = name; + this.height = height; + this.width = width; + this.horizontalFieldOfView = horizontalFieldOfView; + this.simFps = simFps; + this.simLatency = simLatency; + this.simLatencyStdDev = simLatencyStdDev; + this.calibrationErrorMean = calibrationErrorMean; + this.calibrationErrorStdDev = calibrationErrorStdDev; + this.robotToCamera = robotToCamera; + this.translationError = translationError; + this.rotationError = rotationError; + this.singleTagError = singleTagError; + } + + + +} diff --git a/src/main/java/frc/robot/subsystems/vision/Vision.java b/src/main/java/frc/robot/subsystems/vision/Vision.java new file mode 100644 index 0000000..b7fbd1c --- /dev/null +++ b/src/main/java/frc/robot/subsystems/vision/Vision.java @@ -0,0 +1,128 @@ +package frc.robot.subsystems.vision; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; +import org.jspecify.annotations.NullMarked; +import org.littletonrobotics.junction.Logger; +import org.photonvision.targeting.PhotonPipelineResult; +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.wpilibj2.command.SubsystemBase; +import frc.robot.Constants; +import frc.robot.subsystems.swerve.util.SwerveState; +import frc.robot.util.Tuples.Tuple2; + +/** + * Vision subsystem responsible for processing camera-based pose observations and contributing + * vision measurements to the drivetrain pose estimator. + * + *

+ * This subsystem acts as a bridge between one or more vision pipelines (e.g. PhotonVision) and the + * {@link SwerveState} pose estimator. + * + *

Timestamp handling

Vision measurements are frequently delayed relative to the control + * loop. This subsystem ensures measurements are applied in chronological order so that the pose + * estimator can correctly replay history using its internal pose buffer. + * + *

Multiple cameras

The subsystem supports an arbitrary number of cameras, each with its + * own calibration and transform defined in {@link Constants.Vision#cameraConstants}. All cameras + * contribute measurements into a single shared pose estimate. + * + *

Visualization

For debugging and analysis, the subsystem publishes 3D visualization data + * showing the relationship between the robot, cameras, and observed AprilTags. This data is + * intended for tools such as AdvantageScope and is not used for control. + */ +@NullMarked +public class Vision extends SubsystemBase { + + private final VisionIO io; + private final VisionIO.CameraInputs[] cameraInputs; + private final String[] cameraInputKeys; + private final SwerveState state; + private final Translation3d[][] cameraViz; + private final String[] cameraVizKeys; + private final boolean[] cameraContributed; + private final String[] cameraContributedKeys; + + /** + * Creates the vision subsystem. + * + * @param state shared swerve pose estimator to receive vision updates + * @param io vision IO implementation responsible for acquiring camera results + */ + public Vision(SwerveState state, VisionIO io) { + super("Vision"); + this.io = io; + this.state = state; + this.cameraInputs = IntStream.range(0, Constants.Vision.cameraConstants.length) + .mapToObj((_x) -> new VisionIO.CameraInputs()).toArray(VisionIO.CameraInputs[]::new); + this.cameraInputKeys = IntStream.range(0, Constants.Vision.cameraConstants.length) + .mapToObj((i) -> "Vision/Camera_" + i).toArray(String[]::new); + this.cameraViz = IntStream.range(0, Constants.Vision.cameraConstants.length) + .mapToObj((_x) -> new Translation3d[0]).toArray(Translation3d[][]::new); + this.cameraVizKeys = IntStream.range(0, Constants.Vision.cameraConstants.length) + .mapToObj((i) -> "Vision/AprilTagViz_" + i).toArray(String[]::new); + this.cameraContributed = new boolean[Constants.Vision.cameraConstants.length]; + this.cameraContributedKeys = IntStream.range(0, Constants.Vision.cameraConstants.length) + .mapToObj((i) -> "Vision/Contributed_" + i).toArray(String[]::new); + } + + @Override + public void periodic() { + io.updateInputs(cameraInputs); + for (int i = 0; i < cameraInputs.length; i++) { + Logger.processInputs(cameraInputKeys[i], cameraInputs[i]); + } + + // Get 2-tuples of (camera idx, result) + List> results = new ArrayList<>(); + for (int i = 0; i < cameraInputs.length; i++) { + + for (int j = 0; j < cameraInputs[i].results.length; j++) { + var result = cameraInputs[i].results[j]; + results.add(new Tuple2<>(i, result)); + } + } + + // Sort by timestamp (earlier pipeline results come first) + results.sort( + (a, b) -> Double.compare(a._1().getTimestampSeconds(), b._1().getTimestampSeconds())); + + for (int i = 0; i < Constants.Vision.cameraConstants.length; i++) { + cameraViz[i] = new Translation3d[0]; + } + + for (var result : results) { + cameraContributed[result._0()] = state + .addVisionObservation(Constants.Vision.cameraConstants[result._0()], result._1()); + for (int i = 0; i < result._1().targets.size(); i++) { + var cameraPose = new Pose3d(state.getGlobalPoseEstimate()) + .plus(Constants.Vision.cameraConstants[result._0()].robotToCamera); + var target = result._1().targets.get(i); + Constants.Vision.fieldLayout.getTagPose(target.fiducialId).ifPresent(pose -> { + cameraViz[result._0()] = addTwo(cameraViz[result._0()], + cameraPose.getTranslation(), pose.getTranslation()); + }); + } + } + + for (int i = 0; i < Constants.Vision.cameraConstants.length; i++) { + Logger.recordOutput(cameraContributedKeys[i], cameraContributed[i]); + if (cameraViz[i].length == 0) { + continue; + } + Logger.recordOutput(cameraVizKeys[i], cameraViz[i]); + } + } + + private Translation3d[] addTwo(Translation3d[] translations, Translation3d newTranslation1, + Translation3d newTranslation2) { + Translation3d[] res = new Translation3d[translations.length + 2]; + System.arraycopy(translations, 0, res, 0, translations.length); + res[translations.length] = newTranslation1; + res[translations.length + 1] = newTranslation2; + return res; + } + +} diff --git a/src/main/java/frc/robot/subsystems/vision/VisionIO.java b/src/main/java/frc/robot/subsystems/vision/VisionIO.java new file mode 100644 index 0000000..6699df1 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/vision/VisionIO.java @@ -0,0 +1,92 @@ +package frc.robot.subsystems.vision; + +import java.util.Optional; +import org.jspecify.annotations.NullMarked; +import org.littletonrobotics.junction.LogTable; +import org.littletonrobotics.junction.inputs.LoggableInputs; +import org.photonvision.common.dataflow.structures.Packet; +import org.photonvision.struct.PhotonPipelineResultSerde; +import org.photonvision.targeting.PhotonPipelineResult; +import edu.wpi.first.math.Matrix; +import edu.wpi.first.math.numbers.N1; +import edu.wpi.first.math.numbers.N3; +import edu.wpi.first.math.numbers.N8; +import frc.robot.util.GenerateEmptyIO; + +/** IO Class for Vision */ +@GenerateEmptyIO +@NullMarked +public interface VisionIO { + + /** Inputs for an individual camera */ + public static class CameraInputs implements LoggableInputs, Cloneable { + public PhotonPipelineResult[] results = new PhotonPipelineResult[0]; + public Optional> cameraMatrix = Optional.empty(); + public Optional> distCoeffs = Optional.empty(); + + private static final PhotonPipelineResultSerde serde = new PhotonPipelineResultSerde(); + + @Override + public void toLog(LogTable table) { + table.put("length", results.length); + for (int i = 0; i < results.length; i++) { + LogTable subtable = table.getSubtable("[" + i + "]"); + var result = results[i]; + if (result == null) { + var rawBytes = new byte[0]; + subtable.put("rawBytes", rawBytes); + } else { + // https://github.com/PhotonVision/photonvision/blob/78b82e3a96d04f79e0b70746dd975c10c8ff1294/photon-core/src/main/java/org/photonvision/common/dataflow/networktables/NTDataPublisher.java#L166 + var rawBytes = new Packet(1024); + serde.pack(rawBytes, result); + subtable.put("rawBytes", rawBytes.getWrittenDataCopy()); + } + } + if (cameraMatrix.isPresent()) { + table.put("cameraMatrix", cameraMatrix.get().getData()); + } else { + table.put("cameraMatrix", new double[0]); + } + if (distCoeffs.isPresent()) { + table.put("distCoeffs", distCoeffs.get().getData()); + } else { + table.put("distCoeffs", new double[0]); + } + } + + @Override + public void fromLog(LogTable table) { + int length = table.get("length", 0); + results = new PhotonPipelineResult[length]; + for (int i = 0; i < length; i++) { + LogTable subtable = table.getSubtable("[" + i + "]"); + var rawBytes = subtable.get("rawBytes", new byte[0]); + PhotonPipelineResult result; + if (rawBytes.length > 0) { + Packet p = new Packet(rawBytes); + result = serde.unpack(p); + } else { + result = null; + } + results[i] = result; + } + + double[] data = table.get("cameraMatrix", new double[0]); + if (data.length == 0) { + this.cameraMatrix = Optional.empty(); + } else { + this.cameraMatrix = Optional.of(new Matrix<>(N3.instance, N3.instance, data)); + } + data = table.get("distCoeffs", new double[0]); + if (data.length == 0) { + this.distCoeffs = Optional.empty(); + } else { + this.distCoeffs = Optional.of(new Matrix<>(N8.instance, N1.instance, data)); + } + } + + } + + public void updateInputs(CameraInputs[] inputs); + +} diff --git a/src/main/java/frc/robot/subsystems/vision/VisionReal.java b/src/main/java/frc/robot/subsystems/vision/VisionReal.java new file mode 100644 index 0000000..f2c56dc --- /dev/null +++ b/src/main/java/frc/robot/subsystems/vision/VisionReal.java @@ -0,0 +1,31 @@ +package frc.robot.subsystems.vision; + +import java.util.Arrays; +import org.jspecify.annotations.NullMarked; +import org.photonvision.PhotonCamera; +import org.photonvision.targeting.PhotonPipelineResult; +import frc.robot.Constants; + +/** PhotonVision-attached implementation */ +@NullMarked +public class VisionReal implements VisionIO { + + protected final PhotonCamera[] cameras; + + /** PhotonVision-attached implementation */ + public VisionReal() { + cameras = Arrays.stream(Constants.Vision.cameraConstants) + .map((consts) -> new PhotonCamera(consts.name)).toArray(PhotonCamera[]::new); + } + + @Override + public void updateInputs(CameraInputs[] inputs) { + for (int i = 0; i < cameras.length; i++) { + inputs[i].results = + cameras[i].getAllUnreadResults().toArray(PhotonPipelineResult[]::new); + inputs[i].cameraMatrix = cameras[i].getCameraMatrix(); + inputs[i].distCoeffs = cameras[i].getDistCoeffs(); + } + } + +} diff --git a/src/main/java/frc/robot/subsystems/vision/VisionSim.java b/src/main/java/frc/robot/subsystems/vision/VisionSim.java new file mode 100644 index 0000000..d2c77fe --- /dev/null +++ b/src/main/java/frc/robot/subsystems/vision/VisionSim.java @@ -0,0 +1,48 @@ +package frc.robot.subsystems.vision; + +import static edu.wpi.first.units.Units.Hertz; +import static edu.wpi.first.units.Units.Milliseconds; +import org.ironmaple.simulation.drivesims.SwerveDriveSimulation; +import org.jspecify.annotations.NullMarked; +import org.photonvision.simulation.PhotonCameraSim; +import org.photonvision.simulation.SimCameraProperties; +import org.photonvision.simulation.VisionSystemSim; +import frc.robot.Constants; +import frc.robot.subsystems.swerve.SwerveSim; + +/** Simulation of vision using built-in PhotonVision simulator. */ +@NullMarked +public class VisionSim extends VisionReal { + + private final SwerveDriveSimulation sim; + private final VisionSystemSim visionSim; + + /** Simulation of vision using built-in PhotonVision simulator. */ + public VisionSim(SwerveSim sim) { + this.sim = sim.mapleSim; + this.visionSim = new VisionSystemSim("main"); + + visionSim.addAprilTags(Constants.Vision.fieldLayout); + + var constants = Constants.Vision.cameraConstants; + for (int i = 0; i < constants.length; i++) { + SimCameraProperties props = new SimCameraProperties(); + props.setCalibration(constants[i].width, constants[i].height, + constants[i].horizontalFieldOfView); + props.setCalibError(constants[i].calibrationErrorMean, + constants[i].calibrationErrorStdDev); + props.setFPS(constants[i].simFps.in(Hertz)); + props.setAvgLatencyMs(constants[i].simLatency.in(Milliseconds)); + props.setLatencyStdDevMs(constants[i].simLatencyStdDev.in(Milliseconds)); + PhotonCameraSim cameraSim = new PhotonCameraSim(this.cameras[i], props); + visionSim.addCamera(cameraSim, constants[i].robotToCamera); + } + } + + @Override + public void updateInputs(CameraInputs[] inputs) { + visionSim.update(sim.getSimulatedDriveTrainPose()); + super.updateInputs(inputs); + } + +} diff --git a/src/main/java/frc/robot/util/AllianceFlipUtil.java b/src/main/java/frc/robot/util/AllianceFlipUtil.java new file mode 100644 index 0000000..7e3af81 --- /dev/null +++ b/src/main/java/frc/robot/util/AllianceFlipUtil.java @@ -0,0 +1,48 @@ +package frc.robot.util; + +import org.jspecify.annotations.NullMarked; +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.math.util.Units; +import edu.wpi.first.wpilibj.DriverStation; + +/** Utilities for flipping based on alliance */ +@NullMarked +public class AllianceFlipUtil { + public static double fieldWidth = Units.feetToMeters(26.0) + Units.inchesToMeters(5.0); + public static double fieldLength = Units.feetToMeters(57.0) + Units.inchesToMeters(6.875); + + /** Possibly flip */ + public static double applyX(double x) { + return shouldFlip() ? fieldLength - x : x; + } + + /** Possibly flip */ + public static double applyY(double y) { + return shouldFlip() ? fieldWidth - y : y; + } + + /** Possibly flip */ + public static Translation2d apply(Translation2d translation) { + return new Translation2d(applyX(translation.getX()), applyY(translation.getY())); + } + + /** Possibly flip */ + public static Rotation2d apply(Rotation2d rotation) { + return shouldFlip() ? rotation.rotateBy(Rotation2d.kPi) : rotation; + } + + /** Possibly flip */ + public static Pose2d apply(Pose2d pose) { + return shouldFlip() ? new Pose2d(apply(pose.getTranslation()), apply(pose.getRotation())) + : pose; + } + + /** Possibly flip */ + public static boolean shouldFlip() { + return DriverStation.getAlliance().isPresent() + && DriverStation.getAlliance().get() == DriverStation.Alliance.Red; + } + +} diff --git a/src/main/java/frc/robot/util/DeviceDebug.java b/src/main/java/frc/robot/util/DeviceDebug.java new file mode 100644 index 0000000..3434be9 --- /dev/null +++ b/src/main/java/frc/robot/util/DeviceDebug.java @@ -0,0 +1,145 @@ +package frc.robot.util; + +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; +import com.ctre.phoenix6.configs.MotorOutputConfigs; +import com.ctre.phoenix6.hardware.CANcoder; +import com.ctre.phoenix6.hardware.TalonFX; +import com.ctre.phoenix6.signals.NeutralModeValue; +import edu.wpi.first.networktables.NetworkTableEvent; +import edu.wpi.first.networktables.NetworkTableInstance; + +/** + * Runtime debugging utility for exposing low-level device controls via NetworkTables. + * + *

+ * This class allows selected CAN devices to be interactively inspected and reconfigured at runtime + * through NetworkTables (e.g. from Glass, Shuffleboard, or AdvantageScope). It is intended strictly + * for debugging and tuning and should not be relied upon for normal robot operation. + * + *

Design overview

+ *
    + *
  • Devices are explicitly registered by name before debugging is enabled.
  • + *
  • No NetworkTables listeners or control hooks are installed until debugging is explicitly + * enabled via {@code /Debug/Enable}.
  • + *
  • Once enabled, this class publishes per-device debug topics and installs listeners to apply + * configuration changes live.
  • + *
+ * + *

Safety model

+ * + *

+ * Debug functionality is gated behind an explicit NetworkTables boolean ({@code /Debug/Enable}) to + * prevent accidental configuration changes during normal operation. Once enabled, the enable + * listener is removed and debugging remains active for the lifetime of the program. + * + *

+ * This class is not thread-safe and is expected to be initialized and used during robot + * startup before devices are actively commanded. + * + *

+ * Support for additional device types and parameters (e.g. CANcoders, current limits, inversion, + * soft limits) may be added in the future. + */ +public final class DeviceDebug { + + private DeviceDebug() {} + + private static final Map talonFXs = new HashMap<>(); + private static final Map cancoders = new HashMap<>(); + + private static int enableListener; + + /** + * Initializes the debug system and installs the global enable listener. + * + *

+ * This method publishes {@code /Debug/Enable} (default {@code false}) and waits for it to be + * set {@code true}. When enabled, all registered devices will have their debug topics created + * and listeners installed. + * + *

+ * This method should be called exactly once during robot initialization. + */ + public static void initialize() { + NetworkTableInstance instance = NetworkTableInstance.getDefault(); + var enableTopic = instance.getBooleanTopic("/Debug/Enable"); + enableTopic.publish().accept(false); + enableListener = instance.addListener(enableTopic.subscribe(false), + EnumSet.of(NetworkTableEvent.Kind.kValueAll), (ev) -> { + if (ev.valueData.value.getBoolean()) { + setupDebug(instance); + } + }); + } + + private static void setupDebug(NetworkTableInstance instance) { + instance.removeListener(enableListener); + for (var kv : talonFXs.entrySet()) { + String key = "/Debug/TalonFX/" + kv.getKey(); + + var brakeModeTopic = instance.getBooleanTopic(key + "/Brake"); + TalonFX talon = kv.getValue(); + boolean isBrake = getBrakeMode(talon); + var publisher = brakeModeTopic.publish(); + publisher.accept(isBrake); + instance.addListener(brakeModeTopic.subscribe(isBrake), + EnumSet.of(NetworkTableEvent.Kind.kValueRemote), (ev) -> { + setBrakeMode(talon, ev.valueData.value.getBoolean()); + boolean isBrake_ = getBrakeMode(talon); + publisher.accept(isBrake_); + }); + } + } + + /** + * Registers a TalonFX for runtime debugging. + * + *

+ * The provided name is used as part of the NetworkTables path: + * {@code /Debug/TalonFX/<name>}. + * + *

+ * Registration must occur before debugging is enabled. + * + * @param name human-readable identifier for the device + * @param talon TalonFX instance to expose for debugging + */ + public static void register(String name, TalonFX talon) { + talonFXs.put(name, talon); + } + + /** + * Registers a CANcoder for future runtime debugging. + * + *

+ * This method currently records the device but does not yet expose any debug controls. It + * exists to allow future expansion without changing registration semantics. + * + *

+ * Registration must occur before debugging is enabled. + * + * @param name human-readable identifier for the device + * @param cancoder CANcoder instance to register + */ + public static void register(String name, CANcoder cancoder) { + cancoders.put(name, cancoder); + } + + private static boolean getBrakeMode(TalonFX talon) { + MotorOutputConfigs config = new MotorOutputConfigs(); + var configurator = talon.getConfigurator(); + configurator.refresh(config); + return config.NeutralMode.value == NeutralModeValue.Brake.value; + } + + private static void setBrakeMode(TalonFX talon, boolean enableBrake) { + MotorOutputConfigs config = new MotorOutputConfigs(); + var configurator = talon.getConfigurator(); + configurator.refresh(config); + config.NeutralMode = enableBrake ? NeutralModeValue.Brake : NeutralModeValue.Coast; + PhoenixSignals.tryUntilOk(5, () -> configurator.apply(config)); + } + +} diff --git a/src/main/java/frc/robot/util/GenerateEmptyIO.java b/src/main/java/frc/robot/util/GenerateEmptyIO.java new file mode 100644 index 0000000..38367c3 --- /dev/null +++ b/src/main/java/frc/robot/util/GenerateEmptyIO.java @@ -0,0 +1,16 @@ +package frc.robot.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Generate Empty IO implementation using RobotUtils */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface GenerateEmptyIO { + + /** Parameter types for constructor */ + public Class[] value() default {}; + +} diff --git a/src/main/java/frc/robot/util/PhoenixSignals.java b/src/main/java/frc/robot/util/PhoenixSignals.java new file mode 100644 index 0000000..cc387b4 --- /dev/null +++ b/src/main/java/frc/robot/util/PhoenixSignals.java @@ -0,0 +1,108 @@ +package frc.robot.util; + +import java.util.function.Supplier; +import org.jspecify.annotations.NullMarked; +import com.ctre.phoenix6.BaseStatusSignal; +import com.ctre.phoenix6.StatusCode; + +/** + * Utility class that centralizes registration and synchronized refreshing of CTRE Phoenix + * {@link BaseStatusSignal}s. + * + *

+ * Grouping signals and refreshing them together ensures that all sampled values share the same + * synchronization fence, minimizing timestamp skew between related measurements. + * + *

+ * Signals are maintained in separate groups depending on whether they are sourced from a CANivore + * bus or the roboRIO CAN bus, as Phoenix requires refresh calls to be performed per bus. + * + *

+ * This class is purely static and is not intended to be instantiated. + */ +@NullMarked +public class PhoenixSignals { + + private PhoenixSignals() {} + + /** + * Repeatedly executes a Phoenix command until it succeeds or the maximum number of attempts is + * reached. + * + *

+ * This is useful for Phoenix configuration calls that may transiently fail during startup or + * bus contention. + * + * @param maxAttempts maximum number of attempts before giving up + * @param command supplier that executes the Phoenix command and returns a {@link StatusCode} + */ + public static void tryUntilOk(int maxAttempts, Supplier command) { + for (int i = 0; i < maxAttempts; i++) { + var error = command.get(); + if (error.isOK()) { + break; + } + } + } + + /** Signals registered on the CANivore bus for synchronized refresh. */ + private static BaseStatusSignal[] canivoreSignals = new BaseStatusSignal[0]; + /** Signals registered on the roboRIO CAN bus for synchronized refresh. */ + private static BaseStatusSignal[] rioSignals = new BaseStatusSignal[0]; + + /** + * Registers one or more Phoenix signals for synchronized refreshing. + * + *

+ * All registered signals will be refreshed together when {@link #refreshAll()} is called, + * ensuring consistent timestamps across related measurements. + * + *

+ * Signals must be registered to the correct bus (CANivore vs roboRIO). Mixing buses in a single + * refresh call is not supported by Phoenix. + * + *

+ * This method should typically be called during subsystem construction or robot initialization, + * before periodic refreshes occur. + * + * @param canivore {@code true} if the signals are on the CANivore bus, {@code false} if they + * are on the roboRIO CAN bus + * @param signals one or more {@link BaseStatusSignal}s to register + */ + public static void registerSignals(boolean canivore, BaseStatusSignal... signals) { + if (canivore) { + BaseStatusSignal[] newSignals = + new BaseStatusSignal[canivoreSignals.length + signals.length]; + System.arraycopy(canivoreSignals, 0, newSignals, 0, canivoreSignals.length); + System.arraycopy(signals, 0, newSignals, canivoreSignals.length, signals.length); + canivoreSignals = newSignals; + } else { + BaseStatusSignal[] newSignals = + new BaseStatusSignal[rioSignals.length + signals.length]; + System.arraycopy(rioSignals, 0, newSignals, 0, rioSignals.length); + System.arraycopy(signals, 0, newSignals, rioSignals.length, signals.length); + rioSignals = newSignals; + } + } + + /** + * Refreshes all registered Phoenix signals. + * + *

+ * This method should be called once per control loop to update all registered signals in a + * synchronized manner. + * + *

+ * Signals on the CANivore and roboRIO buses are refreshed separately, but each group is + * internally synchronized. + */ + public static void refreshAll() { + if (canivoreSignals.length > 0) { + BaseStatusSignal.refreshAll(canivoreSignals); + } + if (rioSignals.length > 0) { + BaseStatusSignal.refreshAll(rioSignals); + } + } + +} diff --git a/src/main/java/frc/robot/util/SparkSignals.java b/src/main/java/frc/robot/util/SparkSignals.java new file mode 100644 index 0000000..9ef371f --- /dev/null +++ b/src/main/java/frc/robot/util/SparkSignals.java @@ -0,0 +1,158 @@ +package frc.robot.util; + +import java.util.function.Consumer; +import java.util.function.DoubleConsumer; +import java.util.function.DoubleSupplier; +import java.util.function.Function; +import java.util.function.Supplier; +import org.jspecify.annotations.NullMarked; +import com.revrobotics.REVLibError; +import com.revrobotics.spark.SparkBase; + +/** + * Utility methods for safely interacting with REV Spark motor controllers in the presence of + * transient or persistent communication errors. + * + *

+ * REV API calls report errors via {@link REVLibError} rather than exceptions. These helpers + * centralize common error-handling patterns and ensure that invalid or stale values are not + * inadvertently consumed by higher-level logic. + * + *

+ * All utility methods update a shared {@link #sparkStickyFault} flag whenever a non-{@code kOk} + * error is detected. This flag may be monitored elsewhere in the robot code to trigger diagnostics, + * alerts, or safe fallback behavior. + * + *

+ * This class is purely static and is not intended to be instantiated. + */ +@NullMarked +public class SparkSignals { + + private SparkSignals() {} + + /** + * Sticky indicator that is set whenever a Spark-related error is detected. + * + *

+ * Once set, this flag remains {@code true} until explicitly cleared by user code. It can be + * used to surface non-fatal communication issues that may otherwise go unnoticed during + * operation. + */ + public static boolean sparkStickyFault = false; + + /** + * Retrieves and processes a value from a Spark controller only if the underlying call completed + * successfully. + * + *

+ * If the Spark reports an error, the value is discarded and the consumer is not invoked. + * + * @param spark the Spark device associated with the value + * @param supplier supplier that retrieves the value from the Spark + * @param consumer consumer that processes the value if it is valid + */ + public static void ifOk(SparkBase spark, DoubleSupplier supplier, DoubleConsumer consumer) { + double value = supplier.getAsDouble(); + if (spark.getLastError() == REVLibError.kOk) { + consumer.accept(value); + } else { + sparkStickyFault = true; + } + } + + /** + * Retrieves multiple values from a Spark controller and processes them only if all values are + * valid. + * + *

+ * If any supplier produces an error, processing is aborted and the consumer is not invoked. + * + * @param spark the Spark device associated with the values + * @param suppliers array of suppliers retrieving Spark values + * @param consumer consumer that processes the values if all are valid + */ + public static void ifOk(SparkBase spark, DoubleSupplier[] suppliers, + Consumer consumer) { + double[] values = new double[suppliers.length]; + for (int i = 0; i < suppliers.length; i++) { + values[i] = suppliers[i].getAsDouble(); + if (spark.getLastError() != REVLibError.kOk) { + sparkStickyFault = true; + return; + } + } + consumer.accept(values); + } + + /** + * Retrieves a value from a Spark controller, returning a default if the value is invalid. + * + *

+ * If an error occurs, the default value is returned and the sticky fault flag is set. + * + * @param spark the Spark device associated with the value + * @param supplier supplier that retrieves the value + * @param defaultValue value to return if an error is detected + * @return the retrieved value if valid, otherwise {@code defaultValue} + */ + public static double ifOkOrDefault(SparkBase spark, DoubleSupplier supplier, + double defaultValue) { + double value = supplier.getAsDouble(); + if (spark.getLastError() == REVLibError.kOk) { + return value; + } else { + sparkStickyFault = true; + return defaultValue; + } + } + + /** + * Retrieves multiple values from a Spark controller and applies a transformation only if all + * values are valid. + * + *

+ * If any value is invalid, the transformation is skipped and a default value is returned. + * + * @param spark the Spark device associated with the values + * @param suppliers array of suppliers retrieving Spark values + * @param transformer function that computes a result from the retrieved values + * @param defaultValue value to return if any supplier reports an error + * @return transformed value if all inputs are valid, otherwise {@code defaultValue} + */ + public static double ifOkOrDefault(SparkBase spark, DoubleSupplier[] suppliers, + Function transformer, double defaultValue) { + double[] values = new double[suppliers.length]; + for (int i = 0; i < suppliers.length; i++) { + values[i] = suppliers[i].getAsDouble(); + if (spark.getLastError() != REVLibError.kOk) { + sparkStickyFault = true; + return defaultValue; + } + } + return transformer.apply(values); + } + + /** + * Repeatedly executes a Spark command until it succeeds or the maximum number of attempts is + * reached. + * + *

+ * This is commonly used for configuration calls that may fail during initialization due to + * transient CAN issues. + * + * @param maxAttempts maximum number of attempts before giving up + * @param command supplier that executes the Spark command and returns a {@link REVLibError} + */ + public static void tryUntilOk(int maxAttempts, Supplier command) { + for (int i = 0; i < maxAttempts; i++) { + var error = command.get(); + if (error == REVLibError.kOk) { + break; + } else { + sparkStickyFault = true; + } + } + } + +} diff --git a/src/main/java/frc/robot/util/Tuples.java b/src/main/java/frc/robot/util/Tuples.java new file mode 100644 index 0000000..4cc53e6 --- /dev/null +++ b/src/main/java/frc/robot/util/Tuples.java @@ -0,0 +1,565 @@ +package frc.robot.util; + +/** + * Defines various tuple types for Java. + */ +public final class Tuples { + private Tuples() {} + + + /** + * A type that has a 1st element. + */ + public static interface IValue1 { + + /** + * Get the 1st element. + */ + T _0(); + } + + + /** + * A tuple of 1 elements. + */ + public static final class Tuple1 implements IValue1 { + private final T1 v0; + + /** + * Create a new 1-tuple. + */ + public Tuple1(T1 v0) { + this.v0 = v0; + } + + @Override + public T1 _0() { + return this.v0; + } + + } + + + /** + * A type that has a 2nd element. + */ + public static interface IValue2 { + + /** + * Get the 2nd element. + */ + T _1(); + } + + + /** + * A tuple of 2 elements. + */ + public static final class Tuple2 implements IValue1, IValue2 { + private final T1 v0; + private final T2 v1; + + /** + * Create a new 2-tuple. + */ + public Tuple2(T1 v0, T2 v1) { + this.v0 = v0; + this.v1 = v1; + } + + @Override + public T1 _0() { + return this.v0; + } + + @Override + public T2 _1() { + return this.v1; + } + + } + + + /** + * A type that has a 3rd element. + */ + public static interface IValue3 { + + /** + * Get the 3rd element. + */ + T _2(); + } + + + /** + * A tuple of 3 elements. + */ + public static final class Tuple3 implements IValue1, IValue2, IValue3 { + private final T1 v0; + private final T2 v1; + private final T3 v2; + + /** + * Create a new 3-tuple. + */ + public Tuple3(T1 v0, T2 v1, T3 v2) { + this.v0 = v0; + this.v1 = v1; + this.v2 = v2; + } + + @Override + public T1 _0() { + return this.v0; + } + + @Override + public T2 _1() { + return this.v1; + } + + @Override + public T3 _2() { + return this.v2; + } + + } + + + /** + * A type that has a 4th element. + */ + public static interface IValue4 { + + /** + * Get the 4th element. + */ + T _3(); + } + + + /** + * A tuple of 4 elements. + */ + public static final class Tuple4 + implements IValue1, IValue2, IValue3, IValue4 { + private final T1 v0; + private final T2 v1; + private final T3 v2; + private final T4 v3; + + /** + * Create a new 4-tuple. + */ + public Tuple4(T1 v0, T2 v1, T3 v2, T4 v3) { + this.v0 = v0; + this.v1 = v1; + this.v2 = v2; + this.v3 = v3; + } + + @Override + public T1 _0() { + return this.v0; + } + + @Override + public T2 _1() { + return this.v1; + } + + @Override + public T3 _2() { + return this.v2; + } + + @Override + public T4 _3() { + return this.v3; + } + + } + + + /** + * A type that has a 5th element. + */ + public static interface IValue5 { + + /** + * Get the 5th element. + */ + T _4(); + } + + + /** + * A tuple of 5 elements. + */ + public static final class Tuple5 + implements IValue1, IValue2, IValue3, IValue4, IValue5 { + private final T1 v0; + private final T2 v1; + private final T3 v2; + private final T4 v3; + private final T5 v4; + + /** + * Create a new 5-tuple. + */ + public Tuple5(T1 v0, T2 v1, T3 v2, T4 v3, T5 v4) { + this.v0 = v0; + this.v1 = v1; + this.v2 = v2; + this.v3 = v3; + this.v4 = v4; + } + + @Override + public T1 _0() { + return this.v0; + } + + @Override + public T2 _1() { + return this.v1; + } + + @Override + public T3 _2() { + return this.v2; + } + + @Override + public T4 _3() { + return this.v3; + } + + @Override + public T5 _4() { + return this.v4; + } + + } + + + /** + * A type that has a 6th element. + */ + public static interface IValue6 { + + /** + * Get the 6th element. + */ + T _5(); + } + + + /** + * A tuple of 6 elements. + */ + public static final class Tuple6 + implements IValue1, IValue2, IValue3, IValue4, IValue5, IValue6 { + private final T1 v0; + private final T2 v1; + private final T3 v2; + private final T4 v3; + private final T5 v4; + private final T6 v5; + + /** + * Create a new 6-tuple. + */ + public Tuple6(T1 v0, T2 v1, T3 v2, T4 v3, T5 v4, T6 v5) { + this.v0 = v0; + this.v1 = v1; + this.v2 = v2; + this.v3 = v3; + this.v4 = v4; + this.v5 = v5; + } + + @Override + public T1 _0() { + return this.v0; + } + + @Override + public T2 _1() { + return this.v1; + } + + @Override + public T3 _2() { + return this.v2; + } + + @Override + public T4 _3() { + return this.v3; + } + + @Override + public T5 _4() { + return this.v4; + } + + @Override + public T6 _5() { + return this.v5; + } + + } + + + /** + * A type that has a 7th element. + */ + public static interface IValue7 { + + /** + * Get the 7th element. + */ + T _6(); + } + + + /** + * A tuple of 7 elements. + */ + public static final class Tuple7 implements IValue1, + IValue2, IValue3, IValue4, IValue5, IValue6, IValue7 { + private final T1 v0; + private final T2 v1; + private final T3 v2; + private final T4 v3; + private final T5 v4; + private final T6 v5; + private final T7 v6; + + /** + * Create a new 7-tuple. + */ + public Tuple7(T1 v0, T2 v1, T3 v2, T4 v3, T5 v4, T6 v5, T7 v6) { + this.v0 = v0; + this.v1 = v1; + this.v2 = v2; + this.v3 = v3; + this.v4 = v4; + this.v5 = v5; + this.v6 = v6; + } + + @Override + public T1 _0() { + return this.v0; + } + + @Override + public T2 _1() { + return this.v1; + } + + @Override + public T3 _2() { + return this.v2; + } + + @Override + public T4 _3() { + return this.v3; + } + + @Override + public T5 _4() { + return this.v4; + } + + @Override + public T6 _5() { + return this.v5; + } + + @Override + public T7 _6() { + return this.v6; + } + + } + + + /** + * A type that has a 8th element. + */ + public static interface IValue8 { + + /** + * Get the 8th element. + */ + T _7(); + } + + + /** + * A tuple of 8 elements. + */ + public static final class Tuple8 implements IValue1, + IValue2, IValue3, IValue4, IValue5, IValue6, IValue7, IValue8 { + private final T1 v0; + private final T2 v1; + private final T3 v2; + private final T4 v3; + private final T5 v4; + private final T6 v5; + private final T7 v6; + private final T8 v7; + + /** + * Create a new 8-tuple. + */ + public Tuple8(T1 v0, T2 v1, T3 v2, T4 v3, T5 v4, T6 v5, T7 v6, T8 v7) { + this.v0 = v0; + this.v1 = v1; + this.v2 = v2; + this.v3 = v3; + this.v4 = v4; + this.v5 = v5; + this.v6 = v6; + this.v7 = v7; + } + + @Override + public T1 _0() { + return this.v0; + } + + @Override + public T2 _1() { + return this.v1; + } + + @Override + public T3 _2() { + return this.v2; + } + + @Override + public T4 _3() { + return this.v3; + } + + @Override + public T5 _4() { + return this.v4; + } + + @Override + public T6 _5() { + return this.v5; + } + + @Override + public T7 _6() { + return this.v6; + } + + @Override + public T8 _7() { + return this.v7; + } + + } + + + /** + * A type that has a 9th element. + */ + public static interface IValue9 { + + /** + * Get the 9th element. + */ + T _8(); + } + + + /** + * A tuple of 9 elements. + */ + public static final class Tuple9 + implements IValue1, IValue2, IValue3, IValue4, IValue5, IValue6, + IValue7, IValue8, IValue9 { + private final T1 v0; + private final T2 v1; + private final T3 v2; + private final T4 v3; + private final T5 v4; + private final T6 v5; + private final T7 v6; + private final T8 v7; + private final T9 v8; + + /** + * Create a new 9-tuple. + */ + public Tuple9(T1 v0, T2 v1, T3 v2, T4 v3, T5 v4, T6 v5, T7 v6, T8 v7, T9 v8) { + this.v0 = v0; + this.v1 = v1; + this.v2 = v2; + this.v3 = v3; + this.v4 = v4; + this.v5 = v5; + this.v6 = v6; + this.v7 = v7; + this.v8 = v8; + } + + @Override + public T1 _0() { + return this.v0; + } + + @Override + public T2 _1() { + return this.v1; + } + + @Override + public T3 _2() { + return this.v2; + } + + @Override + public T4 _3() { + return this.v3; + } + + @Override + public T5 _4() { + return this.v4; + } + + @Override + public T6 _5() { + return this.v5; + } + + @Override + public T7 _6() { + return this.v6; + } + + @Override + public T8 _7() { + return this.v7; + } + + @Override + public T9 _8() { + return this.v8; + } + + } + +} diff --git a/src/main/java/frc/robot/util/typestate/AltMethod.java b/src/main/java/frc/robot/util/typestate/AltMethod.java new file mode 100644 index 0000000..1b6da96 --- /dev/null +++ b/src/main/java/frc/robot/util/typestate/AltMethod.java @@ -0,0 +1,15 @@ +package frc.robot.util.typestate; + +/** Alternative builder step. */ +public @interface AltMethod { + + /** The alternative parameter type. */ + public Class type(); + + /** If specified, this parameter name is used instead of the constructor parameter name. */ + public String parameter_name() default ""; + + /** Stringified java expression converting the alternative type into the actual type. */ + public String value(); + +} diff --git a/src/main/java/frc/robot/util/typestate/InitField.java b/src/main/java/frc/robot/util/typestate/InitField.java new file mode 100644 index 0000000..a3121df --- /dev/null +++ b/src/main/java/frc/robot/util/typestate/InitField.java @@ -0,0 +1,12 @@ +package frc.robot.util.typestate; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Marks this parameter as required to create the builder. */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.PARAMETER) +public @interface InitField { +} diff --git a/src/main/java/frc/robot/util/typestate/OptionalField.java b/src/main/java/frc/robot/util/typestate/OptionalField.java new file mode 100644 index 0000000..170b5ed --- /dev/null +++ b/src/main/java/frc/robot/util/typestate/OptionalField.java @@ -0,0 +1,12 @@ +package frc.robot.util.typestate; + +/** Marks this parameter as optional. */ +public @interface OptionalField { + + /** Stringified java expression used when this parameter is not specified. */ + public String value(); + + /** Alternative build methods. */ + public AltMethod[] alt() default {}; + +} diff --git a/src/main/java/frc/robot/util/typestate/RequiredField.java b/src/main/java/frc/robot/util/typestate/RequiredField.java new file mode 100644 index 0000000..2bec093 --- /dev/null +++ b/src/main/java/frc/robot/util/typestate/RequiredField.java @@ -0,0 +1,8 @@ +package frc.robot.util.typestate; + +/** Field required to be specified before finishing. */ +public @interface RequiredField { + + /** Alternative build methods. */ + public AltMethod[] alt() default {}; +} diff --git a/src/main/java/frc/robot/util/typestate/TypeStateBuilder.java b/src/main/java/frc/robot/util/typestate/TypeStateBuilder.java new file mode 100644 index 0000000..56744eb --- /dev/null +++ b/src/main/java/frc/robot/util/typestate/TypeStateBuilder.java @@ -0,0 +1,19 @@ +package frc.robot.util.typestate; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Generate TypeState Builder using RobotUtils */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.CONSTRUCTOR) +public @interface TypeStateBuilder { + + /** + * If specified, the builder's class name is this. Otherwise, "Builder" is appended to the + * enclosing class's name. + */ + public String value() default ""; + +} diff --git a/src/main/java/frc/robot/viz/RobotViz.java b/src/main/java/frc/robot/viz/RobotViz.java new file mode 100644 index 0000000..e52f7ca --- /dev/null +++ b/src/main/java/frc/robot/viz/RobotViz.java @@ -0,0 +1,87 @@ +package frc.robot.viz; + +import java.util.Arrays; +import java.util.function.Supplier; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.littletonrobotics.junction.Logger; +import edu.wpi.first.math.geometry.Pose3d; +import frc.robot.Constants; +import frc.robot.subsystems.swerve.Swerve; +import frc.robot.subsystems.swerve.SwerveSim; +import frc.robot.subsystems.vision.CameraConstants; + +/** + * Centralized visualization helper for publishing robot state to logging and visualization tools. + * + *

+ * Currently, this class publishes visualization data related to drivetrain state, including the + * robot's global pose and derived camera poses. + * + *

+ * Future intent: This class is intended to become the single location responsible for + * publishing visualization data for all robot subsystems, such as elevators, arms, + * intakes, and other articulated mechanisms. Each subsystem will contribute geometry or pose data + * that can be rendered together in a unified visualization view. + * + *

+ * This class supports both real and simulated operation: + *

    + *
  • When a {@link SwerveSim} instance is provided, visualization data is sourced from the + * simulator's ground-truth drivetrain pose.
  • + *
  • Otherwise, certain entries with keys indicating ground truth use estimates instead, often + * duplicating other entries (e.g. {@code ActualPose} will be equivalent to + * {@code GlobalEstPose}).
  • + *
+ * + *

+ * All outputs are published via {@link Logger} and are intended strictly for debugging, analysis, + * and visualization (e.g., AdvantageScope). No control or decision-making logic should depend on + * this class. + */ +@NullMarked +public class RobotViz { + + private final @Nullable Supplier robotPoseSupplier; + private final Supplier estPoseSupplier; + + /** + * Creates a new visualization helper. + * + * @param sim optional swerve simulator; when provided, simulated ground-truth drivetrain pose + * is used for visualization + * @param swerve live swerve subsystem providing pose estimates when not simulating + */ + public RobotViz(@Nullable SwerveSim sim, Swerve swerve) { + estPoseSupplier = () -> new Pose3d(swerve.state.getGlobalPoseEstimate()); + if (sim != null) { + robotPoseSupplier = () -> new Pose3d(sim.mapleSim.getSimulatedDriveTrainPose()); + } else { + robotPoseSupplier = estPoseSupplier; + } + } + + /** + * Publishes visualization data for the current control loop iteration. + * + *

+ * This method should be called periodically (e.g., from {@code robotPeriodic}). + * + *

+ * As additional subsystems are integrated, this method will be extended to publish poses, + * transforms, and geometry for other mechanisms relative to the robot frame. + */ + public void periodic() { + Pose3d robotPose = robotPoseSupplier.get(); + Pose3d estPose = estPoseSupplier.get(); + Logger.recordOutput("Viz/ActualPose", robotPose); + for (CameraConstants constants : Constants.Vision.cameraConstants) { + Logger.recordOutput("Viz/Cameras/" + constants.name + "/ActualPose", + robotPose.plus(constants.robotToCamera)); + } + Logger.recordOutput("Viz/GlobalEstPose", estPose); + Logger.recordOutput("Viz/CameraPoses", Arrays.stream(Constants.Vision.cameraConstants) + .map(consts -> robotPose.plus(consts.robotToCamera)).toArray(Pose3d[]::new)); + } + +} diff --git a/vendordeps/ChoreoLib-2025.0.3.json b/vendordeps/ChoreoLib-2025.0.3.json deleted file mode 100644 index 5a8cd54..0000000 --- a/vendordeps/ChoreoLib-2025.0.3.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "fileName": "ChoreoLib-2025.0.3.json", - "name": "ChoreoLib", - "version": "2025.0.3", - "uuid": "b5e23f0a-dac9-4ad2-8dd6-02767c520aca", - "frcYear": "2025", - "mavenUrls": [ - "https://lib.choreo.autos/dep", - "https://repo1.maven.org/maven2" - ], - "jsonUrl": "https://lib.choreo.autos/dep/ChoreoLib2025.json", - "javaDependencies": [ - { - "groupId": "choreo", - "artifactId": "ChoreoLib-java", - "version": "2025.0.3" - }, - { - "groupId": "com.google.code.gson", - "artifactId": "gson", - "version": "2.11.0" - } - ], - "jniDependencies": [], - "cppDependencies": [ - { - "groupId": "choreo", - "artifactId": "ChoreoLib-cpp", - "version": "2025.0.3", - "libName": "ChoreoLib", - "headerClassifier": "headers", - "sharedLibrary": false, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal", - "linuxathena", - "linuxarm32", - "linuxarm64" - ] - } - ] -} \ No newline at end of file diff --git a/vendordeps/ChoreoLib2025.json b/vendordeps/ChoreoLib2025.json new file mode 100644 index 0000000..2c2d3c3 --- /dev/null +++ b/vendordeps/ChoreoLib2025.json @@ -0,0 +1,44 @@ +{ + "fileName": "ChoreoLib2025.json", + "name": "ChoreoLib", + "version": "2025.0.3", + "uuid": "b5e23f0a-dac9-4ad2-8dd6-02767c520aca", + "frcYear": "2025", + "mavenUrls": [ + "https://lib.choreo.autos/dep", + "https://repo1.maven.org/maven2" + ], + "jsonUrl": "https://lib.choreo.autos/dep/ChoreoLib2025.json", + "javaDependencies": [ + { + "groupId": "choreo", + "artifactId": "ChoreoLib-java", + "version": "2025.0.3" + }, + { + "groupId": "com.google.code.gson", + "artifactId": "gson", + "version": "2.11.0" + } + ], + "jniDependencies": [], + "cppDependencies": [ + { + "groupId": "choreo", + "artifactId": "ChoreoLib-cpp", + "version": "2025.0.3", + "libName": "ChoreoLib", + "headerClassifier": "headers", + "sharedLibrary": false, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal", + "linuxathena", + "linuxarm32", + "linuxarm64" + ] + } + ] +} diff --git a/vendordeps/ReduxLib-2025.0.1.json b/vendordeps/ReduxLib-2025.0.1.json new file mode 100644 index 0000000..6cc750e --- /dev/null +++ b/vendordeps/ReduxLib-2025.0.1.json @@ -0,0 +1,72 @@ +{ + "fileName": "ReduxLib-2025.0.1.json", + "name": "ReduxLib", + "version": "2025.0.1", + "frcYear": "2025", + "uuid": "151ecca8-670b-4026-8160-cdd2679ef2bd", + "mavenUrls": [ + "https://maven.reduxrobotics.com/" + ], + "jsonUrl": "https://frcsdk.reduxrobotics.com/ReduxLib_2025.json", + "javaDependencies": [ + { + "groupId": "com.reduxrobotics.frc", + "artifactId": "ReduxLib-java", + "version": "2025.0.1" + } + ], + "jniDependencies": [ + { + "groupId": "com.reduxrobotics.frc", + "artifactId": "ReduxLib-driver", + "version": "2025.0.1", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "linuxathena", + "linuxx86-64", + "linuxarm32", + "linuxarm64", + "osxuniversal", + "windowsx86-64" + ] + } + ], + "cppDependencies": [ + { + "groupId": "com.reduxrobotics.frc", + "artifactId": "ReduxLib-cpp", + "version": "2025.0.1", + "libName": "ReduxLib", + "headerClassifier": "headers", + "sourcesClassifier": "sources", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "linuxathena", + "linuxx86-64", + "linuxarm32", + "linuxarm64", + "osxuniversal", + "windowsx86-64" + ] + }, + { + "groupId": "com.reduxrobotics.frc", + "artifactId": "ReduxLib-driver", + "version": "2025.0.1", + "libName": "ReduxCore", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "linuxathena", + "linuxx86-64", + "linuxarm32", + "linuxarm64", + "osxuniversal", + "windowsx86-64" + ] + } + ] +} \ No newline at end of file diff --git a/vendordeps/Studica-2025.0.1.json b/vendordeps/Studica-2025.0.1.json new file mode 100644 index 0000000..5010be0 --- /dev/null +++ b/vendordeps/Studica-2025.0.1.json @@ -0,0 +1,71 @@ +{ + "fileName": "Studica-2025.0.1.json", + "name": "Studica", + "version": "2025.0.1", + "uuid": "cb311d09-36e9-4143-a032-55bb2b94443b", + "frcYear": "2025", + "mavenUrls": [ + "https://dev.studica.com/maven/release/2025/" + ], + "jsonUrl": "https://dev.studica.com/releases/2025/Studica-2025.0.1.json", + "cppDependencies": [ + { + "artifactId": "Studica-cpp", + "binaryPlatforms": [ + "linuxathena", + "linuxarm32", + "linuxarm64", + "linuxx86-64", + "osxuniversal", + "windowsx86-64" + ], + "groupId": "com.studica.frc", + "headerClassifier": "headers", + "libName": "Studica", + "sharedLibrary": false, + "skipInvalidPlatforms": true, + "version": "2025.0.1" + }, + { + "artifactId": "Studica-driver", + "binaryPlatforms": [ + "linuxathena", + "linuxarm32", + "linuxarm64", + "linuxx86-64", + "osxuniversal", + "windowsx86-64" + ], + "groupId": "com.studica.frc", + "headerClassifier": "headers", + "libName": "StudicaDriver", + "sharedLibrary": false, + "skipInvalidPlatforms": true, + "version": "2025.0.1" + } + ], + "javaDependencies": [ + { + "artifactId": "Studica-java", + "groupId": "com.studica.frc", + "version": "2025.0.1" + } + ], + "jniDependencies": [ + { + "artifactId": "Studica-driver", + "groupId": "com.studica.frc", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "linuxathena", + "linuxarm32", + "linuxarm64", + "linuxx86-64", + "osxuniversal", + "windowsx86-64" + ], + "version": "2025.0.1" + } + ] +} \ No newline at end of file diff --git a/vendordeps/photonlib.json b/vendordeps/photonlib.json new file mode 100644 index 0000000..5c6484d --- /dev/null +++ b/vendordeps/photonlib.json @@ -0,0 +1,71 @@ +{ + "fileName": "photonlib.json", + "name": "photonlib", + "version": "v2025.3.1", + "uuid": "515fe07e-bfc6-11fa-b3de-0242ac130004", + "frcYear": "2025", + "mavenUrls": [ + "https://maven.photonvision.org/repository/internal", + "https://maven.photonvision.org/repository/snapshots" + ], + "jsonUrl": "https://maven.photonvision.org/repository/internal/org/photonvision/photonlib-json/1.0/photonlib-json-1.0.json", + "jniDependencies": [ + { + "groupId": "org.photonvision", + "artifactId": "photontargeting-cpp", + "version": "v2025.3.1", + "skipInvalidPlatforms": true, + "isJar": false, + "validPlatforms": [ + "windowsx86-64", + "linuxathena", + "linuxx86-64", + "osxuniversal" + ] + } + ], + "cppDependencies": [ + { + "groupId": "org.photonvision", + "artifactId": "photonlib-cpp", + "version": "v2025.3.1", + "libName": "photonlib", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxathena", + "linuxx86-64", + "osxuniversal" + ] + }, + { + "groupId": "org.photonvision", + "artifactId": "photontargeting-cpp", + "version": "v2025.3.1", + "libName": "photontargeting", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxathena", + "linuxx86-64", + "osxuniversal" + ] + } + ], + "javaDependencies": [ + { + "groupId": "org.photonvision", + "artifactId": "photonlib-java", + "version": "v2025.3.1" + }, + { + "groupId": "org.photonvision", + "artifactId": "photontargeting-java", + "version": "v2025.3.1" + } + ] +} \ No newline at end of file From b70e718c946409e16cd7b5592283b75ef167933e Mon Sep 17 00:00:00 2001 From: wilsonwatson Date: Sun, 4 Jan 2026 16:58:55 -0600 Subject: [PATCH 2/5] update constants to match 2025 bot for testing --- src/main/java/frc/robot/Constants.java | 49 +++++++++++++------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/main/java/frc/robot/Constants.java b/src/main/java/frc/robot/Constants.java index 5c5e13f..ca2539b 100644 --- a/src/main/java/frc/robot/Constants.java +++ b/src/main/java/frc/robot/Constants.java @@ -11,6 +11,7 @@ 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 edu.wpi.first.math.util.Units; import edu.wpi.first.units.measure.Distance; @@ -57,22 +58,20 @@ public static class SwerveTransformPID { */ public static final class Swerve { /** If true, motors and absolute encoders are on canivore loop. Otherwise on rio. */ - public static final boolean isCanviore = false; + public static final boolean isCanviore = true; public static final NavXComType navXID = NavXComType.kMXP_SPI; - public static final boolean invertGyro = true; - public static final boolean isFieldRelative = true; - public static final boolean isOpenLoop = false; + public static final boolean invertGyro = false; /* Drivetrain Constants */ - public static final double trackWidth = Units.inchesToMeters(23.75); - public static final double wheelBase = Units.inchesToMeters(17.75); + public static final double trackWidth = Units.inchesToMeters(24.229); + public static final double wheelBase = Units.inchesToMeters(24.229); public static final Distance wheelDiameter = Inches.of(3.8); public static final Distance wheelCircumference = wheelDiameter.times(Math.PI); public static final Distance wheelRadius = wheelDiameter.div(2); - public static final Distance bumperFront = Inches.of(15); - public static final Distance bumperRight = Inches.of(15); + public static final Distance bumperFront = Inches.of(17.5); + public static final Distance bumperRight = Inches.of(17.5); public static final Translation2d[] swerveTranslations = new Translation2d[] {new Translation2d(wheelBase / 2.0, trackWidth / 2.0), @@ -162,31 +161,31 @@ public static final class Swerve { public static final ModuleConstants[] modulesConstants = new ModuleConstants[] { // Front Left Module new ModuleConstantsBuilder() - .driveMotorId(6) - .angleMotorId(51) - .canCoderId(4) - .angleOffset(Rotation2d.fromRotations(-0.496826)) + .driveMotorId(2) + .angleMotorId(1) + .canCoderId(1) + .angleOffset(Rotation2d.fromRotations(0.008789)) .finish(), // Front Right Module new ModuleConstantsBuilder() - .driveMotorId(2) - .angleMotorId(40) + .driveMotorId(9) + .angleMotorId(8) .canCoderId(2) - .angleOffset(Rotation2d.fromRotations(0.405518 + 0.5)) + .angleOffset(Rotation2d.fromRotations(-0.301758)) .finish(), // Back Left Module new ModuleConstantsBuilder() - .driveMotorId(3) - .angleMotorId(9) - .canCoderId(1) - .angleOffset(Rotation2d.fromRotations(0.348145)) + .driveMotorId(0) + .angleMotorId(19) + .canCoderId(4) + .angleOffset(Rotation2d.fromRotations(-0.451172)) .finish(), // Back Right Module new ModuleConstantsBuilder() - .driveMotorId(10) - .angleMotorId(8) - .canCoderId(10) - .angleOffset(Rotation2d.fromRotations(0.317627 + 0.5)) + .driveMotorId(11) + .angleMotorId(10) + .canCoderId(3) + .angleOffset(Rotation2d.fromRotations(0.321777)) .finish(), }; // @formatter:on @@ -209,7 +208,9 @@ public static final class Vision { .simLatencyStdDev(0.02) .calibrationErrorMean(0.8) .calibrationErrorStdDev(0.08) - .robotToCamera(new Transform3d(0, 0, 0, new Rotation3d(Units.degreesToRadians(180), 0, 0))) + .robotToCamera(new Transform3d(new Translation3d(Units.inchesToMeters(11), + -Units.inchesToMeters(12), Units.inchesToMeters(10)), + new Rotation3d(Math.PI, 0, 0))) .translationError(0.02) .finish(), }; From ffaf918ad5ccb16d7bbca4d76ef1296e6940c2e9 Mon Sep 17 00:00:00 2001 From: wilsonwatson Date: Sun, 4 Jan 2026 17:02:36 -0600 Subject: [PATCH 3/5] update checks --- checks.xml | 339 +++++++++++++++++++++++++++-------------------------- 1 file changed, 170 insertions(+), 169 deletions(-) diff --git a/checks.xml b/checks.xml index 67cbaef..5845c26 100644 --- a/checks.xml +++ b/checks.xml @@ -1,7 +1,5 @@ - + - - + + - + - + - + - + default="checkstyle-suppressions.xml" /> + - + - - + + - + - + - + + value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)" /> + value="Consider using special escape sequence instead of octal value or Unicode escaped value." /> - - - + + + - - + + - + - + + value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH" /> + value="LITERAL_DO, LITERAL_ELSE, LITERAL_FOR, LITERAL_IF, LITERAL_WHILE" /> + OBJBLOCK, STATIC_INIT, RECORD_DEF, COMPACT_CTOR_DEF" /> - + + value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, + LITERAL_DO" /> - - + + + COMPACT_CTOR_DEF" /> - - + + + value="COMMA, SEMI, TYPECAST, LITERAL_IF, LITERAL_ELSE, + LITERAL_WHILE, LITERAL_DO, LITERAL_FOR, DO_WHILE" /> - - - - - - + + + + + + + SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT, TYPE_EXTENSION_AND" /> + value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)" /> - - - - - - - - + value="WhitespaceAround: ''{0}'' is not preceded with whitespace." /> + + + + + + + + - + COMPACT_CTOR_DEF" /> + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - + + value="Package name ''{0}'' must match pattern ''{1}''." /> - - + + + value="Type name ''{0}'' must match pattern ''{1}''." /> - + + value="Member name ''{0}'' must match pattern ''{1}''." /> - + + value="Parameter name ''{0}'' must match pattern ''{1}''." /> - + + value="Lambda parameter name ''{0}'' must match pattern ''{1}''." /> - + + value="Catch parameter name ''{0}'' must match pattern ''{1}''." /> - + + value="Local variable name ''{0}'' must match pattern ''{1}''." /> - + + value="Pattern variable name ''{0}'' must match pattern ''{1}''." /> - + + value="Class type name ''{0}'' must match pattern ''{1}''." /> - + + value="Record component name ''{0}'' must match pattern ''{1}''." /> - + + value="Record type name ''{0}'' must match pattern ''{1}''." /> - + + value="Method type name ''{0}'' must match pattern ''{1}''." /> - + + value="Interface type name ''{0}'' must match pattern ''{1}''." /> - + + value="GenericWhitespace ''{0}'' is followed by whitespace." /> + value="GenericWhitespace ''{0}'' is preceded with whitespace." /> + value="GenericWhitespace ''{0}'' should followed by whitespace." /> + value="GenericWhitespace ''{0}'' is not preceded with whitespace." /> - - - - - - + + + + + + - - + + + RECORD_COMPONENT_DEF" /> - - + + - + + value="CTOR_DEF, LITERAL_NEW, METHOD_CALL, METHOD_DEF, + SUPER_CTOR_CALL, ENUM_CONSTANT_DEF, RECORD_DEF" /> - + value="COMMA, SEMI, POST_INC, POST_DEC, DOT, + LABELED_STAT, METHOD_REF" /> + + RECORD_DEF" /> - + + TYPE_EXTENSION_AND " /> - + + value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, + RECORD_DEF, COMPACT_CTOR_DEF" /> - - - + + + - - - + + + - + - + - + - + + value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF" /> - - - - - + + + + + - - - - + + + + - + - + value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, + RECORD_DEF, ANNOTATION_DEF" /> + - + + value="Method name ''{0}'' must match pattern ''{1}''." /> - + - + - + - + default="checkstyle-xpath-suppressions.xml" /> + - + \ No newline at end of file From 8b5aa07279573d186558bac9221c00619ceae284 Mon Sep 17 00:00:00 2001 From: Wilson Watson <8907513+wilsonwatson@users.noreply.github.com> Date: Tue, 6 Jan 2026 23:42:32 -0600 Subject: [PATCH 4/5] Tested on 2025 bot --- src/main/java/frc/robot/Constants.java | 14 +++++++------- src/main/java/frc/robot/RobotContainer.java | 3 +++ .../robot/subsystems/swerve/gyro/GyroNavX2.java | 16 +++++++++------- .../subsystems/swerve/mod/SwerveModuleReal.java | 12 +++++++----- .../swerve/util/PhoenixOdometryThread.java | 2 +- .../swerve/util/SwerveRateLimiter.java | 1 + .../subsystems/swerve/util/TuningCommands.java | 3 ++- src/main/java/frc/robot/util/PhoenixSignals.java | 1 + 8 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/main/java/frc/robot/Constants.java b/src/main/java/frc/robot/Constants.java index ca2539b..ce41608 100644 --- a/src/main/java/frc/robot/Constants.java +++ b/src/main/java/frc/robot/Constants.java @@ -61,14 +61,14 @@ public static final class Swerve { public static final boolean isCanviore = true; public static final NavXComType navXID = NavXComType.kMXP_SPI; - public static final boolean invertGyro = false; + public static final boolean invertGyro = true; /* Drivetrain Constants */ public static final double trackWidth = Units.inchesToMeters(24.229); public static final double wheelBase = Units.inchesToMeters(24.229); - public static final Distance wheelDiameter = Inches.of(3.8); + public static final Distance wheelRadius = Inches.of(1.913); + public static final Distance wheelDiameter = wheelRadius.times(2); public static final Distance wheelCircumference = wheelDiameter.times(Math.PI); - public static final Distance wheelRadius = wheelDiameter.div(2); public static final Distance bumperFront = Inches.of(17.5); public static final Distance bumperRight = Inches.of(17.5); @@ -123,15 +123,15 @@ public static final class Swerve { public static final double angleKD = 0.0; /* Drive Motor PID Values */ - public static final double driveKP = 0.12; + public static final double driveKP = 0.0012; public static final double driveKI = 0.0; public static final double driveKD = 0.0; public static final double driveKF = 0.0; /* Drive Motor Characterization Values From SYSID */ - public static final double driveKS = 0.32; - public static final double driveKV = 1.51; - public static final double driveKA = 0.27; + public static final double driveKS = 0.185; + public static final double driveKV = 1.004 / 6.536; + public static final double driveKA = 0.0; /* Swerve Profiling Values */ /** Meters per Second */ diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java index 5bce154..9a3c3c5 100644 --- a/src/main/java/frc/robot/RobotContainer.java +++ b/src/main/java/frc/robot/RobotContainer.java @@ -71,6 +71,9 @@ public RobotContainer(RobotRunType runtimeType) { () -> -driver.getLeftY(), () -> -driver.getLeftX(), () -> -driver.getRightX()))); driver.y().onTrue(swerve.setFieldRelativeOffset()); + + driver.a().whileTrue(swerve.wheelRadiusCharacterization()).onFalse(swerve.emergencyStop()); + driver.b().whileTrue(swerve.feedforwardCharacterization()).onFalse(swerve.emergencyStop()); } /** Runs once per 0.02 seconds after subsystems and commands. */ diff --git a/src/main/java/frc/robot/subsystems/swerve/gyro/GyroNavX2.java b/src/main/java/frc/robot/subsystems/swerve/gyro/GyroNavX2.java index 486e812..bca65c0 100644 --- a/src/main/java/frc/robot/subsystems/swerve/gyro/GyroNavX2.java +++ b/src/main/java/frc/robot/subsystems/swerve/gyro/GyroNavX2.java @@ -25,16 +25,18 @@ public GyroNavX2(PhoenixOdometryThread odometryThread) { public void updateInputs(GyroInputs inputs) { inputs.connected = gyro.isConnected(); - inputs.yaw = Rotation2d.fromDegrees(gyro.getYaw()); - inputs.yawVelocityRadPerSec = Units.degreesToRadians(gyro.getRawGyroZ()); + double invert = Constants.Swerve.invertGyro ? -1.0 : 1.0; - inputs.pitch = Rotation2d.fromDegrees(gyro.getPitch()); - inputs.pitchVelocityRadPerSec = Units.degreesToRadians(gyro.getRawGyroX()); + inputs.yaw = Rotation2d.fromDegrees(invert * gyro.getYaw()); + inputs.yawVelocityRadPerSec = invert * Units.degreesToRadians(gyro.getRawGyroZ()); - inputs.roll = Rotation2d.fromDegrees(gyro.getRoll()); - inputs.rollVelocityRadPerSec = Units.degreesToRadians(gyro.getRawGyroY()); + inputs.pitch = Rotation2d.fromDegrees(invert * gyro.getPitch()); + inputs.pitchVelocityRadPerSec = invert * Units.degreesToRadians(gyro.getRawGyroX()); - inputs.yawRads = yawQueue.stream().mapToDouble(x -> x).toArray(); + inputs.roll = Rotation2d.fromDegrees(invert * gyro.getRoll()); + inputs.rollVelocityRadPerSec = invert * Units.degreesToRadians(gyro.getRawGyroY()); + + inputs.yawRads = yawQueue.stream().mapToDouble(x -> invert * x).toArray(); yawQueue.clear(); } diff --git a/src/main/java/frc/robot/subsystems/swerve/mod/SwerveModuleReal.java b/src/main/java/frc/robot/subsystems/swerve/mod/SwerveModuleReal.java index dd79368..5dfe773 100644 --- a/src/main/java/frc/robot/subsystems/swerve/mod/SwerveModuleReal.java +++ b/src/main/java/frc/robot/subsystems/swerve/mod/SwerveModuleReal.java @@ -95,11 +95,13 @@ public SwerveModuleReal(int index, PhoenixOdometryThread odometryThread) { configAngleEncoder(); // Configure periodic frames - BaseStatusSignal.setUpdateFrequencyForAll(Constants.Swerve.odometryFrequency, drivePosition, - anglePosition); - BaseStatusSignal.setUpdateFrequencyForAll(50.0, driveVelocity, driveAppliedVolts, - driveSupplyCurrentAmps, driveStatorCurrentAmps, angleVelocity, angleAppliedVolts, - angleSupplyCurrentAmps, angleStatorCurrentAmps); + PhoenixSignals.tryUntilOk(5, + () -> BaseStatusSignal.setUpdateFrequencyForAll(Constants.Swerve.odometryFrequency, + drivePosition, anglePosition)); + PhoenixSignals.tryUntilOk(5, + () -> BaseStatusSignal.setUpdateFrequencyForAll(50.0, driveVelocity, driveAppliedVolts, + driveSupplyCurrentAmps, driveStatorCurrentAmps, angleVelocity, angleAppliedVolts, + angleSupplyCurrentAmps, angleStatorCurrentAmps)); PhoenixSignals.tryUntilOk(5, () -> ParentDevice.optimizeBusUtilizationForAll(driveMotor, angleMotor, absoluteEncoder)); diff --git a/src/main/java/frc/robot/subsystems/swerve/util/PhoenixOdometryThread.java b/src/main/java/frc/robot/subsystems/swerve/util/PhoenixOdometryThread.java index 5e0accc..7036005 100644 --- a/src/main/java/frc/robot/subsystems/swerve/util/PhoenixOdometryThread.java +++ b/src/main/java/frc/robot/subsystems/swerve/util/PhoenixOdometryThread.java @@ -55,7 +55,7 @@ public class PhoenixOdometryThread extends Thread { private final Lock signalsLock = new ReentrantLock(); /** Registered Phoenix status signals */ - private BaseStatusSignal[] phoenixSignals; + private BaseStatusSignal[] phoenixSignals = new BaseStatusSignal[0]; /** Registered non-Phoenix signal suppliers */ private final List genericSignals = new ArrayList<>(); /** Output queues corresponding to Phoenix signals */ diff --git a/src/main/java/frc/robot/subsystems/swerve/util/SwerveRateLimiter.java b/src/main/java/frc/robot/subsystems/swerve/util/SwerveRateLimiter.java index 540a1b0..ba4d6a5 100644 --- a/src/main/java/frc/robot/subsystems/swerve/util/SwerveRateLimiter.java +++ b/src/main/java/frc/robot/subsystems/swerve/util/SwerveRateLimiter.java @@ -161,6 +161,7 @@ public ChassisSpeeds limit(ChassisSpeeds wantedSpeedsRobotRelative) { wantedVel.a2 = wantedSpeedsRobotRelative.vyMetersPerSecond; wantedVel.a3 = wantedSpeedsRobotRelative.omegaRadiansPerSecond; publish("wantedVel", wantedVel); + publish("currentVel", currentVel); CommonOps_DDF3.subtract(wantedVel, currentVel, wantedAcc); CommonOps_DDF3.divide(wantedAcc, 0.02); diff --git a/src/main/java/frc/robot/subsystems/swerve/util/TuningCommands.java b/src/main/java/frc/robot/subsystems/swerve/util/TuningCommands.java index aaeb89d..0dcdbd1 100644 --- a/src/main/java/frc/robot/subsystems/swerve/util/TuningCommands.java +++ b/src/main/java/frc/robot/subsystems/swerve/util/TuningCommands.java @@ -12,6 +12,7 @@ import edu.wpi.first.math.filter.SlewRateLimiter; import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.math.kinematics.ChassisSpeeds; +import edu.wpi.first.math.util.Units; import edu.wpi.first.wpilibj.Timer; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.Commands; @@ -209,7 +210,7 @@ public static Command wheelRadiusCharacterization(Swerve swerve, double wheelRadius = (state.gyroDelta * driveBaseRadius) / wheelDelta; Logger.recordOutput("Drive/WheelDelta", wheelDelta); - Logger.recordOutput("Drive/WheelRadius", wheelRadius); + Logger.recordOutput("Drive/WheelRadius", Units.metersToInches(wheelRadius)); }))); } diff --git a/src/main/java/frc/robot/util/PhoenixSignals.java b/src/main/java/frc/robot/util/PhoenixSignals.java index cc387b4..a77793a 100644 --- a/src/main/java/frc/robot/util/PhoenixSignals.java +++ b/src/main/java/frc/robot/util/PhoenixSignals.java @@ -42,6 +42,7 @@ public static void tryUntilOk(int maxAttempts, Supplier command) { if (error.isOK()) { break; } + System.err.println("Phoenix Error: " + error.toString()); } } From 8ec5109e5646dc87e61b34c69055a60f05ec6581 Mon Sep 17 00:00:00 2001 From: Watson Date: Wed, 7 Jan 2026 09:08:50 -0600 Subject: [PATCH 5/5] update to newest RobotTools --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7ae8be7..541fbe4 100644 --- a/build.gradle +++ b/build.gradle @@ -82,7 +82,7 @@ dependencies { nativeRelease wpi.java.vendor.jniRelease(wpi.platforms.desktop) simulationRelease wpi.sim.enableRelease() - annotationProcessor 'com.github.Frc5572:RobotTools:io-gen-SNAPSHOT' + annotationProcessor 'com.github.Frc5572:RobotTools:v2.0' implementation 'org.jspecify:jspecify:1.0.0' implementation 'org.apache.httpcomponents:httpclient:4.5.14'