diff --git a/README.md b/README.md index 258070a..56e8138 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ Arizona's Reference Build and Software Implementation for FRC Robots (read: "A-Z ## Installation -Installation instructions are found in the [INSTALL.md](INSTALL.md) file, and the [Getting -Started Guide](RBSI-GSG.md) includes the steps you'll need to do before taking your robot +Installation instructions are found in the [INSTALL.md](doc/INSTALL.md) file, and the [Getting +Started Guide](doc/RBSI-GSG.md) includes the steps you'll need to do before taking your robot out for a spin. See the [Releases Page](https://github.com/AZ-First/Az-RBSI/releases) for details on the latest release, including restrictions and cautions. @@ -29,7 +29,7 @@ The purpose of Az-RBSI is to help Arizona FRC teams with: ## Design Philosophy The Az-RBSI is centered around a "Reference Build" robot that allows for teams -to communicate quickly and effectivly with each other about gameplay strategy +to communicate quickly and effectively with each other about gameplay strategy and troubleshooting. Additionally, the consolidation around a standard robot design allows for easier swapping of spare parts and programming modules. @@ -44,11 +44,11 @@ effective logging for troubleshooting. * [WPILib](https://docs.wpilib.org/en/stable/index.html) -- FIRST basic libraries * [AdvantageKit]( - https://github.com/Mechanical-Advantage/AdvantageKit/blob/main/docs/WHAT-IS-ADVANTAGEKIT.md) + https://docs.advantagekit.org/getting-start ed/what-is-advantagekit/) -- Logging * [CTRE Phoenix6]( https://v6.docs.ctr-electronics.com/en/stable/docs/api-reference/mechanisms/swerve/swerve-overview.html) - / [YAGSL](https://yagsl.gitbook.io/yagsl) -- Swerve drive library + / [YAGSL](https://docs.yagsl.com/) -- Swerve drive library * [PathPlanner](https://pathplanner.dev/home.html) / [Choreo]( https://sleipnirgroup.github.io/Choreo/) -- Autonomous path planning * [PhotonVision](https://docs.photonvision.org/en/latest/) / [Limelight]( diff --git a/build.gradle b/build.gradle index 12dc3f3..4df38fa 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,10 @@ plugins { id "java" - id "edu.wpi.first.GradleRIO" version "2026.1.1" + id "edu.wpi.first.GradleRIO" version "2026.2.1" id "com.peterabeles.gversion" version "1.10.3" - id "com.diffplug.spotless" version "8.0.+" - id "io.freefair.lombok" version "9.1.+" - id "com.google.protobuf" version "0.9.+" + id "com.diffplug.spotless" version "8.1.0" + id "io.freefair.lombok" version "9.2.0" + id "com.google.protobuf" version "0.9.6" } java { @@ -64,7 +64,7 @@ def deployArtifact = deploy.targets.roborio.artifacts.frcJava wpi.java.debugJni = false // Set this to true to enable desktop support. -def includeDesktopSupport = false +def includeDesktopSupport = true // Defining my dependencies. In this case, WPILib (+ friends), and vendor libraries. // Also defines JUnit 5. diff --git a/INSTALL.md b/doc/INSTALL.md similarity index 86% rename from INSTALL.md rename to doc/INSTALL.md index c9ba218..a1a4518 100644 --- a/INSTALL.md +++ b/doc/INSTALL.md @@ -13,17 +13,24 @@ already have a GitHub account where you will store your 2026 FRC robot code. ### Creating a 2026 FRC project from the Az-RBSI Template -From the [Az-RBSI GiuHub page](https://github.com/AZ-First/Az-RBSI/), click the "Use this template" button in the upper right corner of the page. +From the [Az-RBSI GiuHub page](https://github.com/AZ-First/Az-RBSI/), click the +"Use this template" button in the upper right corner of the page. In the page that opens, select the Owner (most likely your team's account) and -Repository name (*e.g.*, "FRC-2026" or "REBUILT Robot Code" or whatever your team's naming convention -is) into which the create the new robot project. Optionally, include a -description of the repository for your reference. Select "public" or "private" -repository based on the usual practices of your team. +Repository name (*e.g.*, "FRC-2026" or "REBUILT Robot Code" or whatever your +team's naming convention is) into which the create the new robot project. +Optionally, include a description of the repository for your reference. Select +"public" or "private" repository based on the usual practices of your team. The latest release of Az-RBSI is in the `main` (default) branch, so it is recommended to **not** select the "Include all branches" checkbox. +If you want to keep caught up on dependencies, you will need to ENABLE the +Dependency Graph selection under the "Advanced Security" tab of the repository +Settings. + +Enable Dependency Graph + -------- ### Software Requirements @@ -32,22 +39,22 @@ The Az-RBSI requires the [2026 WPILib Installer]( https://github.com/wpilibsuite/allwpilib/releases) (VSCode and associated tools), 2026 firmware installed on all hardware (motors, encoders, power distribution, etc.), the [2026 NI FRC Game Tools]( -https://github.com/wpilibsuite/2026Beta) +https://www.ni.com/en/support/downloads/drivers/download.frc-game-tools.html) (Driver Station and associated tools), and the [2026 CTRE Phoenix Tuner X]( https://v6.docs.ctr-electronics.com/en/stable/docs/tuner/index.html). Take a -moment to update all software and firmware before attempting to load your new -robot project. +moment to update all software and firmware to the latest versions before +attempting to load your new robot project. Please note that you need these _minimum_ versions of the following components: -* WPILib ` v2026.1.1-beta-1` +* WPILib ` v2026.2.1` * RoboRIO image `FRC_roboRIO_2026_v1.2` -------- ### Setting up your new project -When your new robot code respository is created, it will have a single commit +When your new robot code repository is created, it will have a single commit that contains the entire Az-RBSI template for the current release. (See the [Az-RBSI Releases page](https://github.com/AZ-First/Az-RBSI/releases) for more information about the latest release.) @@ -101,6 +108,12 @@ method are encouraged to submit bug reports and code fixes to the [Az-RBSI repository](https://github.com/AZ-First/Az-RBSI). +-------- + +### Getting Started with your Robot Code + +See the Az-RBSI [Getting Started Guide](RBSI-GSG.md) for next steps. + -------- ### Updating your project based on the latest released version of Az-RBSI diff --git a/RBSI-GSG.md b/doc/RBSI-GSG.md similarity index 69% rename from RBSI-GSG.md rename to doc/RBSI-GSG.md index 7c41bd8..11d93d4 100644 --- a/RBSI-GSG.md +++ b/doc/RBSI-GSG.md @@ -41,10 +41,16 @@ modifications to extant RBSI code will be done to files within the ### Tuning constants for optimal performance -4. HHHH - +4. Over the course of your robot project, you will need to tune PID parameters + for both your drivebase and any mechanisms you build to play the game. + AdvantageKit includes detailed instructions for how to tune the various + portions of your drivetrain, and we **STRONGLY RECOMMEND** you work through + these steps **BEFORE** running your robot. + * [Tuning for drivebase with CTRE components](https://docs.advantagekit.org/getting-started/template-projects/talonfx-swerve-template#tuning) + * [Tuning for drivebase with REV components](https://docs.advantagekit.org/getting-started/template-projects/spark-swerve-template/#tuning) + Similar tuning can be done with subsystem components (flywheel, intake, etc.). 5. Power monitoring by subsystem is included in the Az-RBSI. In order to properly match subsystems to ports on your Power Distribution Module, @@ -55,12 +61,17 @@ modifications to extant RBSI code will be done to files within the instantiation]( https://github.com/AZ-First/Az-RBSI/blob/38f6391cb70c4caa90502710f591682815064677/src/main/java/frc/robot/RobotContainer.java#L154-L157) in the `RobotContainer.java` file. -6. All of the constants for needed for tuning your robot should be in the - `Constants.java` file in the `src/main/java/frc/robot` directory. This file - should be thoroughly edited to match the particulars of your robot. Be sure - to work through each section of this file and include the proper values for - your robot. - +6. In the `Constants.java` file, the classes following `RobotDevices` contain + individual containers for robot subsystems and interaction methods. The + `OperatorConstants` class determines how the OPERATOR interacts with the + robot. `DriveBaseConstants` and `FlywheelConstants` (and additional classes + you add for your own mechanisms) contain human-scale conversions and limits + for the subsystem (_e.g._, maximum speed, gear ratios, PID constants, etc.). + `AutoConstants` contains the values needed for your autonomous period method + of choice (currently supported are MANUAL -- you write your own code; + PATHPLANNER, and CHOREO). The next two are related to robot vision, where + the vision system constants are contained in `VisionConstants`, and the + physical properties (location, FOV, etc.) of the cameras are in `Cameras`. -------- diff --git a/doc/dependency_enable.png b/doc/dependency_enable.png new file mode 100644 index 0000000..81691d2 Binary files /dev/null and b/doc/dependency_enable.png differ diff --git a/src/main/deploy/apriltags/2026-none.json b/src/main/deploy/apriltags/2026-none.json new file mode 100644 index 0000000..4763fda --- /dev/null +++ b/src/main/deploy/apriltags/2026-none.json @@ -0,0 +1,7 @@ +{ + "tags": [], + "field": { + "length": 16.4592, + "width": 8.2296 + } +} diff --git a/src/main/deploy/apriltags/2026-official.json b/src/main/deploy/apriltags/2026-official.json new file mode 100644 index 0000000..8c5a52d --- /dev/null +++ b/src/main/deploy/apriltags/2026-official.json @@ -0,0 +1,584 @@ +{ + "tags": [ + { + "ID": 1, + "pose": { + "translation": { + "x": 11.8779798, + "y": 7.4247756, + "z": 0.889 + }, + "rotation": { + "quaternion": { + "W": 6.123233995736766e-17, + "X": 0.0, + "Y": 0.0, + "Z": 1.0 + } + } + } + }, + { + "ID": 2, + "pose": { + "translation": { + "x": 11.9154194, + "y": 4.638039999999999, + "z": 1.12395 + }, + "rotation": { + "quaternion": { + "W": 0.7071067811865476, + "X": 0.0, + "Y": 0.0, + "Z": 0.7071067811865476 + } + } + } + }, + { + "ID": 3, + "pose": { + "translation": { + "x": 11.3118646, + "y": 4.3902376, + "z": 1.12395 + }, + "rotation": { + "quaternion": { + "W": 6.123233995736766e-17, + "X": 0.0, + "Y": 0.0, + "Z": 1.0 + } + } + } + }, + { + "ID": 4, + "pose": { + "translation": { + "x": 11.3118646, + "y": 4.0346376, + "z": 1.12395 + }, + "rotation": { + "quaternion": { + "W": 6.123233995736766e-17, + "X": 0.0, + "Y": 0.0, + "Z": 1.0 + } + } + } + }, + { + "ID": 5, + "pose": { + "translation": { + "x": 11.9154194, + "y": 3.4312351999999997, + "z": 1.12395 + }, + "rotation": { + "quaternion": { + "W": -0.7071067811865475, + "X": -0.0, + "Y": 0.0, + "Z": 0.7071067811865476 + } + } + } + }, + { + "ID": 6, + "pose": { + "translation": { + "x": 11.8779798, + "y": 0.6444996, + "z": 0.889 + }, + "rotation": { + "quaternion": { + "W": 6.123233995736766e-17, + "X": 0.0, + "Y": 0.0, + "Z": 1.0 + } + } + } + }, + { + "ID": 7, + "pose": { + "translation": { + "x": 11.9528844, + "y": 0.6444996, + "z": 0.889 + }, + "rotation": { + "quaternion": { + "W": 1.0, + "X": 0.0, + "Y": 0.0, + "Z": 0.0 + } + } + } + }, + { + "ID": 8, + "pose": { + "translation": { + "x": 12.2710194, + "y": 3.4312351999999997, + "z": 1.12395 + }, + "rotation": { + "quaternion": { + "W": -0.7071067811865475, + "X": -0.0, + "Y": 0.0, + "Z": 0.7071067811865476 + } + } + } + }, + { + "ID": 9, + "pose": { + "translation": { + "x": 12.519177399999998, + "y": 3.6790375999999996, + "z": 1.12395 + }, + "rotation": { + "quaternion": { + "W": 1.0, + "X": 0.0, + "Y": 0.0, + "Z": 0.0 + } + } + } + }, + { + "ID": 10, + "pose": { + "translation": { + "x": 12.519177399999998, + "y": 4.0346376, + "z": 1.12395 + }, + "rotation": { + "quaternion": { + "W": 1.0, + "X": 0.0, + "Y": 0.0, + "Z": 0.0 + } + } + } + }, + { + "ID": 11, + "pose": { + "translation": { + "x": 12.2710194, + "y": 4.638039999999999, + "z": 1.12395 + }, + "rotation": { + "quaternion": { + "W": 0.7071067811865476, + "X": 0.0, + "Y": 0.0, + "Z": 0.7071067811865476 + } + } + } + }, + { + "ID": 12, + "pose": { + "translation": { + "x": 11.9528844, + "y": 7.4247756, + "z": 0.889 + }, + "rotation": { + "quaternion": { + "W": 1.0, + "X": 0.0, + "Y": 0.0, + "Z": 0.0 + } + } + } + }, + { + "ID": 13, + "pose": { + "translation": { + "x": 16.5333172, + "y": 7.4033126, + "z": 0.55245 + }, + "rotation": { + "quaternion": { + "W": 6.123233995736766e-17, + "X": 0.0, + "Y": 0.0, + "Z": 1.0 + } + } + } + }, + { + "ID": 14, + "pose": { + "translation": { + "x": 16.5333172, + "y": 6.9715126, + "z": 0.55245 + }, + "rotation": { + "quaternion": { + "W": 6.123233995736766e-17, + "X": 0.0, + "Y": 0.0, + "Z": 1.0 + } + } + } + }, + { + "ID": 15, + "pose": { + "translation": { + "x": 16.5329616, + "y": 4.3235626, + "z": 0.55245 + }, + "rotation": { + "quaternion": { + "W": 6.123233995736766e-17, + "X": 0.0, + "Y": 0.0, + "Z": 1.0 + } + } + } + }, + { + "ID": 16, + "pose": { + "translation": { + "x": 16.5329616, + "y": 3.8917626, + "z": 0.55245 + }, + "rotation": { + "quaternion": { + "W": 6.123233995736766e-17, + "X": 0.0, + "Y": 0.0, + "Z": 1.0 + } + } + } + }, + { + "ID": 17, + "pose": { + "translation": { + "x": 4.6630844, + "y": 0.6444996, + "z": 0.889 + }, + "rotation": { + "quaternion": { + "W": 1.0, + "X": 0.0, + "Y": 0.0, + "Z": 0.0 + } + } + } + }, + { + "ID": 18, + "pose": { + "translation": { + "x": 4.6256194, + "y": 3.4312351999999997, + "z": 1.12395 + }, + "rotation": { + "quaternion": { + "W": -0.7071067811865475, + "X": -0.0, + "Y": 0.0, + "Z": 0.7071067811865476 + } + } + } + }, + { + "ID": 19, + "pose": { + "translation": { + "x": 5.229174199999999, + "y": 3.6790375999999996, + "z": 1.12395 + }, + "rotation": { + "quaternion": { + "W": 1.0, + "X": 0.0, + "Y": 0.0, + "Z": 0.0 + } + } + } + }, + { + "ID": 20, + "pose": { + "translation": { + "x": 5.229174199999999, + "y": 4.0346376, + "z": 1.12395 + }, + "rotation": { + "quaternion": { + "W": 1.0, + "X": 0.0, + "Y": 0.0, + "Z": 0.0 + } + } + } + }, + { + "ID": 21, + "pose": { + "translation": { + "x": 4.6256194, + "y": 4.638039999999999, + "z": 1.12395 + }, + "rotation": { + "quaternion": { + "W": 0.7071067811865476, + "X": 0.0, + "Y": 0.0, + "Z": 0.7071067811865476 + } + } + } + }, + { + "ID": 22, + "pose": { + "translation": { + "x": 4.6630844, + "y": 7.4247756, + "z": 0.889 + }, + "rotation": { + "quaternion": { + "W": 1.0, + "X": 0.0, + "Y": 0.0, + "Z": 0.0 + } + } + } + }, + { + "ID": 23, + "pose": { + "translation": { + "x": 4.5881798, + "y": 7.4247756, + "z": 0.889 + }, + "rotation": { + "quaternion": { + "W": 6.123233995736766e-17, + "X": 0.0, + "Y": 0.0, + "Z": 1.0 + } + } + } + }, + { + "ID": 24, + "pose": { + "translation": { + "x": 4.2700194, + "y": 4.638039999999999, + "z": 1.12395 + }, + "rotation": { + "quaternion": { + "W": 0.7071067811865476, + "X": 0.0, + "Y": 0.0, + "Z": 0.7071067811865476 + } + } + } + }, + { + "ID": 25, + "pose": { + "translation": { + "x": 4.0218614, + "y": 4.3902376, + "z": 1.12395 + }, + "rotation": { + "quaternion": { + "W": 6.123233995736766e-17, + "X": 0.0, + "Y": 0.0, + "Z": 1.0 + } + } + } + }, + { + "ID": 26, + "pose": { + "translation": { + "x": 4.0218614, + "y": 4.0346376, + "z": 1.12395 + }, + "rotation": { + "quaternion": { + "W": 6.123233995736766e-17, + "X": 0.0, + "Y": 0.0, + "Z": 1.0 + } + } + } + }, + { + "ID": 27, + "pose": { + "translation": { + "x": 4.2700194, + "y": 3.4312351999999997, + "z": 1.12395 + }, + "rotation": { + "quaternion": { + "W": -0.7071067811865475, + "X": -0.0, + "Y": 0.0, + "Z": 0.7071067811865476 + } + } + } + }, + { + "ID": 28, + "pose": { + "translation": { + "x": 4.5881798, + "y": 0.6444996, + "z": 0.889 + }, + "rotation": { + "quaternion": { + "W": 6.123233995736766e-17, + "X": 0.0, + "Y": 0.0, + "Z": 1.0 + } + } + } + }, + { + "ID": 29, + "pose": { + "translation": { + "x": 0.0077469999999999995, + "y": 0.6659626, + "z": 0.55245 + }, + "rotation": { + "quaternion": { + "W": 1.0, + "X": 0.0, + "Y": 0.0, + "Z": 0.0 + } + } + } + }, + { + "ID": 30, + "pose": { + "translation": { + "x": 0.0077469999999999995, + "y": 1.0977626, + "z": 0.55245 + }, + "rotation": { + "quaternion": { + "W": 1.0, + "X": 0.0, + "Y": 0.0, + "Z": 0.0 + } + } + } + }, + { + "ID": 31, + "pose": { + "translation": { + "x": 0.0080772, + "y": 3.7457125999999996, + "z": 0.55245 + }, + "rotation": { + "quaternion": { + "W": 1.0, + "X": 0.0, + "Y": 0.0, + "Z": 0.0 + } + } + } + }, + { + "ID": 32, + "pose": { + "translation": { + "x": 0.0080772, + "y": 4.1775126, + "z": 0.55245 + }, + "rotation": { + "quaternion": { + "W": 1.0, + "X": 0.0, + "Y": 0.0, + "Z": 0.0 + } + } + } + } + ], + "field": { + "length": 16.541, + "width": 8.069 + } +} diff --git a/src/main/java/frc/robot/Constants.java b/src/main/java/frc/robot/Constants.java index 9b81736..1ebb197 100644 --- a/src/main/java/frc/robot/Constants.java +++ b/src/main/java/frc/robot/Constants.java @@ -18,7 +18,6 @@ package frc.robot; import static edu.wpi.first.units.Units.*; -import static frc.robot.util.RBSIEnum.*; import com.pathplanner.lib.config.ModuleConfig; import com.pathplanner.lib.config.PIDConstants; @@ -38,7 +37,15 @@ import frc.robot.subsystems.drive.Drive; import frc.robot.subsystems.drive.SwerveConstants; import frc.robot.util.Alert; +import frc.robot.util.RBSIEnum.AutoType; +import frc.robot.util.RBSIEnum.CTREPro; +import frc.robot.util.RBSIEnum.DriveStyle; +import frc.robot.util.RBSIEnum.Mode; +import frc.robot.util.RBSIEnum.MotorIdleMode; +import frc.robot.util.RBSIEnum.SwerveType; +import frc.robot.util.RBSIEnum.VisionType; import frc.robot.util.RobotDeviceId; +import org.photonvision.simulation.SimCameraProperties; import swervelib.math.Matter; /** @@ -64,7 +71,7 @@ public final class Constants { // under strict caveat emptor -- and submit any error and bugfixes // via GitHub issues. private static SwerveType swerveType = SwerveType.PHOENIX6; // PHOENIX6, YAGSL - private static CTREPro phoenixPro = CTREPro.LICENSED; // LICENSED, UNLICENSED + private static CTREPro phoenixPro = CTREPro.UNLICENSED; // LICENSED, UNLICENSED private static AutoType autoType = AutoType.MANUAL; // MANUAL, PATHPLANNER, CHOREO private static VisionType visionType = VisionType.NONE; // PHOTON, LIMELIGHT, NONE @@ -102,7 +109,7 @@ public static void disableHAL() { /** Physical Constants for Robot Operation ******************************* */ public static final class RobotConstants { - public static final Mass kRobotMass = Kilograms.of(100.); + public static final Mass kRobotMass = Pounds.of(100.); public static final Matter kChassis = new Matter(new Translation3d(0, 0, Inches.of(8).in(Meters)), kRobotMass.in(Kilograms)); // Robot moment of intertial; this can be obtained from a CAD model of your drivetrain. Usually, @@ -112,6 +119,10 @@ public static final class RobotConstants { // Wheel coefficient of friction public static final double kWheelCOF = 1.2; + // Maximum torque applied by wheel + // Kraken X60 stall torque ~7.09 Nm; MK4i L3 gear ratio 6.12:1 + public static final double kMaxWheelTorque = 43.4; // Nm + // Insert here the orientation (CCW == +) of the Rio and IMU from the robot // An angle of "0." means the x-y-z markings on the device match the robot's intrinsic reference // frame. @@ -421,10 +432,14 @@ public static class Cameras { // Robot to camera transforms // (ONLY USED FOR PHOTONVISION -- Limelight: configure in web UI instead) + // Example Camera are mounted on the frame perimeter, 18" up from the floor, centered + // side-to-side public static Transform3d robotToCamera0 = - new Transform3d(0.2, 0.0, 0.2, new Rotation3d(0.0, -0.4, 0.0)); + new Transform3d( + Inches.of(13.0), Inches.of(0), Inches.of(18.0), new Rotation3d(0.0, 0.0, 0.0)); public static Transform3d robotToCamera1 = - new Transform3d(-0.2, 0.0, 0.2, new Rotation3d(0.0, -0.4, Math.PI)); + new Transform3d( + Inches.of(-13.0), Inches.of(0), Inches.of(18.0), new Rotation3d(0.0, 0.0, Math.PI)); // Standard deviation multipliers for each camera // (Adjust to trust some cameras more than others) @@ -435,6 +450,32 @@ public static class Cameras { }; } + /************************************************************************* */ + /** Simulation Camera Properties ***************************************** */ + public static class SimCameras { + public static final SimCameraProperties kSimCamera1Props = + new SimCameraProperties() { + { + setCalibration(1280, 800, Rotation2d.fromDegrees(90)); + setCalibError(0.25, 0.08); + setFPS(30); + setAvgLatencyMs(20); + setLatencyStdDevMs(5); + } + }; + + public static final SimCameraProperties kSimCamera2Props = + new SimCameraProperties() { + { + setCalibration(1280, 800, Rotation2d.fromDegrees(90)); + setCalibError(0.25, 0.08); + setFPS(30); + setAvgLatencyMs(20); + setLatencyStdDevMs(5); + } + }; + } + /************************************************************************* */ /** Deploy Directoy Location Constants *********************************** */ public static final class DeployConstants { diff --git a/src/main/java/frc/robot/Robot.java b/src/main/java/frc/robot/Robot.java index ccfc193..83d5b19 100644 --- a/src/main/java/frc/robot/Robot.java +++ b/src/main/java/frc/robot/Robot.java @@ -18,6 +18,9 @@ package frc.robot; import com.revrobotics.util.StatusLogger; +import edu.wpi.first.apriltag.AprilTagFieldLayout; +import edu.wpi.first.apriltag.AprilTagFields; +import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.wpilibj.DriverStation; import edu.wpi.first.wpilibj.Threads; import edu.wpi.first.wpilibj.Timer; @@ -31,6 +34,10 @@ import org.littletonrobotics.junction.wpilog.WPILOGReader; import org.littletonrobotics.junction.wpilog.WPILOGWriter; import org.littletonrobotics.urcl.URCL; +import org.photonvision.PhotonCamera; +import org.photonvision.simulation.PhotonCameraSim; +import org.photonvision.simulation.SimCameraProperties; +import org.photonvision.simulation.VisionSystemSim; /** * The VM is configured to automatically run this class, and to call the functions corresponding to @@ -43,6 +50,9 @@ public class Robot extends LoggedRobot { private RobotContainer m_robotContainer; private Timer m_disabledTimer; + // Define simulation fields here + private VisionSystemSim visionSim; + /** * This function is run when the robot is first started up and should be used for any * initialization code. @@ -108,7 +118,9 @@ public Robot() { @Override public void robotPeriodic() { // Switch thread to high priority to improve loop timing - Threads.setCurrentThreadPriority(true, 99); + if (isReal()) { + Threads.setCurrentThreadPriority(true, 99); + } // Run all virtual subsystems each time through the loop VirtualSubsystem.periodicAll(); @@ -153,6 +165,10 @@ public void autonomousInit() { // TODO: Make sure Gyro inits here with whatever is in the path planning thingie switch (Constants.getAutoType()) { + case MANUAL: + CommandScheduler.getInstance().schedule(m_robotContainer.getManualAuto()); + break; + case PATHPLANNER: m_autoCommandPathPlanner = m_robotContainer.getAutonomousCommandPathPlanner(); // schedule the autonomous command @@ -160,6 +176,7 @@ public void autonomousInit() { CommandScheduler.getInstance().schedule(m_autoCommandPathPlanner); } break; + case CHOREO: m_robotContainer.getAutonomousCommandChoreo(); break; @@ -186,6 +203,9 @@ public void teleopInit() { CommandScheduler.getInstance().cancelAll(); } m_robotContainer.setMotorBrake(true); + + // In case this got set in sequential practice sessions or whatever + FieldState.wonAuto = null; } /** This function is called periodically during operator control. */ @@ -194,7 +214,7 @@ public void teleopPeriodic() { // For 2026 - REBUILT, the alliance will be provided as a single character // representing the color of the alliance whose goal will go inactive - // first (i.e. ‘R’ = red, ‘B’ = blue). This alliance’s goal will be + // first (i.e. 'R' = red, 'B' = blue). This alliance's goal will be // active in Shifts 2 and 4. // // https://docs.wpilib.org/en/stable/docs/yearly-overview/2026-game-data.html @@ -234,9 +254,38 @@ public void testPeriodic() {} /** This function is called once when the robot is first started up. */ @Override - public void simulationInit() {} + public void simulationInit() { + visionSim = new VisionSystemSim("main"); + + // Load AprilTag field layout + AprilTagFieldLayout fieldLayout = AprilTagFieldLayout.loadField(AprilTagFields.kDefaultField); + + // Register AprilTags with vision simulation + visionSim.addAprilTags(fieldLayout); + + // Simulated Camera Properties + SimCameraProperties cameraProp = new SimCameraProperties(); + // A 1280 x 800 camera with a 100 degree diagonal FOV. + cameraProp.setCalibration(1280, 800, Rotation2d.fromDegrees(100)); + // Approximate detection noise with average and standard deviation error in pixels. + cameraProp.setCalibError(0.25, 0.08); + // Set the camera image capture framerate (Note: this is limited by robot loop rate). + cameraProp.setFPS(20); + // The average and standard deviation in milliseconds of image data latency. + cameraProp.setAvgLatencyMs(35); + cameraProp.setLatencyStdDevMs(5); + + // Define Cameras and add to simulation + PhotonCamera camera0 = new PhotonCamera("frontCam"); + PhotonCamera camera1 = new PhotonCamera("backCam"); + visionSim.addCamera(new PhotonCameraSim(camera0, cameraProp), Constants.Cameras.robotToCamera0); + visionSim.addCamera(new PhotonCameraSim(camera1, cameraProp), Constants.Cameras.robotToCamera1); + } /** This function is called periodically whilst in simulation. */ @Override - public void simulationPeriodic() {} + public void simulationPeriodic() { + // Update sim each sim tick + visionSim.update(m_robotContainer.getDrivebase().getPose()); + } } diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java index faf29dd..c8099d9 100644 --- a/src/main/java/frc/robot/RobotContainer.java +++ b/src/main/java/frc/robot/RobotContainer.java @@ -23,17 +23,21 @@ 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.Transform3d; import edu.wpi.first.math.kinematics.ChassisSpeeds; import edu.wpi.first.math.util.Units; +import edu.wpi.first.wpilibj.Filesystem; import edu.wpi.first.wpilibj.GenericHID; import edu.wpi.first.wpilibj.XboxController; import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.Commands; +import edu.wpi.first.wpilibj2.command.button.CommandJoystick; import edu.wpi.first.wpilibj2.command.button.CommandXboxController; import edu.wpi.first.wpilibj2.command.button.RobotModeTriggers; import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine; import frc.robot.Constants.OperatorConstants; +import frc.robot.Constants.SimCameras; import frc.robot.FieldConstants.AprilTagLayoutType; import frc.robot.commands.AutopilotCommands; import frc.robot.commands.DriveCommands; @@ -47,6 +51,7 @@ import frc.robot.subsystems.imu.ImuIONavX; import frc.robot.subsystems.imu.ImuIOPigeon2; import frc.robot.subsystems.imu.ImuIOSim; +import frc.robot.subsystems.vision.CameraSweepEvaluator; import frc.robot.subsystems.vision.Vision; import frc.robot.subsystems.vision.VisionIO; import frc.robot.subsystems.vision.VisionIOLimelight; @@ -61,6 +66,9 @@ import frc.robot.util.RBSIPowerMonitor; import java.util.Set; import org.littletonrobotics.junction.networktables.LoggedDashboardChooser; +import org.photonvision.PhotonCamera; +import org.photonvision.simulation.PhotonCameraSim; +import org.photonvision.simulation.VisionSystemSim; /** This is the location for defining robot hardware, commands, and controller button bindings. */ public class RobotContainer { @@ -72,6 +80,10 @@ public class RobotContainer { final CommandXboxController operatorController = new CommandXboxController(1); // Second Operator final OverrideSwitches overrides = new OverrideSwitches(2); // Console toggle switches + // These two are needed for the Sweep evaluator for camera FOV simulation + final CommandJoystick joystick3 = new CommandJoystick(3); // Joystick for CamersSweepEvaluator + private final CameraSweepEvaluator sweep; + /** Declare the robot subsystems here ************************************ */ // These are the "Active Subsystems" that the robot controls private final Drive m_drivebase; @@ -152,11 +164,12 @@ public RobotContainer() { default -> null; }; m_accel = new Accelerometer(m_imu); + sweep = null; break; case SIM: // Sim robot, instantiate physics sim IO implementations - m_imu = new ImuIOSim(Constants.loopPeriodSecs); + m_imu = new ImuIOSim(); m_drivebase = new Drive(m_imu); m_flywheel = new Flywheel(new FlywheelIOSim() {}); m_vision = @@ -165,16 +178,38 @@ public RobotContainer() { new VisionIOPhotonVisionSim(camera0Name, robotToCamera0, m_drivebase::getPose), new VisionIOPhotonVisionSim(camera1Name, robotToCamera1, m_drivebase::getPose)); m_accel = new Accelerometer(m_imu); + + // CameraSweepEvaluator Construct + // 1) Create the vision simulation world + VisionSystemSim visionSim = new VisionSystemSim("CameraSweepWorld"); + // 2) Add AprilTags (field layout) + visionSim.addAprilTags(FieldConstants.aprilTagLayout); + // 3) Create two simulated cameras + // Create PhotonCamera objects (names must match VisionIOPhotonVisionSim) + PhotonCamera camera1 = new PhotonCamera(camera0Name); + PhotonCamera camera2 = new PhotonCamera(camera1Name); + + // Wrap them with simulation + properties (2026 API) + PhotonCameraSim cam1 = new PhotonCameraSim(camera1, SimCameras.kSimCamera1Props); + PhotonCameraSim cam2 = new PhotonCameraSim(camera2, SimCameras.kSimCamera2Props); + + // 4) Register cameras with the sim + visionSim.addCamera(cam1, Transform3d.kZero); + visionSim.addCamera(cam2, Transform3d.kZero); + // 5) Create the sweep evaluator + sweep = new CameraSweepEvaluator(visionSim, cam1, cam2); + break; default: // Replayed robot, disable IO implementations - m_imu = new ImuIOSim(Constants.loopPeriodSecs); + m_imu = new ImuIOSim(); m_drivebase = new Drive(m_imu); m_flywheel = new Flywheel(new FlywheelIO() {}); m_vision = new Vision(m_drivebase::addVisionMeasurement, new VisionIO() {}, new VisionIO() {}); m_accel = new Accelerometer(m_imu); + sweep = null; break; } @@ -343,6 +378,41 @@ private void configureBindings() { // Stop when command ended m_drivebase::stop, m_drivebase)); + + // Double-press the A button on Joystick3 to run the CameraSweepEvaluator + // Use WPILib's built-in double-press binding + joystick3 + .button(1) + .multiPress(2, 0.2) + .onTrue( + Commands.runOnce( + () -> { + try { + sweep.runFullSweep( + Filesystem.getOperatingDirectory() + .toPath() + .resolve("camera_sweep.csv") + .toString()); + } catch (Exception e) { + e.printStackTrace(); + } + })); + } + + /** + * Use this to pass the MANUAL SHOOT FUEL command to the main {@link Robot} class. + * + * @return the command to run in autonomous + */ + public Command getManualAuto() { + // NOTE: + // + // For teams not using PathPlanner, this auto may be used to simply shoot the pre-loaded fuel + // into the HUB during AUTO. Since shooters are beyond the scope of Az-RBSI, you will have to + // write your own command and call it here. + + // Replace Commands.none() with your command that shoots fuel into the HUB. + return Commands.none(); } /** @@ -386,6 +456,11 @@ public void updateAlerts() { } } + /** Drivetrain getter method */ + public Drive getDrivebase() { + return m_drivebase; + } + /** * Set up the SysID routines from AdvantageKit * diff --git a/src/main/java/frc/robot/subsystems/drive/Drive.java b/src/main/java/frc/robot/subsystems/drive/Drive.java index 0366120..91129b7 100644 --- a/src/main/java/frc/robot/subsystems/drive/Drive.java +++ b/src/main/java/frc/robot/subsystems/drive/Drive.java @@ -28,7 +28,7 @@ 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.geometry.Twist2d; +import edu.wpi.first.math.geometry.Translation3d; import edu.wpi.first.math.kinematics.ChassisSpeeds; import edu.wpi.first.math.kinematics.SwerveDriveKinematics; import edu.wpi.first.math.kinematics.SwerveModulePosition; @@ -40,16 +40,19 @@ import edu.wpi.first.wpilibj.Alert.AlertType; import edu.wpi.first.wpilibj.DriverStation; import edu.wpi.first.wpilibj.DriverStation.Alliance; +import edu.wpi.first.wpilibj.Timer; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.SubsystemBase; import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine; import frc.robot.Constants; import frc.robot.Constants.AutoConstants; import frc.robot.Constants.DrivebaseConstants; +import frc.robot.Constants.RobotConstants; import frc.robot.subsystems.imu.ImuIO; import frc.robot.util.LocalADStarAK; import frc.robot.util.RBSIEnum.Mode; import frc.robot.util.RBSIParsing; +import java.util.Arrays; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.littletonrobotics.junction.AutoLogOutput; @@ -66,7 +69,7 @@ public class Drive extends SubsystemBase { new Alert("Disconnected gyro, using kinematics as fallback.", AlertType.kError); private SwerveDriveKinematics kinematics = new SwerveDriveKinematics(getModuleTranslations()); - private Rotation2d rawGyroRotation = Rotation2d.kZero; + private Rotation2d rawGyroRotation = imuInputs.yawPosition; private SwerveModulePosition[] lastModulePositions = // For delta tracking new SwerveModulePosition[] { new SwerveModulePosition(), @@ -85,54 +88,75 @@ public class Drive extends SubsystemBase { new TrapezoidProfile.Constraints( DrivebaseConstants.kMaxAngularSpeed, DrivebaseConstants.kMaxAngularAccel)); + private DriveSimPhysics simPhysics; + // Constructor public Drive(ImuIO imuIO) { this.imuIO = imuIO; - switch (Constants.getSwerveType()) { - case PHOENIX6: - // This one is easy because it's all CTRE all the time - for (int i = 0; i < 4; i++) { - modules[i] = new Module(new ModuleIOTalonFX(i), i); - } - break; + if (Constants.getMode() == Mode.REAL) { - case YAGSL: - // Then parse the module(s) - Byte modType = RBSIParsing.parseModuleType(); - for (int i = 0; i < 4; i++) { - switch (modType) { - case 0b00000000: // ALL-CTRE - if (kImuType == "navx" || kImuType == "navx_spi") { - modules[i] = new Module(new ModuleIOTalonFX(i), i); - } else { - throw new RuntimeException( - "For an all-CTRE drive base, use Phoenix Tuner X Swerve Generator instead of YAGSL!"); - } - case 0b00010000: // Blended Talon Drive / NEO Steer - modules[i] = new Module(new ModuleIOBlended(i), i); - break; - case 0b01010000: // NEO motors + CANcoder - modules[i] = new Module(new ModuleIOSparkCANcoder(i), i); - break; - case 0b01010100: // NEO motors + analog encoder - modules[i] = new Module(new ModuleIOSpark(i), i); - break; - default: - throw new RuntimeException("Invalid swerve module combination"); + // Case out the swerve types because Az-RBSI supports a lot + switch (Constants.getSwerveType()) { + case PHOENIX6: + // This one is easy because it's all CTRE all the time + for (int i = 0; i < 4; i++) { + modules[i] = new Module(new ModuleIOTalonFX(i), i); } - } + break; + + case YAGSL: + // Then parse the module(s) + Byte modType = RBSIParsing.parseModuleType(); + for (int i = 0; i < 4; i++) { + switch (modType) { + case 0b00000000: // ALL-CTRE + if (kImuType == "navx" || kImuType == "navx_spi") { + modules[i] = new Module(new ModuleIOTalonFX(i), i); + } else { + throw new RuntimeException( + "For an all-CTRE drive base, use Phoenix Tuner X Swerve Generator instead of YAGSL!"); + } + case 0b00010000: // Blended Talon Drive / NEO Steer + modules[i] = new Module(new ModuleIOBlended(i), i); + break; + case 0b01010000: // NEO motors + CANcoder + modules[i] = new Module(new ModuleIOSparkCANcoder(i), i); + break; + case 0b01010100: // NEO motors + analog encoder + modules[i] = new Module(new ModuleIOSpark(i), i); + break; + default: + throw new RuntimeException("Invalid swerve module combination"); + } + } + break; - default: - throw new RuntimeException("Invalid Swerve Drive Type"); + default: + throw new RuntimeException("Invalid Swerve Drive Type"); + } + // Start odometry thread (for the real robot) + + PhoenixOdometryThread.getInstance().start(); + + } else { + + // If SIM, just order up some SIM modules! + for (int i = 0; i < 4; i++) { + modules[i] = new Module(new ModuleIOSim(), i); + } + + // Load the physics simulator + simPhysics = + new DriveSimPhysics( + kinematics, + RobotConstants.kRobotMOI, // kg m^2 + RobotConstants.kMaxWheelTorque); // Nm } // Usage reporting for swerve template HAL.report(tResourceType.kResourceType_RobotDrive, tInstances.kRobotDriveSwerve_AdvantageKit); - // Start odometry thread - PhoenixOdometryThread.getInstance().start(); - // Configure Autonomous Path Building for PathPlanner based on `AutoType` switch (Constants.getAutoType()) { case PATHPLANNER: @@ -194,39 +218,39 @@ public void periodic() { } } - // Update the IMU inputs — logging happens automatically + // Update the IMU inputs -- logging happens automatically imuIO.updateInputs(imuInputs); - // Feed historical samples into odometry - double[] sampleTimestamps = modules[0].getOdometryTimestamps(); - int sampleCount = sampleTimestamps.length; - - for (int i = 0; i < sampleCount; i++) { - // Read wheel positions and deltas from each module - SwerveModulePosition[] modulePositions = new SwerveModulePosition[4]; - SwerveModulePosition[] moduleDeltas = new SwerveModulePosition[4]; - - for (int moduleIndex = 0; moduleIndex < 4; moduleIndex++) { - modulePositions[moduleIndex] = modules[moduleIndex].getOdometryPositions()[i]; - moduleDeltas[moduleIndex] = - new SwerveModulePosition( - modulePositions[moduleIndex].distanceMeters - - lastModulePositions[moduleIndex].distanceMeters, - modulePositions[moduleIndex].angle); - lastModulePositions[moduleIndex] = modulePositions[moduleIndex]; - } + // Feed historical samples into odometry if REAL robot + if (Constants.getMode() != Mode.SIM) { + double[] sampleTimestamps = modules[0].getOdometryTimestamps(); + int sampleCount = sampleTimestamps.length; + + for (int i = 0; i < sampleCount; i++) { + // Read wheel positions and deltas from each module + SwerveModulePosition[] modulePositions = new SwerveModulePosition[4]; + SwerveModulePosition[] moduleDeltas = new SwerveModulePosition[4]; + + for (int moduleIndex = 0; moduleIndex < 4; moduleIndex++) { + modulePositions[moduleIndex] = modules[moduleIndex].getOdometryPositions()[i]; + moduleDeltas[moduleIndex] = + new SwerveModulePosition( + modulePositions[moduleIndex].distanceMeters + - lastModulePositions[moduleIndex].distanceMeters, + modulePositions[moduleIndex].angle); + lastModulePositions[moduleIndex] = modulePositions[moduleIndex]; + } - // Update gyro angle for odometry - if (imuInputs.connected && imuInputs.odometryYawPositions.length > i) { - rawGyroRotation = imuInputs.odometryYawPositions[i]; - } else { - // Use the angle delta from the kinematics and module deltas - Twist2d twist = kinematics.toTwist2d(moduleDeltas); - rawGyroRotation = rawGyroRotation.plus(new Rotation2d(twist.dtheta)); - } + // Update gyro angle for odometry + Rotation2d yaw = + imuInputs.connected && imuInputs.odometryYawPositions.length > i + ? imuInputs.odometryYawPositions[i] + : imuInputs.yawPosition; - // Apply to pose estimator - m_PoseEstimator.updateWithTime(sampleTimestamps[i], rawGyroRotation, modulePositions); + // Apply to pose estimator + m_PoseEstimator.updateWithTime(sampleTimestamps[i], yaw, modulePositions); + } + Logger.recordOutput("Drive/Pose", m_PoseEstimator.getEstimatedPosition()); } // Module periodic updates @@ -240,6 +264,54 @@ public void periodic() { gyroDisconnectedAlert.set(!imuInputs.connected && Constants.getMode() != Mode.SIM); } + /** Simulation Periodic Method */ + @Override + public void simulationPeriodic() { + final double dt = Constants.loopPeriodSecs; + + // 1) Advance module wheel physics + for (Module module : modules) { + module.simulationPeriodic(); + } + + // 2) Get module states from modules (authoritative) + SwerveModuleState[] moduleStates = + Arrays.stream(modules).map(Module::getState).toArray(SwerveModuleState[]::new); + + // 3) Update SIM physics (linear + angular) + simPhysics.update(moduleStates, dt); + + // 4) Feed IMU from authoritative physics + imuIO.simulationSetYaw(simPhysics.getYaw()); + imuIO.simulationSetOmega(simPhysics.getOmegaRadPerSec()); + imuIO.setLinearAccel( + new Translation3d( + simPhysics.getLinearAccel().getX(), simPhysics.getLinearAccel().getY(), 0.0)); + + // 5) Feed PoseEstimator with authoritative yaw and module positions + SwerveModulePosition[] modulePositions = + Arrays.stream(modules).map(Module::getPosition).toArray(SwerveModulePosition[]::new); + + m_PoseEstimator.resetPosition( + simPhysics.getYaw(), // gyro reading (authoritative) + modulePositions, // wheel positions + simPhysics.getPose() // pose is authoritative + ); + + // 6) Optional: inject vision measurement in SIM + if (simulatedVisionAvailable) { + Pose2d visionPose = getSimulatedVisionPose(); + double visionTimestamp = Timer.getFPGATimestamp(); + var visionStdDevs = getSimulatedVisionStdDevs(); + m_PoseEstimator.addVisionMeasurement(visionPose, visionTimestamp, visionStdDevs); + } + + // 7) Logging + Logger.recordOutput("Sim/Pose", simPhysics.getPose()); + Logger.recordOutput("Sim/Yaw", simPhysics.getYaw()); + Logger.recordOutput("Sim/LinearAccel", simPhysics.getLinearAccel()); + } + /** Drive Base Action Functions ****************************************** */ /** @@ -304,17 +376,6 @@ public void runCharacterization(double output) { } } - // /** Drive Forward Command Factory **************************************** */ - // // Example factory method - // public Command driveForwardCommand(double distance) { - // // This method composes and returns a complex command object - // return Commands.sequence( - // // Use internal methods and sensor data to define the command logic - // new DriveToPositionCommand(this, distance), - // new StopDrivetrainCommand(this) - // ); - // } - /** * Reset the heading ProfiledPIDController * @@ -353,6 +414,7 @@ private SwerveModuleState[] getModuleStates() { } /** Returns the module positions (turn angles and drive positions) for all of the modules. */ + @AutoLogOutput(key = "SwerveStates/Positions") private SwerveModulePosition[] getModulePositions() { SwerveModulePosition[] states = new SwerveModulePosition[4]; for (int i = 0; i < 4; i++) { @@ -370,12 +432,18 @@ public ChassisSpeeds getChassisSpeeds() { /** Returns the current odometry pose. */ @AutoLogOutput(key = "Odometry/Robot") public Pose2d getPose() { + if (Constants.getMode() == Mode.SIM) { + return simPhysics.getPose(); + } return m_PoseEstimator.getEstimatedPosition(); } /** Returns the current odometry rotation. */ + @AutoLogOutput(key = "Odometry/Yaw") public Rotation2d getHeading() { - imuIO.updateInputs(imuInputs); + if (Constants.getMode() == Mode.SIM) { + return simPhysics.getYaw(); + } return imuInputs.yawPosition; } @@ -398,6 +466,31 @@ public double[] getWheelRadiusCharacterizationPositions() { return values; } + /** + * Returns the measured chassis speeds in FIELD coordinates. + * + *

+X = field forward +Y = field left CCW+ = counterclockwise + */ + @AutoLogOutput(key = "SwerveChassisSpeeds/FieldMeasured") + public ChassisSpeeds getFieldRelativeSpeeds() { + // Robot-relative measured speeds from modules + ChassisSpeeds robotRelative = getChassisSpeeds(); + + // Convert to field-relative using authoritative yaw + return ChassisSpeeds.fromRobotRelativeSpeeds(robotRelative, getHeading()); + } + + /** + * Returns the FIELD-relative linear velocity of the robot's center. + * + *

+X = field forward +Y = field left + */ + @AutoLogOutput(key = "Drive/FieldLinearVelocity") + public Translation2d getFieldLinearVelocity() { + ChassisSpeeds fieldSpeeds = getFieldRelativeSpeeds(); + return new Translation2d(fieldSpeeds.vxMetersPerSecond, fieldSpeeds.vyMetersPerSecond); + } + /** Returns the average velocity of the modules in rotations/sec (Phoenix native units). */ public double getFFCharacterizationVelocity() { double output = 0.0; @@ -500,4 +593,44 @@ public void followTrajectory(SwerveSample sample) { // Apply the generated speeds runVelocity(speeds); } + + // ---------------- SIM VISION ---------------- + + // Vision measurement enabled in simulation + private boolean simulatedVisionAvailable = true; + + // Maximum simulated noise in meters/radians + private static final double SIM_VISION_POS_NOISE_M = 0.02; // +/- 2cm + private static final double SIM_VISION_YAW_NOISE_RAD = Math.toRadians(2); // +/- 2 degrees + + /** + * Returns a simulated Pose2d for vision in field coordinates. Adds a small random jitter to + * simulate measurement error. + */ + private Pose2d getSimulatedVisionPose() { + Pose2d truePose = simPhysics.getPose(); // authoritative pose + + // Add small random noise + double dx = (Math.random() * 2 - 1) * SIM_VISION_POS_NOISE_M; + double dy = (Math.random() * 2 - 1) * SIM_VISION_POS_NOISE_M; + double dTheta = (Math.random() * 2 - 1) * SIM_VISION_YAW_NOISE_RAD; + + return new Pose2d( + truePose.getX() + dx, + truePose.getY() + dy, + truePose.getRotation().plus(new Rotation2d(dTheta))); + } + + /** + * Returns the standard deviations for the simulated vision measurement. These values are used by + * the PoseEstimator to weight vision updates. + */ + private edu.wpi.first.math.Matrix getSimulatedVisionStdDevs() { + edu.wpi.first.math.Matrix stdDevs = + new edu.wpi.first.math.Matrix<>(N3.instance, N1.instance); + stdDevs.set(0, 0, 0.02); // X standard deviation (meters) + stdDevs.set(1, 0, 0.02); // Y standard deviation (meters) + stdDevs.set(2, 0, Math.toRadians(2)); // rotation standard deviation (radians) + return stdDevs; + } } diff --git a/src/main/java/frc/robot/subsystems/drive/DriveSimPhysics.java b/src/main/java/frc/robot/subsystems/drive/DriveSimPhysics.java new file mode 100644 index 0000000..e58a4b3 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/drive/DriveSimPhysics.java @@ -0,0 +1,124 @@ +// Copyright (c) 2026 Az-FIRST +// http://github.com/AZ-First +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// version 3 as published by the Free Software Foundation or +// available in the root directory of this project. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package frc.robot.subsystems.drive; + +import edu.wpi.first.math.MathUtil; +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.kinematics.SwerveDriveKinematics; +import edu.wpi.first.math.kinematics.SwerveModuleState; + +/** + * Authoritative drivetrain physics model for SIM. + * + *

Responsibilities: - Integrates yaw with inertia + friction-like damping - Integrates + * translation in the field frame - Produces ONE true robot pose for SIM + */ +public class DriveSimPhysics { + + /* ---------------- Physical state ---------------- */ + private Pose2d pose = Pose2d.kZero; + private double omegaRadPerSec = 0.0; + private Translation2d linearAccel = new Translation2d(0, 0); + private Translation2d prevVelocity = new Translation2d(0, 0); + + /* ---------------- Robot constants ---------------- */ + private final double moiKgMetersSq; + private final double maxTorqueNm; + private static final double ANGULAR_DAMPING = 12.0; // Increased friction-like damping + + private final SwerveDriveKinematics kinematics; + + public DriveSimPhysics( + SwerveDriveKinematics kinematics, double moiKgMetersSq, double maxTorqueNm) { + this.kinematics = kinematics; + this.moiKgMetersSq = moiKgMetersSq; + this.maxTorqueNm = maxTorqueNm; + } + + /** + * Advance the physics model by one timestep. + * + * @param moduleStates Authoritative wheel states + * @param dtSeconds Loop period + */ + public void update(SwerveModuleState[] moduleStates, double dtSeconds) { + + // ------------------ CHASSIS VELOCITY ------------------ + var chassis = kinematics.toChassisSpeeds(moduleStates); + + // Robot-relative velocity + Translation2d robotVelocity = + new Translation2d(chassis.vxMetersPerSecond, chassis.vyMetersPerSecond); + + // Rotate into field frame + Translation2d fieldVelocity = robotVelocity.rotateBy(pose.getRotation()); + + // Compute linear acceleration (finite difference) + linearAccel = fieldVelocity.minus(prevVelocity).div(dtSeconds); + prevVelocity = fieldVelocity; + + // Integrate translation in field frame + Translation2d newTranslation = pose.getTranslation().plus(fieldVelocity.times(dtSeconds)); + + // ------------------ ANGULAR ------------------ + double commandedOmega = chassis.omegaRadiansPerSecond; + + // Simple torque model with damping + double torque = + MathUtil.clamp(commandedOmega * moiKgMetersSq / dtSeconds, -maxTorqueNm, maxTorqueNm); + + // Stronger angular damping to simulate motor friction + torque -= omegaRadPerSec * ANGULAR_DAMPING; + + // Integrate angular velocity + omegaRadPerSec += (torque / moiKgMetersSq) * dtSeconds; + + // Integrate yaw + Rotation2d newYaw = pose.getRotation().plus(new Rotation2d(omegaRadPerSec * dtSeconds)); + + // ------------------ FINAL POSE ------------------ + pose = new Pose2d(newTranslation, newYaw); + } + + /* ================== Getters ================== */ + public Pose2d getPose() { + return pose; + } + + public Rotation2d getYaw() { + return pose.getRotation(); + } + + public double getOmegaRadPerSec() { + return omegaRadPerSec; + } + + public Translation2d getLinearAccel() { + return linearAccel; + } + + /* ================== Reset ================== */ + public void reset(Pose2d pose) { + this.pose = pose; + this.omegaRadPerSec = 0.0; + this.prevVelocity = new Translation2d(0, 0); + this.linearAccel = new Translation2d(0, 0); + } +} diff --git a/src/main/java/frc/robot/subsystems/drive/Module.java b/src/main/java/frc/robot/subsystems/drive/Module.java index 8b647bb..4bd9017 100644 --- a/src/main/java/frc/robot/subsystems/drive/Module.java +++ b/src/main/java/frc/robot/subsystems/drive/Module.java @@ -128,6 +128,11 @@ public double[] getOdometryTimestamps() { return inputs.odometryTimestamps; } + /** Forwards the simulation periodic call to the IO layer */ + public void simulationPeriodic() { + io.simulationPeriodic(); + } + /** Returns the module position in radians. */ public double getWheelRadiusCharacterizationPosition() { return inputs.drivePositionRad; diff --git a/src/main/java/frc/robot/subsystems/drive/ModuleIO.java b/src/main/java/frc/robot/subsystems/drive/ModuleIO.java index 2b7e9da..1005577 100644 --- a/src/main/java/frc/robot/subsystems/drive/ModuleIO.java +++ b/src/main/java/frc/robot/subsystems/drive/ModuleIO.java @@ -60,4 +60,7 @@ public default void setDrivePID(double kP, double kI, double kD) {} /** Set P, I, and D gains for closed-loop control on turn motor */ public default void setTurnPID(double kP, double kI, double kD) {} + + /** Simulation-only update hook */ + default void simulationPeriodic() {} } diff --git a/src/main/java/frc/robot/subsystems/drive/ModuleIOSim.java b/src/main/java/frc/robot/subsystems/drive/ModuleIOSim.java index 5dfa59b..8fb64fe 100644 --- a/src/main/java/frc/robot/subsystems/drive/ModuleIOSim.java +++ b/src/main/java/frc/robot/subsystems/drive/ModuleIOSim.java @@ -9,9 +9,6 @@ package frc.robot.subsystems.drive; -import com.ctre.phoenix6.configs.CANcoderConfiguration; -import com.ctre.phoenix6.configs.TalonFXConfiguration; -import com.ctre.phoenix6.swerve.SwerveModuleConstants; import edu.wpi.first.math.MathUtil; import edu.wpi.first.math.controller.PIDController; import edu.wpi.first.math.geometry.Rotation2d; @@ -20,6 +17,7 @@ import edu.wpi.first.math.util.Units; import edu.wpi.first.wpilibj.Timer; import edu.wpi.first.wpilibj.simulation.DCMotorSim; +import frc.robot.Constants; /** * Physics sim implementation of module IO. The sim models are configured using a set of module @@ -49,19 +47,17 @@ public class ModuleIOSim implements ModuleIO { private double driveAppliedVolts = 0.0; private double turnAppliedVolts = 0.0; - public ModuleIOSim( - SwerveModuleConstants - constants) { + public ModuleIOSim() { // Create drive and turn sim models driveSim = new DCMotorSim( LinearSystemId.createDCMotorSystem( - DRIVE_GEARBOX, constants.DriveInertia, constants.DriveMotorGearRatio), + DRIVE_GEARBOX, SwerveConstants.kDriveInertia, SwerveConstants.kDriveGearRatio), DRIVE_GEARBOX); turnSim = new DCMotorSim( LinearSystemId.createDCMotorSystem( - TURN_GEARBOX, constants.SteerInertia, constants.SteerMotorGearRatio), + TURN_GEARBOX, SwerveConstants.kSteerInertia, SwerveConstants.kSteerGearRatio), TURN_GEARBOX); // Enable wrapping for turn PID @@ -69,45 +65,59 @@ public ModuleIOSim( } @Override - public void updateInputs(ModuleIOInputs inputs) { - // Run closed-loop control + public void simulationPeriodic() { + // Closed-loop control if (driveClosedLoop) { driveAppliedVolts = driveFFVolts + driveController.calculate(driveSim.getAngularVelocityRadPerSec()); } else { driveController.reset(); } + if (turnClosedLoop) { + // TODO: turn PID has no feedforward or inertia compensation; fix turnAppliedVolts = turnController.calculate(turnSim.getAngularPositionRad()); } else { turnController.reset(); } - // Update simulation state + // Apply voltages driveSim.setInputVoltage(MathUtil.clamp(driveAppliedVolts, -12.0, 12.0)); turnSim.setInputVoltage(MathUtil.clamp(turnAppliedVolts, -12.0, 12.0)); - driveSim.update(0.02); - turnSim.update(0.02); - // Update drive inputs + // Advance physics + driveSim.update(Constants.loopPeriodSecs); + turnSim.update(Constants.loopPeriodSecs); + } + + @Override + public void updateInputs(ModuleIOInputs inputs) { + // Convert rotor -> mechanism + double mechanismPositionRad = driveSim.getAngularPositionRad(); + double mechanismVelocityRadPerSec = driveSim.getAngularVelocityRadPerSec(); + + // Drive inputs inputs.driveConnected = true; - inputs.drivePositionRad = driveSim.getAngularPositionRad(); - inputs.driveVelocityRadPerSec = driveSim.getAngularVelocityRadPerSec(); + inputs.drivePositionRad = mechanismPositionRad; + inputs.driveVelocityRadPerSec = mechanismVelocityRadPerSec; inputs.driveAppliedVolts = driveAppliedVolts; inputs.driveCurrentAmps = Math.abs(driveSim.getCurrentDrawAmps()); - // Update turn inputs + // Turn inputs + double steerPositionRad = turnSim.getAngularPositionRad(); + inputs.turnConnected = true; inputs.turnEncoderConnected = true; - inputs.turnAbsolutePosition = new Rotation2d(turnSim.getAngularPositionRad()); - inputs.turnPosition = new Rotation2d(turnSim.getAngularPositionRad()); - inputs.turnVelocityRadPerSec = turnSim.getAngularVelocityRadPerSec(); + inputs.turnAbsolutePosition = new Rotation2d(steerPositionRad); + inputs.turnPosition = new Rotation2d(steerPositionRad); + inputs.turnVelocityRadPerSec = + turnSim.getAngularVelocityRadPerSec() / SwerveConstants.kSteerGearRatio; inputs.turnAppliedVolts = turnAppliedVolts; inputs.turnCurrentAmps = Math.abs(turnSim.getCurrentDrawAmps()); - // Update odometry inputs (50Hz because high-frequency odometry in sim doesn't matter) + // Odometry (single sample per loop is fine) inputs.odometryTimestamps = new double[] {Timer.getFPGATimestamp()}; - inputs.odometryDrivePositionsRad = new double[] {inputs.drivePositionRad}; + inputs.odometryDrivePositionsRad = new double[] {mechanismPositionRad}; inputs.odometryTurnPositions = new Rotation2d[] {inputs.turnPosition}; } @@ -124,10 +134,16 @@ public void setTurnOpenLoop(double output) { } @Override - public void setDriveVelocity(double velocityRadPerSec) { + public void setDriveVelocity(double wheelRadPerSec) { driveClosedLoop = true; - driveFFVolts = DRIVE_KS * Math.signum(velocityRadPerSec) + DRIVE_KV * velocityRadPerSec; - driveController.setSetpoint(velocityRadPerSec); + + // Convert wheel -> rotor + double rotorRadPerSec = wheelRadPerSec * SwerveConstants.kDriveGearRatio; + + // Feedforward should also be in rotor units + driveFFVolts = DRIVE_KS * Math.signum(rotorRadPerSec) + DRIVE_KV * rotorRadPerSec; + + driveController.setSetpoint(rotorRadPerSec); } @Override diff --git a/src/main/java/frc/robot/subsystems/imu/ImuIO.java b/src/main/java/frc/robot/subsystems/imu/ImuIO.java index 4434b24..9273f0c 100644 --- a/src/main/java/frc/robot/subsystems/imu/ImuIO.java +++ b/src/main/java/frc/robot/subsystems/imu/ImuIO.java @@ -56,4 +56,11 @@ default void updateInputs(ImuIOInputs inputs) {} /** Zero the yaw to a known field-relative angle */ default void zeroYaw(Rotation2d yaw) {} + + /** Simulation-only update hooks */ + default void simulationSetYaw(Rotation2d yaw) {} + + default void simulationSetOmega(double omegaRadPerSec) {} + + default void setLinearAccel(Translation3d accelMps2) {} } diff --git a/src/main/java/frc/robot/subsystems/imu/ImuIOSim.java b/src/main/java/frc/robot/subsystems/imu/ImuIOSim.java index b0eb800..10a7b6d 100644 --- a/src/main/java/frc/robot/subsystems/imu/ImuIOSim.java +++ b/src/main/java/frc/robot/subsystems/imu/ImuIOSim.java @@ -17,71 +17,77 @@ package frc.robot.subsystems.imu; -import static edu.wpi.first.units.Units.*; +import static edu.wpi.first.units.Units.RadiansPerSecond; import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.wpilibj.Timer; import java.util.LinkedList; import java.util.Queue; +import org.littletonrobotics.junction.Logger; /** Simulated IMU for full robot simulation & replay logging */ public class ImuIOSim implements ImuIO { - private double yawDeg = 0.0; - private double yawRateDps = 0.0; + // --- AUTHORITATIVE SIM STATE --- + private Rotation2d yaw = Rotation2d.kZero; + private double yawRateRadPerSec = 0.0; private Translation3d linearAccel = Translation3d.kZero; + // --- ODOMETRY HISTORY FOR LOGGING/LATENCY --- private final Queue odomTimestamps = new LinkedList<>(); - private final Queue odomYaws = new LinkedList<>(); + private final Queue odomYaws = new LinkedList<>(); - private final double loopPeriodSecs; + public ImuIOSim() {} - public ImuIOSim(double loopPeriodSecs) { - this.loopPeriodSecs = loopPeriodSecs; + // ---------------- SIMULATION INPUTS (PUSH) ---------------- + + /** Set yaw from authoritative physics */ + public void simulationSetYaw(Rotation2d yaw) { + this.yaw = yaw; + } + + /** Set angular velocity from authoritative physics */ + public void simulationSetOmega(double omegaRadPerSec) { + this.yawRateRadPerSec = omegaRadPerSec; } + /** Set linear acceleration from physics (optional) */ + public void setLinearAccel(Translation3d accelMps2) { + this.linearAccel = accelMps2; + } + + // ---------------- IO UPDATE (PULL) ---------------- + + /** Populate the IMUIOInputs object with the current SIM state */ @Override public void updateInputs(ImuIOInputs inputs) { - // Populate raw IMU readings inputs.connected = true; - inputs.yawPosition = Rotation2d.fromDegrees(yawDeg); - inputs.yawVelocityRadPerSec = RadiansPerSecond.of(yawRateDps); + + // Authoritative physics + inputs.yawPosition = yaw; + inputs.yawVelocityRadPerSec = RadiansPerSecond.of(yawRateRadPerSec); inputs.linearAccel = linearAccel; - // Maintain odometry history for latency/logging - double now = System.currentTimeMillis() / 1000.0; + // Maintain odometry history for logging / latency purposes + double now = Timer.getFPGATimestamp(); odomTimestamps.add(now); - odomYaws.add(yawDeg); + odomYaws.add(yaw); while (odomTimestamps.size() > 50) odomTimestamps.poll(); while (odomYaws.size() > 50) odomYaws.poll(); - // Fill the provided inputs object inputs.odometryYawTimestamps = odomTimestamps.stream().mapToDouble(d -> d).toArray(); - inputs.odometryYawPositions = - odomYaws.stream().map(d -> Rotation2d.fromDegrees(d)).toArray(Rotation2d[]::new); + inputs.odometryYawPositions = odomYaws.toArray(Rotation2d[]::new); + + // Logging for SIM analysis + Logger.recordOutput("IMU/Yaw", yaw); + Logger.recordOutput("IMU/YawRateDps", Math.toDegrees(yawRateRadPerSec)); } @Override public void zeroYaw(Rotation2d yaw) { - yawDeg = yaw.getDegrees(); - } - - // --- Simulation helpers to update the IMU state --- - public void setYawDeg(double deg) { - yawDeg = deg; - } - - public void setYawRateDps(double dps) { - yawRateDps = dps; - } - - public void setLinearAccel(Translation3d accelMps2) { - linearAccel = accelMps2; - } - - /** Optional: integrate yaw from yawRate over loop period */ - public void integrateYaw() { - yawDeg += yawRateDps * loopPeriodSecs; + this.yaw = yaw; + this.yawRateRadPerSec = 0.0; } } diff --git a/src/main/java/frc/robot/subsystems/vision/CameraPlacementEvaluator.java b/src/main/java/frc/robot/subsystems/vision/CameraPlacementEvaluator.java new file mode 100644 index 0000000..aa907ee --- /dev/null +++ b/src/main/java/frc/robot/subsystems/vision/CameraPlacementEvaluator.java @@ -0,0 +1,90 @@ +// Copyright (c) 2026 Az-FIRST +// http://github.com/AZ-First +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// version 3 as published by the Free Software Foundation or +// available in the root directory of this project. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package frc.robot.subsystems.vision; + +import edu.wpi.first.math.geometry.*; +import edu.wpi.first.wpilibj.Timer; +import frc.robot.Constants; +import frc.robot.subsystems.drive.Drive; +import java.util.List; +import org.photonvision.simulation.PhotonCameraSim; +import org.photonvision.simulation.SimCameraProperties; +import org.photonvision.simulation.VisionSystemSim; +import org.photonvision.simulation.VisionTargetSim; +import org.photonvision.targeting.PhotonPipelineResult; + +public class CameraPlacementEvaluator { + + private final Drive driveSim; + private final VisionSystemSim visionSim; + + public CameraPlacementEvaluator(Drive driveSim, VisionSystemSim visionSim) { + this.driveSim = driveSim; + this.visionSim = visionSim; + } + + public double evaluateCameraPose( + PhotonCameraSim simCam, Transform3d robotToCamera, SimCameraProperties props) { + + // Add or adjust camera transform in vision sim + visionSim.adjustCamera(simCam, robotToCamera); + + // Test field poses + Pose2d[] testPoses = + new Pose2d[] { + new Pose2d(1.0, 1.0, new Rotation2d(0)), + new Pose2d(3.0, 1.0, new Rotation2d(Math.PI / 2)), + new Pose2d(5.0, 3.0, new Rotation2d(Math.PI)), + new Pose2d(2.0, 4.0, new Rotation2d(-Math.PI / 2)), + }; + + double totalScore = 0.0; + double latencyMillis = 0.0; + + for (Pose2d pose : testPoses) { + + driveSim.resetPose(pose); + + for (int i = 0; i < 5; i++) { + driveSim.simulationPeriodic(); + visionSim.update(driveSim.getPose()); + Timer.delay(Constants.loopPeriodSecs); + } + + var maybeCamPose3d = visionSim.getCameraPose(simCam); + if (maybeCamPose3d.isEmpty()) { + continue; + } + Pose3d camFieldPose = maybeCamPose3d.get(); + + List allTargets = (List) visionSim.getVisionTargets(); + + PhotonPipelineResult result = simCam.process(latencyMillis, camFieldPose, allTargets); + + int tagsSeen = result.targets.size(); + + Pose2d estimatedPose = driveSim.getPose(); + double poseError = pose.getTranslation().getDistance(estimatedPose.getTranslation()); + + double score = tagsSeen - poseError; + totalScore += score; + } + + return totalScore / testPoses.length; + } +} diff --git a/src/main/java/frc/robot/subsystems/vision/CameraSweepEvaluator.java b/src/main/java/frc/robot/subsystems/vision/CameraSweepEvaluator.java new file mode 100644 index 0000000..5cb532d --- /dev/null +++ b/src/main/java/frc/robot/subsystems/vision/CameraSweepEvaluator.java @@ -0,0 +1,126 @@ +// Copyright (c) 2026 Az-FIRST +// http://github.com/AZ-First +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// version 3 as published by the Free Software Foundation or +// available in the root directory of this project. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package frc.robot.subsystems.vision; + +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Translation3d; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.littletonrobotics.junction.Logger; +import org.photonvision.simulation.PhotonCameraSim; +import org.photonvision.simulation.VisionSystemSim; +import org.photonvision.simulation.VisionTargetSim; +import org.photonvision.targeting.PhotonPipelineResult; + +/** Sweeps two simulated cameras in a grid of poses and writes a CSV of performance. */ +public class CameraSweepEvaluator { + + private final VisionSystemSim visionSim; + private final PhotonCameraSim camSim1; + private final PhotonCameraSim camSim2; + + public CameraSweepEvaluator( + VisionSystemSim visionSim, PhotonCameraSim camSim1, PhotonCameraSim camSim2) { + this.visionSim = visionSim; + this.camSim1 = camSim1; + this.camSim2 = camSim2; + } + + /** + * Run a full sweep of candidate camera placements. + * + * @param outputCsvPath Path to write the CSV results + */ + public void runFullSweep(String outputCsvPath) throws IOException { + // Example field bounds (meters) -- tune these for your field size + double[] fieldX = {0.0, 2.0, 4.0, 6.0, 8.0}; // 0–8m along field length + double[] fieldY = {0.0, 1.0, 2.0, 3.0, 4.0}; // 0–4m along field width + double[] robotZ = {0.0}; // Usually floor height + double[] camYaw = {-30, 0, 30}; // Camera yaw + double[] camPitch = {-10, 0, 10}; // Camera pitch + + try (FileWriter writer = new FileWriter(outputCsvPath)) { + writer.write("robotX,robotY,robotZ," + "cam1Yaw,cam1Pitch,cam2Yaw,cam2Pitch,score\n"); + + // Sweep robot over the field + for (double rx : fieldX) { + for (double ry : fieldY) { + for (double rz : robotZ) { + Pose3d robotPose = + new Pose3d( + new Translation3d(rx, ry, rz), + new Rotation3d(0, 0, 0) // robot heading = 0 for simplicity + ); + + // Sweep camera rotations + for (double c1Yaw : camYaw) { + for (double c1Pitch : camPitch) { + Pose3d cam1Pose = + new Pose3d( + robotPose.getTranslation(), + new Rotation3d(Math.toRadians(c1Pitch), 0, Math.toRadians(c1Yaw))); + + for (double c2Yaw : camYaw) { + for (double c2Pitch : camPitch) { + Pose3d cam2Pose = + new Pose3d( + robotPose.getTranslation(), + new Rotation3d(Math.toRadians(c2Pitch), 0, Math.toRadians(c2Yaw))); + + // Get all vision targets + Set simTargets = visionSim.getVisionTargets(); + List targetList = new ArrayList<>(simTargets); + + // Simulate camera processing + PhotonPipelineResult res1 = camSim1.process(0, cam1Pose, targetList); + PhotonPipelineResult res2 = camSim2.process(0, cam2Pose, targetList); + + // Score + double score = res1.getTargets().size() + res2.getTargets().size(); + if (res1.getTargets().size() >= 2) score += 2.0; + if (res2.getTargets().size() >= 2) score += 2.0; + + // Penalize ambiguity safely with loops + for (var t : res1.getTargets()) score -= t.getPoseAmbiguity() * 2.0; + for (var t : res2.getTargets()) score -= t.getPoseAmbiguity() * 2.0; + + // Write CSV row + writer.write( + String.format( + "%.2f,%.2f,%.2f,%.1f,%.1f,%.1f,%.1f,%.1f\n", + rx, ry, rz, c1Yaw, c1Pitch, c2Yaw, c2Pitch, score)); + + Logger.recordOutput("CameraSweep/Score", score); + Logger.recordOutput("CameraSweep/Cam1Pose", cam1Pose); + Logger.recordOutput("CameraSweep/Cam2Pose", cam2Pose); + } + } + } + } + } + } + } + + writer.flush(); + } + } +} diff --git a/src/main/java/frc/robot/subsystems/vision/Vision.java b/src/main/java/frc/robot/subsystems/vision/Vision.java index cb55b42..6b71f49 100644 --- a/src/main/java/frc/robot/subsystems/vision/Vision.java +++ b/src/main/java/frc/robot/subsystems/vision/Vision.java @@ -24,6 +24,7 @@ import edu.wpi.first.wpilibj.Alert; import edu.wpi.first.wpilibj.Alert.AlertType; import edu.wpi.first.wpilibj2.command.SubsystemBase; +import frc.robot.Constants.Cameras; import frc.robot.FieldConstants; import frc.robot.subsystems.vision.VisionIO.PoseObservationType; import java.util.LinkedList; @@ -53,6 +54,10 @@ public Vision(VisionConsumer consumer, VisionIO... io) { new Alert( "Vision camera " + Integer.toString(i) + " is disconnected.", AlertType.kWarning); } + + // Log the robot-to-camera transformations + Logger.recordOutput("Vision/RobotToCamera0", Cameras.robotToCamera0); + Logger.recordOutput("Vision/RobotToCamera1", Cameras.robotToCamera1); } /** diff --git a/src/main/java/frc/robot/util/RBSIPowerMonitor.java b/src/main/java/frc/robot/util/RBSIPowerMonitor.java index 3177679..54972cf 100644 --- a/src/main/java/frc/robot/util/RBSIPowerMonitor.java +++ b/src/main/java/frc/robot/util/RBSIPowerMonitor.java @@ -76,12 +76,12 @@ public void periodic() { } } - if (voltage < PowerConstants.kVoltageWarning) { - new Alert("Low battery voltage!", AlertType.WARNING).set(true); - } - if (voltage < PowerConstants.kVoltageCritical) { - new Alert("Critical battery voltage!", AlertType.ERROR).set(true); - } + // if (voltage < PowerConstants.kVoltageWarning) { + // new Alert("Low battery voltage!", AlertType.WARNING).set(true); + // } + // if (voltage < PowerConstants.kVoltageCritical) { + // new Alert("Critical battery voltage!", AlertType.ERROR).set(true); + // } // --- Battery estimation --- long nowUs = RobotController.getFPGATime(); diff --git a/vendordeps/Phoenix5-5.36.0.json b/vendordeps/Phoenix5-replay-5.36.0.json similarity index 68% rename from vendordeps/Phoenix5-5.36.0.json rename to vendordeps/Phoenix5-replay-5.36.0.json index c60dd4c..7fbfcf5 100644 --- a/vendordeps/Phoenix5-5.36.0.json +++ b/vendordeps/Phoenix5-replay-5.36.0.json @@ -1,31 +1,31 @@ { - "fileName": "Phoenix5-5.36.0.json", + "fileName": "Phoenix5-replay-5.36.0.json", "name": "CTRE-Phoenix (v5)", "version": "5.36.0", "frcYear": "2026", - "uuid": "ab676553-b602-441f-a38d-f1296eff6537", + "uuid": "fbc886a4-2cec-40c0-9835-71086a8cc3df", "mavenUrls": [ "https://maven.ctr-electronics.com/release/" ], - "jsonUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix/Phoenix5-frc2026-latest.json", + "jsonUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix/Phoenix5-replay-frc2026-latest.json", "requires": [ { - "uuid": "e995de00-2c64-4df5-8831-c1441420ff19", + "uuid": "e7900d8d-826f-4dca-a1ff-182f658e98af", "errorMessage": "Phoenix 5 requires low-level libraries from Phoenix 6. Please add the Phoenix 6 vendordep before adding Phoenix 5.", - "offlineFileName": "Phoenix6-frc2026-latest.json", - "onlineUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix6/latest/Phoenix6-frc2026-latest.json" + "offlineFileName": "Phoenix6-replay-frc2026-latest.json", + "onlineUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix6/latest/Phoenix6-replay-frc2026-latest.json" } ], "conflictsWith": [ { - "uuid": "e7900d8d-826f-4dca-a1ff-182f658e98af", - "errorMessage": "Users must use the Phoenix 5 replay vendordep when using the Phoenix 6 replay vendordep.", - "offlineFileName": "Phoenix6-replay-frc2026-latest.json" + "uuid": "e995de00-2c64-4df5-8831-c1441420ff19", + "errorMessage": "Users must use the regular Phoenix 5 vendordep when using the regular Phoenix 6 vendordep.", + "offlineFileName": "Phoenix6-frc2026-latest.json" }, { - "uuid": "fbc886a4-2cec-40c0-9835-71086a8cc3df", + "uuid": "ab676553-b602-441f-a38d-f1296eff6537", "errorMessage": "Users cannot have both the replay and regular Phoenix 5 vendordeps in their robot program.", - "offlineFileName": "Phoenix5-replay-frc2026-latest.json" + "offlineFileName": "Phoenix5-frc2026-latest.json" } ], "javaDependencies": [ @@ -47,11 +47,22 @@ "version": "5.36.0", "isJar": false, "skipInvalidPlatforms": true, + "validPlatforms": [ + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix.replay", + "artifactId": "cci-replay", + "version": "5.36.0", + "isJar": false, + "skipInvalidPlatforms": true, "validPlatforms": [ "windowsx86-64", "linuxx86-64", "linuxarm64", - "linuxathena" + "osxuniversal" ], "simMode": "hwsim" }, @@ -80,9 +91,6 @@ "sharedLibrary": true, "skipInvalidPlatforms": true, "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "linuxarm64", "linuxathena" ], "simMode": "hwsim" @@ -96,9 +104,6 @@ "sharedLibrary": true, "skipInvalidPlatforms": true, "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "linuxarm64", "linuxathena" ], "simMode": "hwsim" @@ -111,11 +116,56 @@ "headerClassifier": "headers", "sharedLibrary": true, "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix.replay", + "artifactId": "wpiapi-cpp-replay", + "version": "5.36.0", + "libName": "CTRE_Phoenix_WPIReplay", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, "binaryPlatforms": [ "windowsx86-64", "linuxx86-64", "linuxarm64", - "linuxathena" + "osxuniversal" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix.replay", + "artifactId": "api-cpp-replay", + "version": "5.36.0", + "libName": "CTRE_PhoenixReplay", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix.replay", + "artifactId": "cci-replay", + "version": "5.36.0", + "libName": "CTRE_PhoenixCCIReplay", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" ], "simMode": "hwsim" }, diff --git a/vendordeps/Phoenix6-26.1.0.json b/vendordeps/Phoenix6-replay-26.1.0.json similarity index 87% rename from vendordeps/Phoenix6-26.1.0.json rename to vendordeps/Phoenix6-replay-26.1.0.json index 5d2d04d..27019ab 100644 --- a/vendordeps/Phoenix6-26.1.0.json +++ b/vendordeps/Phoenix6-replay-26.1.0.json @@ -1,18 +1,18 @@ { - "fileName": "Phoenix6-26.1.0.json", - "name": "CTRE-Phoenix (v6)", + "fileName": "Phoenix6-replay-26.1.0.json", + "name": "CTRE-Phoenix (v6) Replay", "version": "26.1.0", "frcYear": "2026", - "uuid": "e995de00-2c64-4df5-8831-c1441420ff19", + "uuid": "e7900d8d-826f-4dca-a1ff-182f658e98af", "mavenUrls": [ "https://maven.ctr-electronics.com/release/" ], - "jsonUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix6/latest/Phoenix6-frc2026-latest.json", + "jsonUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix6/latest/Phoenix6-replay-frc2026-latest.json", "conflictsWith": [ { - "uuid": "e7900d8d-826f-4dca-a1ff-182f658e98af", + "uuid": "e995de00-2c64-4df5-8831-c1441420ff19", "errorMessage": "Users can not have both the replay and regular Phoenix 6 vendordeps in their robot program.", - "offlineFileName": "Phoenix6-replay-frc2026-latest.json" + "offlineFileName": "Phoenix6-frc2026-latest.json" } ], "javaDependencies": [ @@ -30,9 +30,6 @@ "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "linuxarm64", "linuxathena" ], "simMode": "hwsim" @@ -43,11 +40,36 @@ "version": "26.1.0", "isJar": false, "skipInvalidPlatforms": true, + "validPlatforms": [ + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.replay", + "artifactId": "api-cpp-replay", + "version": "26.1.0", + "isJar": false, + "skipInvalidPlatforms": true, "validPlatforms": [ "windowsx86-64", "linuxx86-64", "linuxarm64", - "linuxathena" + "osxuniversal" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.replay", + "artifactId": "tools-replay", + "version": "26.1.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" ], "simMode": "hwsim" }, @@ -230,9 +252,6 @@ "sharedLibrary": true, "skipInvalidPlatforms": true, "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "linuxarm64", "linuxathena" ], "simMode": "hwsim" @@ -245,11 +264,40 @@ "headerClassifier": "headers", "sharedLibrary": true, "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.replay", + "artifactId": "wpiapi-cpp-replay", + "version": "26.1.0", + "libName": "CTRE_Phoenix6_WPIReplay", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, "binaryPlatforms": [ "windowsx86-64", "linuxx86-64", "linuxarm64", - "linuxathena" + "osxuniversal" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.replay", + "artifactId": "tools-replay", + "version": "26.1.0", + "libName": "CTRE_PhoenixTools_Replay", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" ], "simMode": "hwsim" }, diff --git a/vendordeps/maple-sim.json b/vendordeps/maple-sim.json index 6596d65..aa65f46 100644 --- a/vendordeps/maple-sim.json +++ b/vendordeps/maple-sim.json @@ -1,7 +1,7 @@ { "fileName": "maple-sim.json", "name": "maplesim", - "version": "0.3.14", + "version": "0.4.0-beta", "frcYear": "2026", "uuid": "c39481e8-4a63-4a4c-9df6-48d91e4da37b", "mavenUrls": [ @@ -13,7 +13,7 @@ { "groupId": "org.ironmaple", "artifactId": "maplesim-java", - "version": "0.3.14" + "version": "0.4.0-beta" }, { "groupId": "org.dyn4j", diff --git a/vendordeps/photonlib.json b/vendordeps/photonlib.json index 16e6991..d4075e7 100644 --- a/vendordeps/photonlib.json +++ b/vendordeps/photonlib.json @@ -1,7 +1,7 @@ { "fileName": "photonlib.json", "name": "photonlib", - "version": "v2026.1.1-rc-2", + "version": "v2026.1.1-rc-4", "uuid": "515fe07e-bfc6-11fa-b3de-0242ac130004", "frcYear": "2026", "mavenUrls": [ @@ -13,7 +13,7 @@ { "groupId": "org.photonvision", "artifactId": "photontargeting-cpp", - "version": "v2026.1.1-rc-2", + "version": "v2026.1.1-rc-4", "skipInvalidPlatforms": true, "isJar": false, "validPlatforms": [ @@ -28,7 +28,7 @@ { "groupId": "org.photonvision", "artifactId": "photonlib-cpp", - "version": "v2026.1.1-rc-2", + "version": "v2026.1.1-rc-4", "libName": "photonlib", "headerClassifier": "headers", "sharedLibrary": true, @@ -43,7 +43,7 @@ { "groupId": "org.photonvision", "artifactId": "photontargeting-cpp", - "version": "v2026.1.1-rc-2", + "version": "v2026.1.1-rc-4", "libName": "photontargeting", "headerClassifier": "headers", "sharedLibrary": true, @@ -60,12 +60,12 @@ { "groupId": "org.photonvision", "artifactId": "photonlib-java", - "version": "v2026.1.1-rc-2" + "version": "v2026.1.1-rc-4" }, { "groupId": "org.photonvision", "artifactId": "photontargeting-java", - "version": "v2026.1.1-rc-2" + "version": "v2026.1.1-rc-4" } ] } diff --git a/vendordeps/yagsl-2025.8.0.json b/vendordeps/yagsl-2025.8.0.json deleted file mode 100644 index 0c9ef4f..0000000 --- a/vendordeps/yagsl-2025.8.0.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "fileName": "yagsl-2025.8.0.json", - "name": "YAGSL", - "version": "2025.8.0", - "frcYear": "2026", - "uuid": "1ccce5a4-acd2-4d18-bca3-4b8047188400", - "mavenUrls": [ - "https://broncbotz3481.github.io/YAGSL-Lib/yagsl/repos" - ], - "jsonUrl": "https://broncbotz3481.github.io/YAGSL-Lib/yagsl/yagsl.json", - "javaDependencies": [ - { - "groupId": "swervelib", - "artifactId": "YAGSL-java", - "version": "2025.8.0" - } - ], - "requires": [ - { - "uuid": "3f48eb8c-50fe-43a6-9cb7-44c86353c4cb", - "errorMessage": "REVLib is required!", - "offlineFileName": "REVLib-2025.0.0.json", - "onlineUrl": "https://software-metadata.revrobotics.com/REVLib-2025.json" - }, - { - "uuid": "151ecca8-670b-4026-8160-cdd2679ef2bd", - "errorMessage": "ReduxLib is required!", - "offlineFileName": "ReduxLib-2025.0.0.json", - "onlineUrl": "https://frcsdk.reduxrobotics.com/ReduxLib_2025.json" - }, - { - "uuid": "e995de00-2c64-4df5-8831-c1441420ff19", - "errorMessage": "Phoenix6 is required!", - "offlineFileName": "Phoenix6-25.1.0.json", - "onlineUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix6/latest/Phoenix6-frc2025-latest.json" - }, - { - "uuid": "ab676553-b602-441f-a38d-f1296eff6537", - "errorMessage": "Phoenix5 is required!", - "offlineFileName": "Phoenix5-5.35.0.json", - "onlineUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix/Phoenix5-frc2025-latest.json" - }, - { - "uuid": "cb311d09-36e9-4143-a032-55bb2b94443b", - "errorMessage": "Studica is required!", - "offlineFileName": "Studica-2025.0.0.json", - "onlineUrl": "https://dev.studica.com/releases/2025/Studica-2025.0.0.json" - }, - { - "uuid": "60b2694b-9e6e-4026-81ee-6f167946f4b0", - "errorMessage": "ThriftyLib is required!", - "offlineFileName": "ThriftyLib.json", - "onlineUrl": "https://docs.home.thethriftybot.com/ThriftyLib.json" - }, - { - "uuid": "c39481e8-4a63-4a4c-9df6-48d91e4da37b", - "errorMessage": "maple-sim is required for simulation", - "offlineFileName": "maple-sim.json", - "onlineUrl": "https://shenzhen-robotics-alliance.github.io/maple-sim/vendordep/maple-sim.json" - } - ], - "jniDependencies": [], - "cppDependencies": [] -} diff --git a/vendordeps/yagsl-2026.1.17.json b/vendordeps/yagsl-2026.1.17.json new file mode 100644 index 0000000..2ad9a4f --- /dev/null +++ b/vendordeps/yagsl-2026.1.17.json @@ -0,0 +1,58 @@ +{ + "fileName": "yagsl-2026.1.17.json", + "name": "YAGSL", + "version": "2026.1.17", + "frcYear": "2026", + "uuid": "1ccce5a4-acd2-4d18-bca3-4b8047188400", + "mavenUrls": [ + "https://yet-another-software-suite.github.io/YAGSL/releases/", + "https://repo1.maven.org/maven2" + ], + "jsonUrl": "https://yet-another-software-suite.github.io/YAGSL/yagsl.json", + "javaDependencies": [ + { + "groupId": "swervelib", + "artifactId": "YAGSL-java", + "version": "2026.1.17" + }, + { + "groupId": "org.dyn4j", + "artifactId": "dyn4j", + "version": "5.0.2" + } + ], + "requires": [ + { + "uuid": "151ecca8-670b-4026-8160-cdd2679ef2bd", + "errorMessage": "ReduxLib is required!", + "offlineFileName": "ReduxLib.json", + "onlineUrl": "https://frcsdk.reduxrobotics.com/ReduxLib_2026.json" + }, + { + "uuid": "3f48eb8c-50fe-43a6-9cb7-44c86353c4cb", + "errorMessage": "REVLib is required!", + "offlineFileName": "REVLib.json", + "onlineUrl": "https://software-metadata.revrobotics.com/REVLib-2026.json" + }, + { + "uuid": "e7900d8d-826f-4dca-a1ff-182f658e98af", + "errorMessage": "Phoenix6 Replay is required!", + "offlineFileName": "Phoenix6-replay-26.1.0.json", + "onlineUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix6/latest/Phoenix6-replay-frc2026-latest.json" + }, + { + "uuid": "fbc886a4-2cec-40c0-9835-71086a8cc3df", + "errorMessage": "Phoenix5 Replay Compatibility is required!", + "offlineFileName": "Phoenix5-replay-5.36.0.json", + "onlineUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix/Phoenix5-replay-frc2026-latest.json" + }, + { + "uuid": "60b2694b-9e6e-4026-81ee-6f167946f4b0", + "errorMessage": "ThriftyLib is required!", + "offlineFileName": "ThriftyLib.json", + "onlineUrl": "https://docs.home.thethriftybot.com/ThriftyLib-2026.json" + } + ], + "jniDependencies": [], + "cppDependencies": [] +}