From c65c505504fbcea4c58c29a76fbad7d5136818e7 Mon Sep 17 00:00:00 2001 From: IanTapply22 Date: Sat, 11 Nov 2023 14:01:32 -0500 Subject: [PATCH 01/14] Updated to 2024 beta --- .gitignore | 10 +++++++ .wpilib/wpilib_preferences.json | 2 +- WPILib-License.md | 2 +- build.gradle | 9 ++++-- gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++- gradlew | 35 ++++++++++++++--------- gradlew.bat | 1 + settings.gradle | 5 +++- vendordeps/WPILibNewCommands.json | 1 + 10 files changed, 49 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 9535c83..a8d1911 100644 --- a/.gitignore +++ b/.gitignore @@ -158,5 +158,15 @@ gradle-app.setting .settings/ bin/ +# IntelliJ +*.iml +*.ipr +*.iws +.idea/ +out/ + +# Fleet +.fleet + # Simulation GUI and other tools window save file *-window.json diff --git a/.wpilib/wpilib_preferences.json b/.wpilib/wpilib_preferences.json index ad66d9e..18fa281 100644 --- a/.wpilib/wpilib_preferences.json +++ b/.wpilib/wpilib_preferences.json @@ -1,6 +1,6 @@ { "enableCppIntellisense": false, "currentLanguage": "java", - "projectYear": "2023", + "projectYear": "2024beta", "teamNumber": 1114 } \ No newline at end of file diff --git a/WPILib-License.md b/WPILib-License.md index 3d5a824..43b62ec 100644 --- a/WPILib-License.md +++ b/WPILib-License.md @@ -1,4 +1,4 @@ -Copyright (c) 2009-2021 FIRST and other WPILib contributors +Copyright (c) 2009-2023 FIRST and other WPILib contributors All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/build.gradle b/build.gradle index b233ef1..10ee205 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,12 @@ plugins { id "java" - id "edu.wpi.first.GradleRIO" version "2023.4.3" + id "edu.wpi.first.GradleRIO" version "2024.1.1-beta-3" } -sourceCompatibility = JavaVersion.VERSION_11 -targetCompatibility = JavaVersion.VERSION_11 +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} def ROBOT_MAIN_CLASS = "frc.robot.Main" @@ -84,6 +86,7 @@ wpi.sim.addDriverstation() // knows where to look for our Robot Class. jar { from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } + from sourceSets.main.allSource manifest edu.wpi.first.gradlerio.GradleRIOPlugin.javaManifest(ROBOT_MAIN_CLASS) duplicatesStrategy = DuplicatesStrategy.INCLUDE } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832f090a2944b7473328c07c9755baa3196..7f93135c49b765f8051ef9d0a6055ff8e46073d8 100644 GIT binary patch delta 44091 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 F{tsc4gtPzv delta 41205 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 jFA#1K+D2e>Q|zP /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +131,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index f127cfd..93e3f59 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% diff --git a/settings.gradle b/settings.gradle index 48c039e..d94f73c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,7 +4,7 @@ pluginManagement { repositories { mavenLocal() gradlePluginPortal() - String frcYear = '2023' + String frcYear = '2024' File frcHome if (OperatingSystem.current().isWindows()) { String publicFolder = System.getenv('PUBLIC') @@ -25,3 +25,6 @@ pluginManagement { } } } + +Properties props = System.getProperties(); +props.setProperty("org.gradle.internal.native.headers.unresolved.dependencies.ignore", "true"); diff --git a/vendordeps/WPILibNewCommands.json b/vendordeps/WPILibNewCommands.json index bd535bf..67bf389 100644 --- a/vendordeps/WPILibNewCommands.json +++ b/vendordeps/WPILibNewCommands.json @@ -3,6 +3,7 @@ "name": "WPILib-New-Commands", "version": "1.0.0", "uuid": "111e20f7-815e-48f8-9dd6-e675ce75b266", + "frcYear": "2024", "mavenUrls": [], "jsonUrl": "", "javaDependencies": [ From 13daaa7c61618fdbcc85aec6dcb8d88ffd304304 Mon Sep 17 00:00:00 2001 From: Ian Tapply Date: Sat, 11 Nov 2023 19:55:30 -0800 Subject: [PATCH 02/14] Update 2024-beta branch (#24) Update 2024-beta branch --------- Co-authored-by: Cole MacPhail --- .github/ISSUE_TEMPLATE/bug_report.md | 30 +++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 +++++++++++++ .github/pull_request_template.md | 35 +++++++++++++++++++++++ README.md | 18 ++++++++++++ 4 files changed, 103 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/pull_request_template.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..a88c0da --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,30 @@ +--- +name: Bug report +about: Something's wrong and it shouldn't be +title: "[BUG]" +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behaviour: +1. Start robot in teleop +2. Press button '...' +3. Drive forwards +4. Something breaks + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Media** +If applicable, add screenshots, pictures or videos to help explain your problem. + +**Potential Resolution** +If you think you know what's causing the problem, mention it here. Otherwise, delete this section. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..190ff9a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: I can't do something and I want to +title: "[FEATURE]" +labels: enhancement +assignees: '' + +--- + +**Problem Description** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...], It should be easier to do [...] + +**Desired Behaviour** +A clear and concise description of what you want to happen. Ex. When I press the A button, I want to lock to a vision target. + +**Alternatives considered** +A clear and concise description of any alternative solutions or features you've considered, if any. Ex. Instead of locking to a vision target, the robot could lock to the angle of the loading station. + +**Additional context** +Add any other context about the feature request here. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..705dbad --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,35 @@ +# Description + +Please include a summary of the changes and the related issue. Additionally, include relevant motivation and context. List any dependencies that are required for this change. + +Fixes # (issue) + +## Type of change + +Please delete options that are not relevant. + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update + +# How Has This Been Tested? + +Depending on the change, this may not be necessary. If so, please delete this section. + +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. + +If this was tested on a real robot, please attach or link to a video of the robot behaving as expected. + +- [ ] Test A +- [ ] Test B + +# Checklist: + +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation, if any +- [ ] My changes generate no new warnings +- [ ] I have performed tests that prove my fix is effective or that my feature works, if necessary +- [ ] New and existing unit tests pass locally with my changes diff --git a/README.md b/README.md index 2695cf7..4a0c12d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,20 @@ # Simbot-Base A base robot with the most up to date utilities and starting components to get started on robot code as fast as possible + +## Contributing +Want to contribute to this project? We make it as simple as possible to open a pull request and contribute to the codebase to make it the best it can. To open a pull request and contribute code, it's easy! Just follow these steps: + +1. Fork the repository and either contribute to the `main` branch on that repository, or make a new branch on that forked repository. +2. Once you have pushed all of the changes to your forked repository, go to [the pull request section](https://github.com/Simbotics/2023-Simbot/pulls) in our repository and click the `new pull request` button on the right side of your screen. +3. When you are on the pull request screen, set the compare branch to the one on your forked repository and set the base branch to be the `main` branch. We also kindly ask that you add a short description of the changes you have to our codebase in the description section of the pull request. Please be sure that you set an appropriate title for your pull request. +4. Finally, once you have reviewed your contributions just hit the `create pull request` button and you're all set! + +_Note: Please await the review of one or more repository managers until your code changes are pushed to main._ **ALL GITHUB ACTIONS MUST PASS IN A PULL REQUEST TO BE ELIGEABLE TO BE MERGED** + +## Contributors and Credits +Thank you to the following who has contributed, supplied the tools to make this project even possible, or has supported this project along the way. + +- [CTR Electronics](https://github.com/CrossTheRoadElec) - Provided an example and starter [repository](https://github.com/CrossTheRoadElec/Phoenix6-Examples/tree/main/java/SwerveWithPathPlanner) to build from +- [Ian Tapply](https://github.com/IanTapply22) - Main student contributor +- [Kaleb Dodd](https://github.com/kaleb-dodd) - Project coordinator +- [Cole MacPhail](https://github.com/colemacphail) - Project coordinator \ No newline at end of file From b803e88a2e4d0ce65b2121b2686ffc3c5e26643f Mon Sep 17 00:00:00 2001 From: Cobblestone Date: Sun, 12 Nov 2023 00:16:24 -0500 Subject: [PATCH 03/14] Port controller input (#23) # Description Support for the Logitech F310 controller has been added Fixes #6 ## Type of change - [X] New feature (non-breaking change which adds functionality) # How Has This Been Tested? - All axes and buttons used in the old `LogitechF310` were checked against the values used internally by WPILib's `CommandXboxController` to ensure they matched. This should prevent triggers from being bound to the wrong key. # Checklist: - [ ] My code follows the style guidelines of this project - [X] I have performed a self-review of my code - [X] I have commented my code, particularly in hard-to-understand areas - [X] I have made corresponding changes to the documentation, if any - [X] My changes generate no new warnings - [X] I have performed tests that prove my fix is effective or that my feature works, if necessary - [ ] New and existing unit tests pass locally with my changes --------- Co-authored-by: IanTapply22 Co-authored-by: Cole MacPhail --- .../java/frc/robot/CommandController.java | 230 ++++++++++++++++++ src/main/java/frc/robot/RobotContainer.java | 22 +- 2 files changed, 241 insertions(+), 11 deletions(-) create mode 100644 src/main/java/frc/robot/CommandController.java diff --git a/src/main/java/frc/robot/CommandController.java b/src/main/java/frc/robot/CommandController.java new file mode 100644 index 0000000..a866512 --- /dev/null +++ b/src/main/java/frc/robot/CommandController.java @@ -0,0 +1,230 @@ +package frc.robot; + +import edu.wpi.first.wpilibj.GenericHID.RumbleType; +import edu.wpi.first.wpilibj2.command.button.CommandXboxController; +import edu.wpi.first.wpilibj2.command.button.Trigger; + +/** + * The {@code CommandController} class acts as a wrapper around an existing + * {@code CommandXboxController}, providing developers with the ability to rename its + * methods for improved clarity on the functionality of each button. + * + *

By extending this class, developers can use more intuitive method names + * that align with the specific actions associated with each button on the + * controller. This aims to enhance code readability and understanding, making it + * easier for developers to work with the controller in a more expressive manner.

+ * + *

Example usage:

+ *
+ * {@code
+ * CommandController driver = new CommandController(0);
+ * driver.greenButton().onTrue(() -> {}); // insert command
+ * + * @author Simbotics + * + * @see CommandXboxController + */ +public class CommandController { + CommandXboxController joystick; + + public CommandController(int port) { + this.joystick = new CommandXboxController(port); + } + + /** + * Gets the left joystick X axis. + * + * @return The left joystick X axis. + */ + public double getLeftX() { + return this.joystick.getLeftX(); + } + + /** + * Gets the left joystick Y axis. + * + * @return The left joystick Y axis. + */ + public double getLeftY() { + return -this.joystick.getLeftY(); + } + + /** + * Gets the pressed state of the left trigger. + * + * @return The pressed state of the left trigger as a trigger. + */ + public Trigger leftTrigger() { + return this.joystick.rightTrigger(); + } + + /** + * Gets the pressed state of the right trigger. + * + * @return The pressed state of the right trigger as a trigger. + */ + public Trigger rightTrigger() { + return this.joystick.rightTrigger(); + } + + /** + * Gets the right joystick X axis. + * + * @return The right joystick X axis. + */ + public double getRightX() { + return this.joystick.getRightX(); + } + + /** + * Gets the right joystick Y axis. + * + * @return The right joystick Y axis. + */ + public double getRightY() { + return -this.joystick.getRightY(); + } + + /** + * Gets the green buttons trigger. + * + * @return the trigger for the green button + */ + public Trigger greenButton() { + return this.joystick.a(); + } + + /** + * Gets the blue buttons trigger. + * + * @return the trigger for the blue button + */ + public Trigger blueButton() { + return this.joystick.x(); + } + + /** + * Gets the red buttons trigger. + * + * @return the trigger for the red button + */ + public Trigger redButton() { + return this.joystick.b(); + } + + /** + * Gets the yellow buttons trigger. + * + * @return the trigger for the yellow button + */ + public Trigger yellowButton() { + return this.joystick.y(); + } + + /** + * Gets the back button trigger + * + * @return the trigger for the back button + */ + public Trigger backButton() { + return this.joystick.back(); + } + +/** + * Gets the back button trigger + * + * @return the trigger for the back button + */ + public Trigger startButton() { + return this.joystick.start(); + } + + /** + * Gets the pressed state of the top left back button. + * + * @return The value of the pressed state as a trigger. + */ + public Trigger leftBumper() { + return this.joystick.leftBumper(); + } + + /** + * Gets the pressed state of the top right back button. + * + * @return The value of the pressed state as a trigger. + */ + public Trigger rightBumper() { + return this.joystick.rightBumper(); + } + + /** + * Gets the pressed state of the left joystick button. + * + * @return The state of the left joystick button as a trigger. + */ + public Trigger leftStickClick() { + return this.joystick.button(9); + } + + /** + * Gets the pressed state of the right joystick button. + * + * @return The state of the of the right joystick button as a boolean. + */ + public Trigger rightStickClick() { + return this.joystick.button(10); + } + + /** POV methods. */ + public int getPOVVal() { + return this.joystick.getHID().getPOV(0); + } + + /** + * Get trigger for when POV angle is 180 + * + * @return trigger for when POV is 180 + */ + public Trigger POVDown() { + return this.joystick.povDown(); + } + + /** + * Get trigger for when POV angle is 90 + * + * @return trigger for when POV is 90 + */ + public Trigger POVRight() { + return this.joystick.povRight(); + } + + + /** + * Get trigger for when POV angle is 0 + * + * @return trigger for when POV is 0 + */ + public Trigger POVUp() { + return this.joystick.povUp(); + } + + + /** + * Get trigger for when POV angle is 270 + * + * @return trigger for when POV is 270 + */ + public Trigger POVLeft() { + return this.joystick.povLeft(); + } + + /** + * Sets the amount of rumble for supported controllers. + * + * @param rumble Amount of rumble; Either 0 or 1. + */ + public void setRumble(double rumble) { + this.joystick.getHID().setRumble(RumbleType.kLeftRumble, rumble); + this.joystick.getHID().setRumble(RumbleType.kRightRumble, rumble); + } +} diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java index cabed9b..02c61ff 100644 --- a/src/main/java/frc/robot/RobotContainer.java +++ b/src/main/java/frc/robot/RobotContainer.java @@ -10,7 +10,6 @@ import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.InstantCommand; -import edu.wpi.first.wpilibj2.command.button.CommandXboxController; import frc.robot.generated.TunerConstants; public class RobotContainer { @@ -18,7 +17,8 @@ public class RobotContainer { final double MaxAngularRate = Math.PI; // Half a rotation per second max angular velocity /* Setting up bindings for necessary control of the swerve drive platform */ - CommandXboxController joystick = new CommandXboxController(0); // My joystick + CommandController driver = new CommandController(0); // Driver Controller + CommandController operator = new CommandController(1); // Operator Controller CommandSwerveDrivetrain drivetrain = TunerConstants.DriveTrain; // My drivetrain SwerveRequest.FieldCentric drive = new SwerveRequest.FieldCentric().withIsOpenLoop(true).withDeadband(MaxSpeed * 0.1).withRotationalDeadband(MaxAngularRate * 0.1); // I want field-centric // driving in open loop @@ -35,23 +35,23 @@ public class RobotContainer { private void configureBindings() { drivetrain.setDefaultCommand( // Drivetrain will execute this command periodically - drivetrain.applyRequest(() -> drive.withVelocityX(-joystick.getLeftY() * MaxSpeed) // Drive forward with + drivetrain.applyRequest(() -> drive.withVelocityX(-driver.getLeftY() * MaxSpeed) // Drive forward with // negative Y (forward) - .withVelocityY(-joystick.getLeftX() * MaxSpeed) // Drive left with negative X (left) - .withRotationalRate(-joystick.getRightX() * MaxAngularRate) // Drive counterclockwise with negative X (left) + .withVelocityY(-driver.getLeftX() * MaxSpeed) // Drive left with negative X (left) + .withRotationalRate(-driver.getRightX() * MaxAngularRate) // Drive counterclockwise with negative X (left) ).ignoringDisable(true)); - joystick.a().whileTrue(drivetrain.applyRequest(() -> brake)); - joystick.b().whileTrue(drivetrain - .applyRequest(() -> point.withModuleDirection(new Rotation2d(-joystick.getLeftY(), -joystick.getLeftX())))); + driver.greenButton().whileTrue(drivetrain.applyRequest(() -> brake)); + driver.yellowButton().whileTrue(drivetrain + .applyRequest(() -> point.withModuleDirection(new Rotation2d(-driver.getLeftY(), -driver.getLeftX())))); // if (Utils.isSimulation()) { // drivetrain.seedFieldRelative(new Pose2d(new Translation2d(), Rotation2d.fromDegrees(90))); // } drivetrain.registerTelemetry(logger::telemeterize); - joystick.pov(0).whileTrue(drivetrain.applyRequest(()->forwardStraight.withVelocityX(0.5).withVelocityY(0))); - joystick.pov(180).whileTrue(drivetrain.applyRequest(()->forwardStraight.withVelocityX(-0.5).withVelocityY(0))); + driver.POVUp().whileTrue(drivetrain.applyRequest(()->forwardStraight.withVelocityX(0.5).withVelocityY(0))); + driver.POVDown().whileTrue(drivetrain.applyRequest(()->forwardStraight.withVelocityX(-0.5).withVelocityY(0))); } public RobotContainer() { @@ -64,6 +64,6 @@ public Command getAutonomousCommand() { } public boolean seedPoseButtonDown() { - return joystick.leftBumper().getAsBoolean(); + return driver.leftBumper().getAsBoolean(); } } From f5625fd4c9dc7d39dabfa1a2d022ec38e64c443b Mon Sep 17 00:00:00 2001 From: Ian Tapply Date: Sun, 12 Nov 2023 11:54:40 -0800 Subject: [PATCH 04/14] Sync beta branch with main (#27) Sync beta branch with main --------- Co-authored-by: Cole MacPhail Co-authored-by: github-actions <> --- .github/workflows/build.yml | 30 + .github/workflows/format.yml | 22 + CONTRIBUTING.md | 9 + README.md | 10 - .../java/frc/robot/CommandController.java | 32 +- .../frc/robot/CommandSwerveDrivetrain.java | 120 +- src/main/java/frc/robot/LimelightHelpers.java | 1241 ++++++++--------- src/main/java/frc/robot/Robot.java | 9 +- src/main/java/frc/robot/RobotContainer.java | 52 +- src/main/java/frc/robot/Telemetry.java | 203 +-- .../frc/robot/generated/TunerConstants.java | 206 +-- 11 files changed, 1025 insertions(+), 909 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/format.yml create mode 100644 CONTRIBUTING.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..39bb4bb --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,30 @@ +name: Build + +# Controls when the action will run. Triggers the workflow on push to all branches. +on: [ push ] + +jobs: + + # Build our code to see if it can be deployed without errors + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # This grabs the WPILib docker container + container: wpilib/roborio-cross-ubuntu:2023-22.04 + + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v3 + + # Declares the repository safe and not under dubious ownership. + - name: Add repository to git safe directories + run: git config --global --add safe.directory $GITHUB_WORKSPACE + + # Grant execute permission for gradlew + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + # Runs a single command using the runners shell + - name: Compile and run tests on robot code + run: ./gradlew build diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 0000000..251b269 --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,22 @@ +name: Format + +on: + pull_request: + branches: + - main + - 2024-beta + +jobs: + + formatting: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.ref }} + - uses: axel-op/googlejavaformat-action@v3 + with: + args: "--skip-sorting-imports --replace" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8a158f9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,9 @@ +## Contributing +Want to contribute to this project? We make it as simple as possible to open a pull request and contribute to the codebase to make it the best it can. To open a pull request and contribute code, it's easy! Just follow these steps: + +1. Fork the repository and either contribute to the `main` branch on that repository, or make a new branch on that forked repository. +2. Once you have pushed all of the changes to your forked repository, go to [the pull request section](https://github.com/Simbotics/2023-Simbot/pulls) in our repository and click the `new pull request` button on the right side of your screen. +3. When you are on the pull request screen, set the compare branch to the one on your forked repository and set the base branch to be the `main` branch. We also kindly ask that you add a short description of the changes you have to our codebase in the description section of the pull request. Please be sure that you set an appropriate title for your pull request. +4. Finally, once you have reviewed your contributions just hit the `create pull request` button and you're all set! + +_Note: Please await the review of one or more repository managers until your code changes are pushed to main._ **ALL GITHUB ACTIONS MUST PASS IN A PULL REQUEST TO BE ELIGIBLE TO BE MERGED** \ No newline at end of file diff --git a/README.md b/README.md index 4a0c12d..b07a9e9 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,6 @@ # Simbot-Base A base robot with the most up to date utilities and starting components to get started on robot code as fast as possible -## Contributing -Want to contribute to this project? We make it as simple as possible to open a pull request and contribute to the codebase to make it the best it can. To open a pull request and contribute code, it's easy! Just follow these steps: - -1. Fork the repository and either contribute to the `main` branch on that repository, or make a new branch on that forked repository. -2. Once you have pushed all of the changes to your forked repository, go to [the pull request section](https://github.com/Simbotics/2023-Simbot/pulls) in our repository and click the `new pull request` button on the right side of your screen. -3. When you are on the pull request screen, set the compare branch to the one on your forked repository and set the base branch to be the `main` branch. We also kindly ask that you add a short description of the changes you have to our codebase in the description section of the pull request. Please be sure that you set an appropriate title for your pull request. -4. Finally, once you have reviewed your contributions just hit the `create pull request` button and you're all set! - -_Note: Please await the review of one or more repository managers until your code changes are pushed to main._ **ALL GITHUB ACTIONS MUST PASS IN A PULL REQUEST TO BE ELIGEABLE TO BE MERGED** - ## Contributors and Credits Thank you to the following who has contributed, supplied the tools to make this project even possible, or has supported this project along the way. diff --git a/src/main/java/frc/robot/CommandController.java b/src/main/java/frc/robot/CommandController.java index a866512..da7dbb1 100644 --- a/src/main/java/frc/robot/CommandController.java +++ b/src/main/java/frc/robot/CommandController.java @@ -19,19 +19,19 @@ * {@code * CommandController driver = new CommandController(0); * driver.greenButton().onTrue(() -> {}); // insert command - * + * * @author Simbotics - * + * * @see CommandXboxController */ public class CommandController { - CommandXboxController joystick; + CommandXboxController joystick; + + public CommandController(int port) { + this.joystick = new CommandXboxController(port); + } - public CommandController(int port) { - this.joystick = new CommandXboxController(port); - } - - /** + /** * Gets the left joystick X axis. * * @return The left joystick X axis. @@ -123,16 +123,16 @@ public Trigger yellowButton() { /** * Gets the back button trigger - * + * * @return the trigger for the back button */ public Trigger backButton() { return this.joystick.back(); } -/** + /** * Gets the back button trigger - * + * * @return the trigger for the back button */ public Trigger startButton() { @@ -182,7 +182,7 @@ public int getPOVVal() { /** * Get trigger for when POV angle is 180 - * + * * @return trigger for when POV is 180 */ public Trigger POVDown() { @@ -191,27 +191,25 @@ public Trigger POVDown() { /** * Get trigger for when POV angle is 90 - * + * * @return trigger for when POV is 90 */ public Trigger POVRight() { return this.joystick.povRight(); } - /** * Get trigger for when POV angle is 0 - * + * * @return trigger for when POV is 0 */ public Trigger POVUp() { return this.joystick.povUp(); } - /** * Get trigger for when POV angle is 270 - * + * * @return trigger for when POV is 270 */ public Trigger POVLeft() { diff --git a/src/main/java/frc/robot/CommandSwerveDrivetrain.java b/src/main/java/frc/robot/CommandSwerveDrivetrain.java index 884b426..5211b54 100644 --- a/src/main/java/frc/robot/CommandSwerveDrivetrain.java +++ b/src/main/java/frc/robot/CommandSwerveDrivetrain.java @@ -20,66 +20,80 @@ import edu.wpi.first.wpilibj2.command.Subsystem; /** - * Class that extends the Phoenix SwerveDrivetrain class and implements subsystem - * so it can be used in command-based projects easily. + * Class that extends the Phoenix SwerveDrivetrain class and implements subsystem so it can be used + * in command-based projects easily. */ public class CommandSwerveDrivetrain extends SwerveDrivetrain implements Subsystem { - private SwerveRequest.ApplyChassisSpeeds autoRequest = new SwerveRequest.ApplyChassisSpeeds(); - public CommandSwerveDrivetrain(SwerveDrivetrainConstants driveTrainConstants, double OdometryUpdateFrequency, SwerveModuleConstants... modules) { - super(driveTrainConstants, OdometryUpdateFrequency, modules); - configurePathPlanner(); - } - public CommandSwerveDrivetrain(SwerveDrivetrainConstants driveTrainConstants, SwerveModuleConstants... modules) { - super(driveTrainConstants, modules); - configurePathPlanner(); - } + private SwerveRequest.ApplyChassisSpeeds autoRequest = new SwerveRequest.ApplyChassisSpeeds(); - private void configurePathPlanner() { - AutoBuilder.configureHolonomic( - ()->this.getState().Pose, // Supplier of current robot pose - this::seedFieldRelative, // Consumer for seeding pose against auto - this::getCurrentRobotChassisSpeeds, - (speeds)->this.setControl(autoRequest.withSpeeds(speeds)), // Consumer of ChassisSpeeds to drive the robot - new HolonomicPathFollowerConfig(new PIDConstants(10, 0, 0), - new PIDConstants(10, 0, 0), - 1, - 1, - new ReplanningConfig(), - 0.004), - this); // Subsystem for requirements - } + public CommandSwerveDrivetrain( + SwerveDrivetrainConstants driveTrainConstants, + double OdometryUpdateFrequency, + SwerveModuleConstants... modules) { + super(driveTrainConstants, OdometryUpdateFrequency, modules); + configurePathPlanner(); + } - public Command applyRequest(Supplier requestSupplier) { - return new RunCommand(()->{this.setControl(requestSupplier.get());}, this); - } + public CommandSwerveDrivetrain( + SwerveDrivetrainConstants driveTrainConstants, SwerveModuleConstants... modules) { + super(driveTrainConstants, modules); + configurePathPlanner(); + } - public Command getAutoPath(String pathName) { - return new PathPlannerAuto(pathName); - } + private void configurePathPlanner() { + AutoBuilder.configureHolonomic( + () -> this.getState().Pose, // Supplier of current robot pose + this::seedFieldRelative, // Consumer for seeding pose against auto + this::getCurrentRobotChassisSpeeds, + (speeds) -> + this.setControl( + autoRequest.withSpeeds(speeds)), // Consumer of ChassisSpeeds to drive the robot + new HolonomicPathFollowerConfig( + new PIDConstants(10, 0, 0), + new PIDConstants(10, 0, 0), + 1, + 1, + new ReplanningConfig(), + 0.004), + this); // Subsystem for requirements + } - @Override - public void simulationPeriodic() { - /* Assume */ - updateSimState(0.02, 12); - } - /** - * Takes the specified location and makes it the current pose for - * field-relative maneuvers - * - * @param location Pose to make the current pose at. - */ - @Override - public void seedFieldRelative(Pose2d location) { - try { - m_stateLock.writeLock().lock(); + public Command applyRequest(Supplier requestSupplier) { + return new RunCommand( + () -> { + this.setControl(requestSupplier.get()); + }, + this); + } - m_odometry.resetPosition(Rotation2d.fromDegrees(m_pigeon2.getYaw().getValue()), m_modulePositions, location); - } finally { - m_stateLock.writeLock().unlock(); - } - } + public Command getAutoPath(String pathName) { + return new PathPlannerAuto(pathName); + } + + @Override + public void simulationPeriodic() { + /* Assume */ + updateSimState(0.02, 12); + } - public ChassisSpeeds getCurrentRobotChassisSpeeds() { - return m_kinematics.toChassisSpeeds(getState().ModuleStates); + /** + * Takes the specified location and makes it the current pose for field-relative maneuvers + * + * @param location Pose to make the current pose at. + */ + @Override + public void seedFieldRelative(Pose2d location) { + try { + m_stateLock.writeLock().lock(); + + m_odometry.resetPosition( + Rotation2d.fromDegrees(m_pigeon2.getYaw().getValue()), m_modulePositions, location); + } finally { + m_stateLock.writeLock().unlock(); } + } + + public ChassisSpeeds getCurrentRobotChassisSpeeds() { + return m_kinematics.toChassisSpeeds(getState().ModuleStates); + } } diff --git a/src/main/java/frc/robot/LimelightHelpers.java b/src/main/java/frc/robot/LimelightHelpers.java index 7aae8fb..9e162e8 100644 --- a/src/main/java/frc/robot/LimelightHelpers.java +++ b/src/main/java/frc/robot/LimelightHelpers.java @@ -1,4 +1,4 @@ -//LimelightHelpers v1.2.1 (March 1, 2023) +// LimelightHelpers v1.2.1 (March 1, 2023) package frc.robot; @@ -28,753 +28,740 @@ public class LimelightHelpers { - public static class LimelightTarget_Retro { - - @JsonProperty("t6c_ts") - private double[] cameraPose_TargetSpace; - - @JsonProperty("t6r_fs") - private double[] robotPose_FieldSpace; - - @JsonProperty("t6r_ts") - private double[] robotPose_TargetSpace; - - @JsonProperty("t6t_cs") - private double[] targetPose_CameraSpace; - - @JsonProperty("t6t_rs") - private double[] targetPose_RobotSpace; - - public Pose3d getCameraPose_TargetSpace() - { - return toPose3D(cameraPose_TargetSpace); - } - public Pose3d getRobotPose_FieldSpace() - { - return toPose3D(robotPose_FieldSpace); - } - public Pose3d getRobotPose_TargetSpace() - { - return toPose3D(robotPose_TargetSpace); - } - public Pose3d getTargetPose_CameraSpace() - { - return toPose3D(targetPose_CameraSpace); - } - public Pose3d getTargetPose_RobotSpace() - { - return toPose3D(targetPose_RobotSpace); - } - - public Pose2d getCameraPose_TargetSpace2D() - { - return toPose2D(cameraPose_TargetSpace); - } - public Pose2d getRobotPose_FieldSpace2D() - { - return toPose2D(robotPose_FieldSpace); - } - public Pose2d getRobotPose_TargetSpace2D() - { - return toPose2D(robotPose_TargetSpace); - } - public Pose2d getTargetPose_CameraSpace2D() - { - return toPose2D(targetPose_CameraSpace); - } - public Pose2d getTargetPose_RobotSpace2D() - { - return toPose2D(targetPose_RobotSpace); - } - - @JsonProperty("ta") - public double ta; - - @JsonProperty("tx") - public double tx; - - @JsonProperty("txp") - public double tx_pixels; - - @JsonProperty("ty") - public double ty; - - @JsonProperty("typ") - public double ty_pixels; - - @JsonProperty("ts") - public double ts; - - public LimelightTarget_Retro() { - cameraPose_TargetSpace = new double[6]; - robotPose_FieldSpace = new double[6]; - robotPose_TargetSpace = new double[6]; - targetPose_CameraSpace = new double[6]; - targetPose_RobotSpace = new double[6]; - } - - } - - public static class LimelightTarget_Fiducial { - - @JsonProperty("fID") - public double fiducialID; - - @JsonProperty("fam") - public String fiducialFamily; - - @JsonProperty("t6c_ts") - private double[] cameraPose_TargetSpace; - - @JsonProperty("t6r_fs") - private double[] robotPose_FieldSpace; - - @JsonProperty("t6r_ts") - private double[] robotPose_TargetSpace; - - @JsonProperty("t6t_cs") - private double[] targetPose_CameraSpace; - - @JsonProperty("t6t_rs") - private double[] targetPose_RobotSpace; - - public Pose3d getCameraPose_TargetSpace() - { - return toPose3D(cameraPose_TargetSpace); - } - public Pose3d getRobotPose_FieldSpace() - { - return toPose3D(robotPose_FieldSpace); - } - public Pose3d getRobotPose_TargetSpace() - { - return toPose3D(robotPose_TargetSpace); - } - public Pose3d getTargetPose_CameraSpace() - { - return toPose3D(targetPose_CameraSpace); - } - public Pose3d getTargetPose_RobotSpace() - { - return toPose3D(targetPose_RobotSpace); - } - - public Pose2d getCameraPose_TargetSpace2D() - { - return toPose2D(cameraPose_TargetSpace); - } - public Pose2d getRobotPose_FieldSpace2D() - { - return toPose2D(robotPose_FieldSpace); - } - public Pose2d getRobotPose_TargetSpace2D() - { - return toPose2D(robotPose_TargetSpace); - } - public Pose2d getTargetPose_CameraSpace2D() - { - return toPose2D(targetPose_CameraSpace); - } - public Pose2d getTargetPose_RobotSpace2D() - { - return toPose2D(targetPose_RobotSpace); - } - - @JsonProperty("ta") - public double ta; + public static class LimelightTarget_Retro { - @JsonProperty("tx") - public double tx; + @JsonProperty("t6c_ts") + private double[] cameraPose_TargetSpace; - @JsonProperty("txp") - public double tx_pixels; + @JsonProperty("t6r_fs") + private double[] robotPose_FieldSpace; - @JsonProperty("ty") - public double ty; + @JsonProperty("t6r_ts") + private double[] robotPose_TargetSpace; - @JsonProperty("typ") - public double ty_pixels; + @JsonProperty("t6t_cs") + private double[] targetPose_CameraSpace; - @JsonProperty("ts") - public double ts; - - public LimelightTarget_Fiducial() { - cameraPose_TargetSpace = new double[6]; - robotPose_FieldSpace = new double[6]; - robotPose_TargetSpace = new double[6]; - targetPose_CameraSpace = new double[6]; - targetPose_RobotSpace = new double[6]; - } - } - - public static class LimelightTarget_Barcode { + @JsonProperty("t6t_rs") + private double[] targetPose_RobotSpace; + public Pose3d getCameraPose_TargetSpace() { + return toPose3D(cameraPose_TargetSpace); } - public static class LimelightTarget_Classifier { - - @JsonProperty("class") - public String className; - - @JsonProperty("classID") - public double classID; - - @JsonProperty("conf") - public double confidence; - - @JsonProperty("zone") - public double zone; - - @JsonProperty("tx") - public double tx; - - @JsonProperty("txp") - public double tx_pixels; - - @JsonProperty("ty") - public double ty; - - @JsonProperty("typ") - public double ty_pixels; - - public LimelightTarget_Classifier() { - } + public Pose3d getRobotPose_FieldSpace() { + return toPose3D(robotPose_FieldSpace); } - public static class LimelightTarget_Detector { - - @JsonProperty("class") - public String className; - - @JsonProperty("classID") - public double classID; - - @JsonProperty("conf") - public double confidence; - - @JsonProperty("ta") - public double ta; - - @JsonProperty("tx") - public double tx; - - @JsonProperty("txp") - public double tx_pixels; - - @JsonProperty("ty") - public double ty; + public Pose3d getRobotPose_TargetSpace() { + return toPose3D(robotPose_TargetSpace); + } - @JsonProperty("typ") - public double ty_pixels; + public Pose3d getTargetPose_CameraSpace() { + return toPose3D(targetPose_CameraSpace); + } - public LimelightTarget_Detector() { - } + public Pose3d getTargetPose_RobotSpace() { + return toPose3D(targetPose_RobotSpace); } - public static class Results { + public Pose2d getCameraPose_TargetSpace2D() { + return toPose2D(cameraPose_TargetSpace); + } - @JsonProperty("pID") - public double pipelineID; + public Pose2d getRobotPose_FieldSpace2D() { + return toPose2D(robotPose_FieldSpace); + } - @JsonProperty("tl") - public double latency_pipeline; + public Pose2d getRobotPose_TargetSpace2D() { + return toPose2D(robotPose_TargetSpace); + } - @JsonProperty("cl") - public double latency_capture; + public Pose2d getTargetPose_CameraSpace2D() { + return toPose2D(targetPose_CameraSpace); + } - public double latency_jsonParse; + public Pose2d getTargetPose_RobotSpace2D() { + return toPose2D(targetPose_RobotSpace); + } - @JsonProperty("ts") - public double timestamp_LIMELIGHT_publish; + @JsonProperty("ta") + public double ta; - @JsonProperty("ts_rio") - public double timestamp_RIOFPGA_capture; + @JsonProperty("tx") + public double tx; - @JsonProperty("v") - @JsonFormat(shape = Shape.NUMBER) - public boolean valid; + @JsonProperty("txp") + public double tx_pixels; - @JsonProperty("botpose") - public double[] botpose; + @JsonProperty("ty") + public double ty; - @JsonProperty("botpose_wpired") - public double[] botpose_wpired; + @JsonProperty("typ") + public double ty_pixels; - @JsonProperty("botpose_wpiblue") - public double[] botpose_wpiblue; + @JsonProperty("ts") + public double ts; - @JsonProperty("t6c_rs") - public double[] camerapose_robotspace; + public LimelightTarget_Retro() { + cameraPose_TargetSpace = new double[6]; + robotPose_FieldSpace = new double[6]; + robotPose_TargetSpace = new double[6]; + targetPose_CameraSpace = new double[6]; + targetPose_RobotSpace = new double[6]; + } + } - public Pose3d getBotPose3d() { - return toPose3D(botpose); - } - - public Pose3d getBotPose3d_wpiRed() { - return toPose3D(botpose_wpired); - } - - public Pose3d getBotPose3d_wpiBlue() { - return toPose3D(botpose_wpiblue); - } + public static class LimelightTarget_Fiducial { - public Pose2d getBotPose2d() { - return toPose2D(botpose); - } - - public Pose2d getBotPose2d_wpiRed() { - return toPose2D(botpose_wpired); - } - - public Pose2d getBotPose2d_wpiBlue() { - return toPose2D(botpose_wpiblue); - } + @JsonProperty("fID") + public double fiducialID; - @JsonProperty("Retro") - public LimelightTarget_Retro[] targets_Retro; + @JsonProperty("fam") + public String fiducialFamily; - @JsonProperty("Fiducial") - public LimelightTarget_Fiducial[] targets_Fiducials; + @JsonProperty("t6c_ts") + private double[] cameraPose_TargetSpace; - @JsonProperty("Classifier") - public LimelightTarget_Classifier[] targets_Classifier; + @JsonProperty("t6r_fs") + private double[] robotPose_FieldSpace; - @JsonProperty("Detector") - public LimelightTarget_Detector[] targets_Detector; + @JsonProperty("t6r_ts") + private double[] robotPose_TargetSpace; - @JsonProperty("Barcode") - public LimelightTarget_Barcode[] targets_Barcode; + @JsonProperty("t6t_cs") + private double[] targetPose_CameraSpace; - public Results() { - botpose = new double[6]; - botpose_wpired = new double[6]; - botpose_wpiblue = new double[6]; - camerapose_robotspace = new double[6]; - targets_Retro = new LimelightTarget_Retro[0]; - targets_Fiducials = new LimelightTarget_Fiducial[0]; - targets_Classifier = new LimelightTarget_Classifier[0]; - targets_Detector = new LimelightTarget_Detector[0]; - targets_Barcode = new LimelightTarget_Barcode[0]; + @JsonProperty("t6t_rs") + private double[] targetPose_RobotSpace; - } + public Pose3d getCameraPose_TargetSpace() { + return toPose3D(cameraPose_TargetSpace); } - public static class LimelightResults { - @JsonProperty("Results") - public Results targetingResults; - - public LimelightResults() { - targetingResults = new Results(); - } + public Pose3d getRobotPose_FieldSpace() { + return toPose3D(robotPose_FieldSpace); } - private static ObjectMapper mapper; - - /** - * Print JSON Parse time to the console in milliseconds - */ - static boolean profileJSON = false; - - static final String sanitizeName(String name) { - if (name == "" || name == null) { - return "limelight"; - } - return name; + public Pose3d getRobotPose_TargetSpace() { + return toPose3D(robotPose_TargetSpace); } - private static Pose3d toPose3D(double[] inData){ - if(inData.length < 6) - { - System.err.println("Bad LL 3D Pose Data!"); - return new Pose3d(); - } - return new Pose3d( - new Translation3d(inData[0], inData[1], inData[2]), - new Rotation3d(Units.degreesToRadians(inData[3]), Units.degreesToRadians(inData[4]), - Units.degreesToRadians(inData[5]))); + public Pose3d getTargetPose_CameraSpace() { + return toPose3D(targetPose_CameraSpace); } - private static Pose2d toPose2D(double[] inData){ - if(inData.length < 6) - { - System.err.println("Bad LL 2D Pose Data!"); - return new Pose2d(); - } - Translation2d tran2d = new Translation2d(inData[0], inData[1]); - Rotation2d r2d = new Rotation2d(Units.degreesToRadians(inData[5])); - return new Pose2d(tran2d, r2d); + public Pose3d getTargetPose_RobotSpace() { + return toPose3D(targetPose_RobotSpace); } - public static NetworkTable getLimelightNTTable(String tableName) { - return NetworkTableInstance.getDefault().getTable(sanitizeName(tableName)); + public Pose2d getCameraPose_TargetSpace2D() { + return toPose2D(cameraPose_TargetSpace); } - public static NetworkTableEntry getLimelightNTTableEntry(String tableName, String entryName) { - return getLimelightNTTable(tableName).getEntry(entryName); + public Pose2d getRobotPose_FieldSpace2D() { + return toPose2D(robotPose_FieldSpace); } - public static double getLimelightNTDouble(String tableName, String entryName) { - return getLimelightNTTableEntry(tableName, entryName).getDouble(0.0); + public Pose2d getRobotPose_TargetSpace2D() { + return toPose2D(robotPose_TargetSpace); } - public static void setLimelightNTDouble(String tableName, String entryName, double val) { - getLimelightNTTableEntry(tableName, entryName).setDouble(val); + public Pose2d getTargetPose_CameraSpace2D() { + return toPose2D(targetPose_CameraSpace); } - public static void setLimelightNTDoubleArray(String tableName, String entryName, double[] val) { - getLimelightNTTableEntry(tableName, entryName).setDoubleArray(val); + public Pose2d getTargetPose_RobotSpace2D() { + return toPose2D(targetPose_RobotSpace); } - public static double[] getLimelightNTDoubleArray(String tableName, String entryName) { - return getLimelightNTTableEntry(tableName, entryName).getDoubleArray(new double[0]); - } + @JsonProperty("ta") + public double ta; - public static String getLimelightNTString(String tableName, String entryName) { - return getLimelightNTTableEntry(tableName, entryName).getString(""); - } + @JsonProperty("tx") + public double tx; - public static URL getLimelightURLString(String tableName, String request) { - String urlString = "http://" + sanitizeName(tableName) + ".local:5807/" + request; - URL url; - try { - url = new URL(urlString); - return url; - } catch (MalformedURLException e) { - System.err.println("bad LL URL"); - } - return null; - } - ///// - ///// + @JsonProperty("txp") + public double tx_pixels; - public static double getTX(String limelightName) { - return getLimelightNTDouble(limelightName, "tx"); - } + @JsonProperty("ty") + public double ty; - public static double getTY(String limelightName) { - return getLimelightNTDouble(limelightName, "ty"); - } + @JsonProperty("typ") + public double ty_pixels; - public static double getTA(String limelightName) { - return getLimelightNTDouble(limelightName, "ta"); - } + @JsonProperty("ts") + public double ts; - public static double getLatency_Pipeline(String limelightName) { - return getLimelightNTDouble(limelightName, "tl"); + public LimelightTarget_Fiducial() { + cameraPose_TargetSpace = new double[6]; + robotPose_FieldSpace = new double[6]; + robotPose_TargetSpace = new double[6]; + targetPose_CameraSpace = new double[6]; + targetPose_RobotSpace = new double[6]; } + } - public static double getLatency_Capture(String limelightName) { - return getLimelightNTDouble(limelightName, "cl"); - } + public static class LimelightTarget_Barcode {} - public static double getCurrentPipelineIndex(String limelightName) { - return getLimelightNTDouble(limelightName, "getpipe"); - } + public static class LimelightTarget_Classifier { - public static String getJSONDump(String limelightName) { - return getLimelightNTString(limelightName, "json"); - } + @JsonProperty("class") + public String className; - /** - * Switch to getBotPose - * - * @param limelightName - * @return - */ - @Deprecated - public static double[] getBotpose(String limelightName) { - return getLimelightNTDoubleArray(limelightName, "botpose"); - } + @JsonProperty("classID") + public double classID; - /** - * Switch to getBotPose_wpiRed - * - * @param limelightName - * @return - */ - @Deprecated - public static double[] getBotpose_wpiRed(String limelightName) { - return getLimelightNTDoubleArray(limelightName, "botpose_wpired"); - } + @JsonProperty("conf") + public double confidence; - /** - * Switch to getBotPose_wpiBlue - * - * @param limelightName - * @return - */ - @Deprecated - public static double[] getBotpose_wpiBlue(String limelightName) { - return getLimelightNTDoubleArray(limelightName, "botpose_wpiblue"); - } + @JsonProperty("zone") + public double zone; - public static double[] getBotPose(String limelightName) { - return getLimelightNTDoubleArray(limelightName, "botpose"); - } + @JsonProperty("tx") + public double tx; - public static double[] getBotPose_wpiRed(String limelightName) { - return getLimelightNTDoubleArray(limelightName, "botpose_wpired"); - } + @JsonProperty("txp") + public double tx_pixels; - public static double[] getBotPose_wpiBlue(String limelightName) { - return getLimelightNTDoubleArray(limelightName, "botpose_wpiblue"); - } + @JsonProperty("ty") + public double ty; - public static double[] getBotPose_TargetSpace(String limelightName) { - return getLimelightNTDoubleArray(limelightName, "botpose_targetspace"); - } + @JsonProperty("typ") + public double ty_pixels; - public static double[] getCameraPose_TargetSpace(String limelightName) { - return getLimelightNTDoubleArray(limelightName, "camerapose_targetspace"); - } + public LimelightTarget_Classifier() {} + } - public static double[] getTargetPose_CameraSpace(String limelightName) { - return getLimelightNTDoubleArray(limelightName, "targetpose_cameraspace"); - } + public static class LimelightTarget_Detector { - public static double[] getTargetPose_RobotSpace(String limelightName) { - return getLimelightNTDoubleArray(limelightName, "targetpose_robotspace"); - } + @JsonProperty("class") + public String className; - public static double[] getTargetColor(String limelightName) { - return getLimelightNTDoubleArray(limelightName, "tc"); - } + @JsonProperty("classID") + public double classID; - public static double getFiducialID(String limelightName) { - return getLimelightNTDouble(limelightName, "tid"); - } + @JsonProperty("conf") + public double confidence; - public static double getNeuralClassID(String limelightName) { - return getLimelightNTDouble(limelightName, "tclass"); - } + @JsonProperty("ta") + public double ta; - ///// - ///// + @JsonProperty("tx") + public double tx; - public static Pose3d getBotPose3d(String limelightName) { - double[] poseArray = getLimelightNTDoubleArray(limelightName, "botpose"); - return toPose3D(poseArray); - } + @JsonProperty("txp") + public double tx_pixels; - public static Pose3d getBotPose3d_wpiRed(String limelightName) { - double[] poseArray = getLimelightNTDoubleArray(limelightName, "botpose_wpired"); - return toPose3D(poseArray); - } + @JsonProperty("ty") + public double ty; - public static Pose3d getBotPose3d_wpiBlue(String limelightName) { - double[] poseArray = getLimelightNTDoubleArray(limelightName, "botpose_wpiblue"); - return toPose3D(poseArray); - } + @JsonProperty("typ") + public double ty_pixels; - public static Pose3d getBotPose3d_TargetSpace(String limelightName) { - double[] poseArray = getLimelightNTDoubleArray(limelightName, "botpose_targetspace"); - return toPose3D(poseArray); - } + public LimelightTarget_Detector() {} + } - public static Pose3d getCameraPose3d_TargetSpace(String limelightName) { - double[] poseArray = getLimelightNTDoubleArray(limelightName, "camerapose_targetspace"); - return toPose3D(poseArray); - } + public static class Results { - public static Pose3d getTargetPose3d_CameraSpace(String limelightName) { - double[] poseArray = getLimelightNTDoubleArray(limelightName, "targetpose_cameraspace"); - return toPose3D(poseArray); - } + @JsonProperty("pID") + public double pipelineID; - public static Pose3d getTargetPose3d_RobotSpace(String limelightName) { - double[] poseArray = getLimelightNTDoubleArray(limelightName, "targetpose_robotspace"); - return toPose3D(poseArray); - } + @JsonProperty("tl") + public double latency_pipeline; - public static Pose3d getCameraPose3d_RobotSpace(String limelightName) { - double[] poseArray = getLimelightNTDoubleArray(limelightName, "camerapose_robotspace"); - return toPose3D(poseArray); - } + @JsonProperty("cl") + public double latency_capture; - /** - * Gets the Pose2d for easy use with Odometry vision pose estimator - * (addVisionMeasurement) - * - * @param limelightName - * @return - */ - public static Pose2d getBotPose2d_wpiBlue(String limelightName) { + public double latency_jsonParse; - double[] result = getBotPose_wpiBlue(limelightName); - return toPose2D(result); - } + @JsonProperty("ts") + public double timestamp_LIMELIGHT_publish; - /** - * Gets the Pose2d for easy use with Odometry vision pose estimator - * (addVisionMeasurement) - * - * @param limelightName - * @return - */ - public static Pose2d getBotPose2d_wpiRed(String limelightName) { + @JsonProperty("ts_rio") + public double timestamp_RIOFPGA_capture; - double[] result = getBotPose_wpiRed(limelightName); - return toPose2D(result); + @JsonProperty("v") + @JsonFormat(shape = Shape.NUMBER) + public boolean valid; - } + @JsonProperty("botpose") + public double[] botpose; - /** - * Gets the Pose2d for easy use with Odometry vision pose estimator - * (addVisionMeasurement) - * - * @param limelightName - * @return - */ - public static Pose2d getBotPose2d(String limelightName) { + @JsonProperty("botpose_wpired") + public double[] botpose_wpired; - double[] result = getBotPose(limelightName); - return toPose2D(result); + @JsonProperty("botpose_wpiblue") + public double[] botpose_wpiblue; - } + @JsonProperty("t6c_rs") + public double[] camerapose_robotspace; - public static boolean getTV(String limelightName) { - return 1.0 == getLimelightNTDouble(limelightName, "tv"); + public Pose3d getBotPose3d() { + return toPose3D(botpose); } - ///// - ///// - - public static void setPipelineIndex(String limelightName, int pipelineIndex) { - setLimelightNTDouble(limelightName, "pipeline", pipelineIndex); + public Pose3d getBotPose3d_wpiRed() { + return toPose3D(botpose_wpired); } - /** - * The LEDs will be controlled by Limelight pipeline settings, and not by robot - * code. - */ - public static void setLEDMode_PipelineControl(String limelightName) { - setLimelightNTDouble(limelightName, "ledMode", 0); + public Pose3d getBotPose3d_wpiBlue() { + return toPose3D(botpose_wpiblue); } - public static void setLEDMode_ForceOff(String limelightName) { - setLimelightNTDouble(limelightName, "ledMode", 1); + public Pose2d getBotPose2d() { + return toPose2D(botpose); } - public static void setLEDMode_ForceBlink(String limelightName) { - setLimelightNTDouble(limelightName, "ledMode", 2); + public Pose2d getBotPose2d_wpiRed() { + return toPose2D(botpose_wpired); } - public static void setLEDMode_ForceOn(String limelightName) { - setLimelightNTDouble(limelightName, "ledMode", 3); + public Pose2d getBotPose2d_wpiBlue() { + return toPose2D(botpose_wpiblue); } - public static void setStreamMode_Standard(String limelightName) { - setLimelightNTDouble(limelightName, "stream", 0); - } + @JsonProperty("Retro") + public LimelightTarget_Retro[] targets_Retro; - public static void setStreamMode_PiPMain(String limelightName) { - setLimelightNTDouble(limelightName, "stream", 1); - } + @JsonProperty("Fiducial") + public LimelightTarget_Fiducial[] targets_Fiducials; - public static void setStreamMode_PiPSecondary(String limelightName) { - setLimelightNTDouble(limelightName, "stream", 2); - } + @JsonProperty("Classifier") + public LimelightTarget_Classifier[] targets_Classifier; - public static void setCameraMode_Processor(String limelightName) { - setLimelightNTDouble(limelightName, "camMode", 0); - } - public static void setCameraMode_Driver(String limelightName) { - setLimelightNTDouble(limelightName, "camMode", 1); - } + @JsonProperty("Detector") + public LimelightTarget_Detector[] targets_Detector; + @JsonProperty("Barcode") + public LimelightTarget_Barcode[] targets_Barcode; - /** - * Sets the crop window. The crop window in the UI must be completely open for - * dynamic cropping to work. - */ - public static void setCropWindow(String limelightName, double cropXMin, double cropXMax, double cropYMin, double cropYMax) { - double[] entries = new double[4]; - entries[0] = cropXMin; - entries[1] = cropXMax; - entries[2] = cropYMin; - entries[3] = cropYMax; - setLimelightNTDoubleArray(limelightName, "crop", entries); + public Results() { + botpose = new double[6]; + botpose_wpired = new double[6]; + botpose_wpiblue = new double[6]; + camerapose_robotspace = new double[6]; + targets_Retro = new LimelightTarget_Retro[0]; + targets_Fiducials = new LimelightTarget_Fiducial[0]; + targets_Classifier = new LimelightTarget_Classifier[0]; + targets_Detector = new LimelightTarget_Detector[0]; + targets_Barcode = new LimelightTarget_Barcode[0]; } + } + + public static class LimelightResults { + @JsonProperty("Results") + public Results targetingResults; - public static void setCameraPose_RobotSpace(String limelightName, double forward, double side, double up, double roll, double pitch, double yaw) { - double[] entries = new double[6]; - entries[0] = forward; - entries[1] = side; - entries[2] = up; - entries[3] = roll; - entries[4] = pitch; - entries[5] = yaw; - setLimelightNTDoubleArray(limelightName, "camerapose_robotspace_set", entries); + public LimelightResults() { + targetingResults = new Results(); } + } - ///// - ///// + private static ObjectMapper mapper; - public static void setPythonScriptData(String limelightName, double[] outgoingPythonData) { - setLimelightNTDoubleArray(limelightName, "llrobot", outgoingPythonData); - } + /** Print JSON Parse time to the console in milliseconds */ + static boolean profileJSON = false; - public static double[] getPythonScriptData(String limelightName) { - return getLimelightNTDoubleArray(limelightName, "llpython"); + static final String sanitizeName(String name) { + if (name == "" || name == null) { + return "limelight"; } + return name; + } - ///// - ///// - - /** - * Asynchronously take snapshot. - */ - public static CompletableFuture takeSnapshot(String tableName, String snapshotName) { - return CompletableFuture.supplyAsync(() -> { - return SYNCH_TAKESNAPSHOT(tableName, snapshotName); - }); + private static Pose3d toPose3D(double[] inData) { + if (inData.length < 6) { + System.err.println("Bad LL 3D Pose Data!"); + return new Pose3d(); } + return new Pose3d( + new Translation3d(inData[0], inData[1], inData[2]), + new Rotation3d( + Units.degreesToRadians(inData[3]), + Units.degreesToRadians(inData[4]), + Units.degreesToRadians(inData[5]))); + } - private static boolean SYNCH_TAKESNAPSHOT(String tableName, String snapshotName) { - URL url = getLimelightURLString(tableName, "capturesnapshot"); - try { - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("GET"); - if (snapshotName != null && snapshotName != "") { - connection.setRequestProperty("snapname", snapshotName); - } - - int responseCode = connection.getResponseCode(); - if (responseCode == 200) { - return true; - } else { - System.err.println("Bad LL Request"); - } - } catch (IOException e) { - System.err.println(e.getMessage()); - } - return false; - } - - /** - * Parses Limelight's JSON results dump into a LimelightResults Object - */ - public static LimelightResults getLatestResults(String limelightName) { - - long start = System.nanoTime(); - LimelightHelpers.LimelightResults results = new LimelightHelpers.LimelightResults(); - if (mapper == null) { - mapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - } - - try { - results = mapper.readValue(getJSONDump(limelightName), LimelightResults.class); - } catch (JsonProcessingException e) { - System.err.println("lljson error: " + e.getMessage()); - } - - long end = System.nanoTime(); - double millis = (end - start) * .000001; - results.targetingResults.latency_jsonParse = millis; - if (profileJSON) { - System.out.printf("lljson: %.2f\r\n", millis); - } - - return results; + private static Pose2d toPose2D(double[] inData) { + if (inData.length < 6) { + System.err.println("Bad LL 2D Pose Data!"); + return new Pose2d(); } + Translation2d tran2d = new Translation2d(inData[0], inData[1]); + Rotation2d r2d = new Rotation2d(Units.degreesToRadians(inData[5])); + return new Pose2d(tran2d, r2d); + } + + public static NetworkTable getLimelightNTTable(String tableName) { + return NetworkTableInstance.getDefault().getTable(sanitizeName(tableName)); + } + + public static NetworkTableEntry getLimelightNTTableEntry(String tableName, String entryName) { + return getLimelightNTTable(tableName).getEntry(entryName); + } + + public static double getLimelightNTDouble(String tableName, String entryName) { + return getLimelightNTTableEntry(tableName, entryName).getDouble(0.0); + } + + public static void setLimelightNTDouble(String tableName, String entryName, double val) { + getLimelightNTTableEntry(tableName, entryName).setDouble(val); + } + + public static void setLimelightNTDoubleArray(String tableName, String entryName, double[] val) { + getLimelightNTTableEntry(tableName, entryName).setDoubleArray(val); + } + + public static double[] getLimelightNTDoubleArray(String tableName, String entryName) { + return getLimelightNTTableEntry(tableName, entryName).getDoubleArray(new double[0]); + } + + public static String getLimelightNTString(String tableName, String entryName) { + return getLimelightNTTableEntry(tableName, entryName).getString(""); + } + + public static URL getLimelightURLString(String tableName, String request) { + String urlString = "http://" + sanitizeName(tableName) + ".local:5807/" + request; + URL url; + try { + url = new URL(urlString); + return url; + } catch (MalformedURLException e) { + System.err.println("bad LL URL"); + } + return null; + } + + ///// + ///// + + public static double getTX(String limelightName) { + return getLimelightNTDouble(limelightName, "tx"); + } + + public static double getTY(String limelightName) { + return getLimelightNTDouble(limelightName, "ty"); + } + + public static double getTA(String limelightName) { + return getLimelightNTDouble(limelightName, "ta"); + } + + public static double getLatency_Pipeline(String limelightName) { + return getLimelightNTDouble(limelightName, "tl"); + } + + public static double getLatency_Capture(String limelightName) { + return getLimelightNTDouble(limelightName, "cl"); + } + + public static double getCurrentPipelineIndex(String limelightName) { + return getLimelightNTDouble(limelightName, "getpipe"); + } + + public static String getJSONDump(String limelightName) { + return getLimelightNTString(limelightName, "json"); + } + + /** + * Switch to getBotPose + * + * @param limelightName + * @return + */ + @Deprecated + public static double[] getBotpose(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "botpose"); + } + + /** + * Switch to getBotPose_wpiRed + * + * @param limelightName + * @return + */ + @Deprecated + public static double[] getBotpose_wpiRed(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "botpose_wpired"); + } + + /** + * Switch to getBotPose_wpiBlue + * + * @param limelightName + * @return + */ + @Deprecated + public static double[] getBotpose_wpiBlue(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "botpose_wpiblue"); + } + + public static double[] getBotPose(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "botpose"); + } + + public static double[] getBotPose_wpiRed(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "botpose_wpired"); + } + + public static double[] getBotPose_wpiBlue(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "botpose_wpiblue"); + } + + public static double[] getBotPose_TargetSpace(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "botpose_targetspace"); + } + + public static double[] getCameraPose_TargetSpace(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "camerapose_targetspace"); + } + + public static double[] getTargetPose_CameraSpace(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "targetpose_cameraspace"); + } + + public static double[] getTargetPose_RobotSpace(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "targetpose_robotspace"); + } + + public static double[] getTargetColor(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "tc"); + } + + public static double getFiducialID(String limelightName) { + return getLimelightNTDouble(limelightName, "tid"); + } + + public static double getNeuralClassID(String limelightName) { + return getLimelightNTDouble(limelightName, "tclass"); + } + + ///// + ///// + + public static Pose3d getBotPose3d(String limelightName) { + double[] poseArray = getLimelightNTDoubleArray(limelightName, "botpose"); + return toPose3D(poseArray); + } + + public static Pose3d getBotPose3d_wpiRed(String limelightName) { + double[] poseArray = getLimelightNTDoubleArray(limelightName, "botpose_wpired"); + return toPose3D(poseArray); + } + + public static Pose3d getBotPose3d_wpiBlue(String limelightName) { + double[] poseArray = getLimelightNTDoubleArray(limelightName, "botpose_wpiblue"); + return toPose3D(poseArray); + } + + public static Pose3d getBotPose3d_TargetSpace(String limelightName) { + double[] poseArray = getLimelightNTDoubleArray(limelightName, "botpose_targetspace"); + return toPose3D(poseArray); + } + + public static Pose3d getCameraPose3d_TargetSpace(String limelightName) { + double[] poseArray = getLimelightNTDoubleArray(limelightName, "camerapose_targetspace"); + return toPose3D(poseArray); + } + + public static Pose3d getTargetPose3d_CameraSpace(String limelightName) { + double[] poseArray = getLimelightNTDoubleArray(limelightName, "targetpose_cameraspace"); + return toPose3D(poseArray); + } + + public static Pose3d getTargetPose3d_RobotSpace(String limelightName) { + double[] poseArray = getLimelightNTDoubleArray(limelightName, "targetpose_robotspace"); + return toPose3D(poseArray); + } + + public static Pose3d getCameraPose3d_RobotSpace(String limelightName) { + double[] poseArray = getLimelightNTDoubleArray(limelightName, "camerapose_robotspace"); + return toPose3D(poseArray); + } + + /** + * Gets the Pose2d for easy use with Odometry vision pose estimator (addVisionMeasurement) + * + * @param limelightName + * @return + */ + public static Pose2d getBotPose2d_wpiBlue(String limelightName) { + + double[] result = getBotPose_wpiBlue(limelightName); + return toPose2D(result); + } + + /** + * Gets the Pose2d for easy use with Odometry vision pose estimator (addVisionMeasurement) + * + * @param limelightName + * @return + */ + public static Pose2d getBotPose2d_wpiRed(String limelightName) { + + double[] result = getBotPose_wpiRed(limelightName); + return toPose2D(result); + } + + /** + * Gets the Pose2d for easy use with Odometry vision pose estimator (addVisionMeasurement) + * + * @param limelightName + * @return + */ + public static Pose2d getBotPose2d(String limelightName) { + + double[] result = getBotPose(limelightName); + return toPose2D(result); + } + + public static boolean getTV(String limelightName) { + return 1.0 == getLimelightNTDouble(limelightName, "tv"); + } + + ///// + ///// + + public static void setPipelineIndex(String limelightName, int pipelineIndex) { + setLimelightNTDouble(limelightName, "pipeline", pipelineIndex); + } + + /** The LEDs will be controlled by Limelight pipeline settings, and not by robot code. */ + public static void setLEDMode_PipelineControl(String limelightName) { + setLimelightNTDouble(limelightName, "ledMode", 0); + } + + public static void setLEDMode_ForceOff(String limelightName) { + setLimelightNTDouble(limelightName, "ledMode", 1); + } + + public static void setLEDMode_ForceBlink(String limelightName) { + setLimelightNTDouble(limelightName, "ledMode", 2); + } + + public static void setLEDMode_ForceOn(String limelightName) { + setLimelightNTDouble(limelightName, "ledMode", 3); + } + + public static void setStreamMode_Standard(String limelightName) { + setLimelightNTDouble(limelightName, "stream", 0); + } + + public static void setStreamMode_PiPMain(String limelightName) { + setLimelightNTDouble(limelightName, "stream", 1); + } + + public static void setStreamMode_PiPSecondary(String limelightName) { + setLimelightNTDouble(limelightName, "stream", 2); + } + + public static void setCameraMode_Processor(String limelightName) { + setLimelightNTDouble(limelightName, "camMode", 0); + } + + public static void setCameraMode_Driver(String limelightName) { + setLimelightNTDouble(limelightName, "camMode", 1); + } + + /** + * Sets the crop window. The crop window in the UI must be completely open for dynamic cropping to + * work. + */ + public static void setCropWindow( + String limelightName, double cropXMin, double cropXMax, double cropYMin, double cropYMax) { + double[] entries = new double[4]; + entries[0] = cropXMin; + entries[1] = cropXMax; + entries[2] = cropYMin; + entries[3] = cropYMax; + setLimelightNTDoubleArray(limelightName, "crop", entries); + } + + public static void setCameraPose_RobotSpace( + String limelightName, + double forward, + double side, + double up, + double roll, + double pitch, + double yaw) { + double[] entries = new double[6]; + entries[0] = forward; + entries[1] = side; + entries[2] = up; + entries[3] = roll; + entries[4] = pitch; + entries[5] = yaw; + setLimelightNTDoubleArray(limelightName, "camerapose_robotspace_set", entries); + } + + ///// + ///// + + public static void setPythonScriptData(String limelightName, double[] outgoingPythonData) { + setLimelightNTDoubleArray(limelightName, "llrobot", outgoingPythonData); + } + + public static double[] getPythonScriptData(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "llpython"); + } + + ///// + ///// + + /** Asynchronously take snapshot. */ + public static CompletableFuture takeSnapshot(String tableName, String snapshotName) { + return CompletableFuture.supplyAsync( + () -> { + return SYNCH_TAKESNAPSHOT(tableName, snapshotName); + }); + } + + private static boolean SYNCH_TAKESNAPSHOT(String tableName, String snapshotName) { + URL url = getLimelightURLString(tableName, "capturesnapshot"); + try { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + if (snapshotName != null && snapshotName != "") { + connection.setRequestProperty("snapname", snapshotName); + } + + int responseCode = connection.getResponseCode(); + if (responseCode == 200) { + return true; + } else { + System.err.println("Bad LL Request"); + } + } catch (IOException e) { + System.err.println(e.getMessage()); + } + return false; + } + + /** Parses Limelight's JSON results dump into a LimelightResults Object */ + public static LimelightResults getLatestResults(String limelightName) { + + long start = System.nanoTime(); + LimelightHelpers.LimelightResults results = new LimelightHelpers.LimelightResults(); + if (mapper == null) { + mapper = + new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + try { + results = mapper.readValue(getJSONDump(limelightName), LimelightResults.class); + } catch (JsonProcessingException e) { + System.err.println("lljson error: " + e.getMessage()); + } + + long end = System.nanoTime(); + double millis = (end - start) * .000001; + results.targetingResults.latency_jsonParse = millis; + if (profileJSON) { + System.out.printf("lljson: %.2f\r\n", millis); + } + + return results; + } } diff --git a/src/main/java/frc/robot/Robot.java b/src/main/java/frc/robot/Robot.java index 1d1e383..8bafc7b 100644 --- a/src/main/java/frc/robot/Robot.java +++ b/src/main/java/frc/robot/Robot.java @@ -4,7 +4,6 @@ package frc.robot; - import com.ctre.phoenix6.SignalLogger; import edu.wpi.first.math.geometry.Pose2d; @@ -28,10 +27,11 @@ public void robotInit() { SignalLogger.start(); } + @Override public void robotPeriodic() { - CommandScheduler.getInstance().run(); - if(UseLimelight) { + CommandScheduler.getInstance().run(); + if (UseLimelight) { var lastResult = LimelightHelpers.getLatestResults("limelight").targetingResults; Pose2d llPose = lastResult.getBotPose2d_wpiBlue(); @@ -77,8 +77,7 @@ public void teleopInit() { public void teleopPeriodic() {} @Override - public void teleopExit() { - } + public void teleopExit() {} @Override public void testInit() { diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java index 02c61ff..556d128 100644 --- a/src/main/java/frc/robot/RobotContainer.java +++ b/src/main/java/frc/robot/RobotContainer.java @@ -17,13 +17,18 @@ public class RobotContainer { final double MaxAngularRate = Math.PI; // Half a rotation per second max angular velocity /* Setting up bindings for necessary control of the swerve drive platform */ - CommandController driver = new CommandController(0); // Driver Controller - CommandController operator = new CommandController(1); // Operator Controller + CommandController driver = new CommandController(0); // Driver Controller + CommandController operator = new CommandController(1); // Operator Controller CommandSwerveDrivetrain drivetrain = TunerConstants.DriveTrain; // My drivetrain - SwerveRequest.FieldCentric drive = new SwerveRequest.FieldCentric().withIsOpenLoop(true).withDeadband(MaxSpeed * 0.1).withRotationalDeadband(MaxAngularRate * 0.1); // I want field-centric - // driving in open loop + SwerveRequest.FieldCentric drive = + new SwerveRequest.FieldCentric() + .withIsOpenLoop(true) + .withDeadband(MaxSpeed * 0.1) + .withRotationalDeadband(MaxAngularRate * 0.1); // I want field-centric + // driving in open loop SwerveRequest.SwerveDriveBrake brake = new SwerveRequest.SwerveDriveBrake(); - SwerveRequest.RobotCentric forwardStraight = new SwerveRequest.RobotCentric().withIsOpenLoop(true); + SwerveRequest.RobotCentric forwardStraight = + new SwerveRequest.RobotCentric().withIsOpenLoop(true); SwerveRequest.PointWheelsAt point = new SwerveRequest.PointWheelsAt(); /* Path follower */ @@ -35,23 +40,40 @@ public class RobotContainer { private void configureBindings() { drivetrain.setDefaultCommand( // Drivetrain will execute this command periodically - drivetrain.applyRequest(() -> drive.withVelocityX(-driver.getLeftY() * MaxSpeed) // Drive forward with - // negative Y (forward) - .withVelocityY(-driver.getLeftX() * MaxSpeed) // Drive left with negative X (left) - .withRotationalRate(-driver.getRightX() * MaxAngularRate) // Drive counterclockwise with negative X (left) - ).ignoringDisable(true)); + drivetrain + .applyRequest( + () -> + drive + .withVelocityX(-driver.getLeftY() * MaxSpeed) // Drive forward with + // negative Y (forward) + .withVelocityY( + -driver.getLeftX() * MaxSpeed) // Drive left with negative X (left) + .withRotationalRate( + -driver.getRightX() + * MaxAngularRate) // Drive counterclockwise with negative X (left) + ) + .ignoringDisable(true)); driver.greenButton().whileTrue(drivetrain.applyRequest(() -> brake)); - driver.yellowButton().whileTrue(drivetrain - .applyRequest(() -> point.withModuleDirection(new Rotation2d(-driver.getLeftY(), -driver.getLeftX())))); + driver + .yellowButton() + .whileTrue( + drivetrain.applyRequest( + () -> + point.withModuleDirection( + new Rotation2d(-driver.getLeftY(), -driver.getLeftX())))); // if (Utils.isSimulation()) { // drivetrain.seedFieldRelative(new Pose2d(new Translation2d(), Rotation2d.fromDegrees(90))); // } drivetrain.registerTelemetry(logger::telemeterize); - driver.POVUp().whileTrue(drivetrain.applyRequest(()->forwardStraight.withVelocityX(0.5).withVelocityY(0))); - driver.POVDown().whileTrue(drivetrain.applyRequest(()->forwardStraight.withVelocityX(-0.5).withVelocityY(0))); + driver.POVUp() + .whileTrue( + drivetrain.applyRequest(() -> forwardStraight.withVelocityX(0.5).withVelocityY(0))); + driver.POVDown() + .whileTrue( + drivetrain.applyRequest(() -> forwardStraight.withVelocityX(-0.5).withVelocityY(0))); } public RobotContainer() { @@ -60,7 +82,7 @@ public RobotContainer() { public Command getAutonomousCommand() { /* First put the drivetrain into auto run mode, then run the auto */ - return new InstantCommand(()->{}); + return new InstantCommand(() -> {}); } public boolean seedPoseButtonDown() { diff --git a/src/main/java/frc/robot/Telemetry.java b/src/main/java/frc/robot/Telemetry.java index 3fb3ded..16e9089 100644 --- a/src/main/java/frc/robot/Telemetry.java +++ b/src/main/java/frc/robot/Telemetry.java @@ -19,101 +19,114 @@ import edu.wpi.first.wpilibj.util.Color8Bit; public class Telemetry { - private final double MaxSpeed; - private int logEntry; - private int odomEntry; - - /** - * Construct a telemetry object, with the specified max speed of the robot - * - * @param maxSpeed Maximum speed in meters per second - */ - public Telemetry(double maxSpeed) { - MaxSpeed = maxSpeed; - logEntry = DataLogManager.getLog().start("odometry", "double[]"); - odomEntry = DataLogManager.getLog().start("odom period", "double"); + private final double MaxSpeed; + private int logEntry; + private int odomEntry; + + /** + * Construct a telemetry object, with the specified max speed of the robot + * + * @param maxSpeed Maximum speed in meters per second + */ + public Telemetry(double maxSpeed) { + MaxSpeed = maxSpeed; + logEntry = DataLogManager.getLog().start("odometry", "double[]"); + odomEntry = DataLogManager.getLog().start("odom period", "double"); + } + + /* What to publish over networktables for telemetry */ + NetworkTableInstance inst = NetworkTableInstance.getDefault(); + + /* Robot pose for field positioning */ + NetworkTable table = inst.getTable("Pose"); + DoubleArrayPublisher fieldPub = table.getDoubleArrayTopic("robotPose").publish(); + StringPublisher fieldTypePub = table.getStringTopic(".type").publish(); + + /* Robot speeds for general checking */ + NetworkTable driveStats = inst.getTable("Drive"); + DoublePublisher velocityX = driveStats.getDoubleTopic("Velocity X").publish(); + DoublePublisher velocityY = driveStats.getDoubleTopic("Velocity Y").publish(); + DoublePublisher speed = driveStats.getDoubleTopic("Speed").publish(); + DoublePublisher odomPeriod = driveStats.getDoubleTopic("Odometry Period").publish(); + + /* Keep a reference of the last pose to calculate the speeds */ + Pose2d m_lastPose = new Pose2d(); + double lastTime = Utils.getCurrentTimeSeconds(); + + /* Mechanisms to represent the swerve module states */ + Mechanism2d[] m_moduleMechanisms = + new Mechanism2d[] { + new Mechanism2d(1, 1), new Mechanism2d(1, 1), new Mechanism2d(1, 1), new Mechanism2d(1, 1), + }; + /* A direction and length changing ligament for speed representation */ + MechanismLigament2d[] m_moduleSpeeds = + new MechanismLigament2d[] { + m_moduleMechanisms[0] + .getRoot("RootSpeed", 0.5, 0.5) + .append(new MechanismLigament2d("Speed", 0.5, 0)), + m_moduleMechanisms[1] + .getRoot("RootSpeed", 0.5, 0.5) + .append(new MechanismLigament2d("Speed", 0.5, 0)), + m_moduleMechanisms[2] + .getRoot("RootSpeed", 0.5, 0.5) + .append(new MechanismLigament2d("Speed", 0.5, 0)), + m_moduleMechanisms[3] + .getRoot("RootSpeed", 0.5, 0.5) + .append(new MechanismLigament2d("Speed", 0.5, 0)), + }; + /* A direction changing and length constant ligament for module direction */ + MechanismLigament2d[] m_moduleDirections = + new MechanismLigament2d[] { + m_moduleMechanisms[0] + .getRoot("RootDirection", 0.5, 0.5) + .append(new MechanismLigament2d("Direction", 0.1, 0, 0, new Color8Bit(Color.kWhite))), + m_moduleMechanisms[1] + .getRoot("RootDirection", 0.5, 0.5) + .append(new MechanismLigament2d("Direction", 0.1, 0, 0, new Color8Bit(Color.kWhite))), + m_moduleMechanisms[2] + .getRoot("RootDirection", 0.5, 0.5) + .append(new MechanismLigament2d("Direction", 0.1, 0, 0, new Color8Bit(Color.kWhite))), + m_moduleMechanisms[3] + .getRoot("RootDirection", 0.5, 0.5) + .append(new MechanismLigament2d("Direction", 0.1, 0, 0, new Color8Bit(Color.kWhite))), + }; + + /* Accept the swerve drive state and telemeterize it to smartdashboard */ + public void telemeterize(SwerveDriveState state) { + /* Telemeterize the pose */ + Pose2d pose = state.Pose; + fieldTypePub.set("Field2d"); + fieldPub.set(new double[] {pose.getX(), pose.getY(), pose.getRotation().getDegrees()}); + + /* Telemeterize the robot's general speeds */ + double currentTime = Utils.getCurrentTimeSeconds(); + double diffTime = currentTime - lastTime; + lastTime = currentTime; + Translation2d distanceDiff = pose.minus(m_lastPose).getTranslation(); + m_lastPose = pose; + + Translation2d velocities = distanceDiff.div(diffTime); + + speed.set(velocities.getNorm()); + velocityX.set(velocities.getX()); + velocityY.set(velocities.getY()); + odomPeriod.set(1.0 / state.OdometryPeriod); + + /* Telemeterize the module's states */ + for (int i = 0; i < 4; ++i) { + m_moduleSpeeds[i].setAngle(state.ModuleStates[i].angle); + m_moduleDirections[i].setAngle(state.ModuleStates[i].angle); + m_moduleSpeeds[i].setLength(state.ModuleStates[i].speedMetersPerSecond / (2 * MaxSpeed)); + + SmartDashboard.putData("Module " + i, m_moduleMechanisms[i]); } - /* What to publish over networktables for telemetry */ - NetworkTableInstance inst = NetworkTableInstance.getDefault(); - - /* Robot pose for field positioning */ - NetworkTable table = inst.getTable("Pose"); - DoubleArrayPublisher fieldPub = table.getDoubleArrayTopic("robotPose").publish(); - StringPublisher fieldTypePub = table.getStringTopic(".type").publish(); - - /* Robot speeds for general checking */ - NetworkTable driveStats = inst.getTable("Drive"); - DoublePublisher velocityX = driveStats.getDoubleTopic("Velocity X").publish(); - DoublePublisher velocityY = driveStats.getDoubleTopic("Velocity Y").publish(); - DoublePublisher speed = driveStats.getDoubleTopic("Speed").publish(); - DoublePublisher odomPeriod = driveStats.getDoubleTopic("Odometry Period").publish(); - - /* Keep a reference of the last pose to calculate the speeds */ - Pose2d m_lastPose = new Pose2d(); - double lastTime = Utils.getCurrentTimeSeconds(); - - /* Mechanisms to represent the swerve module states */ - Mechanism2d[] m_moduleMechanisms = new Mechanism2d[] { - new Mechanism2d(1, 1), - new Mechanism2d(1, 1), - new Mechanism2d(1, 1), - new Mechanism2d(1, 1), - }; - /* A direction and length changing ligament for speed representation */ - MechanismLigament2d[] m_moduleSpeeds = new MechanismLigament2d[] { - m_moduleMechanisms[0].getRoot("RootSpeed", 0.5, 0.5).append(new MechanismLigament2d("Speed", 0.5, 0)), - m_moduleMechanisms[1].getRoot("RootSpeed", 0.5, 0.5).append(new MechanismLigament2d("Speed", 0.5, 0)), - m_moduleMechanisms[2].getRoot("RootSpeed", 0.5, 0.5).append(new MechanismLigament2d("Speed", 0.5, 0)), - m_moduleMechanisms[3].getRoot("RootSpeed", 0.5, 0.5).append(new MechanismLigament2d("Speed", 0.5, 0)), - }; - /* A direction changing and length constant ligament for module direction */ - MechanismLigament2d[] m_moduleDirections = new MechanismLigament2d[] { - m_moduleMechanisms[0].getRoot("RootDirection", 0.5, 0.5) - .append(new MechanismLigament2d("Direction", 0.1, 0, 0, new Color8Bit(Color.kWhite))), - m_moduleMechanisms[1].getRoot("RootDirection", 0.5, 0.5) - .append(new MechanismLigament2d("Direction", 0.1, 0, 0, new Color8Bit(Color.kWhite))), - m_moduleMechanisms[2].getRoot("RootDirection", 0.5, 0.5) - .append(new MechanismLigament2d("Direction", 0.1, 0, 0, new Color8Bit(Color.kWhite))), - m_moduleMechanisms[3].getRoot("RootDirection", 0.5, 0.5) - .append(new MechanismLigament2d("Direction", 0.1, 0, 0, new Color8Bit(Color.kWhite))), - }; - - /* Accept the swerve drive state and telemeterize it to smartdashboard */ - public void telemeterize(SwerveDriveState state) { - /* Telemeterize the pose */ - Pose2d pose = state.Pose; - fieldTypePub.set("Field2d"); - fieldPub.set(new double[] { - pose.getX(), - pose.getY(), - pose.getRotation().getDegrees() - }); - - /* Telemeterize the robot's general speeds */ - double currentTime = Utils.getCurrentTimeSeconds(); - double diffTime = currentTime - lastTime; - lastTime = currentTime; - Translation2d distanceDiff = pose.minus(m_lastPose).getTranslation(); - m_lastPose = pose; - - Translation2d velocities = distanceDiff.div(diffTime); - - speed.set(velocities.getNorm()); - velocityX.set(velocities.getX()); - velocityY.set(velocities.getY()); - odomPeriod.set(1.0 / state.OdometryPeriod); - - /* Telemeterize the module's states */ - for (int i = 0; i < 4; ++i) { - m_moduleSpeeds[i].setAngle(state.ModuleStates[i].angle); - m_moduleDirections[i].setAngle(state.ModuleStates[i].angle); - m_moduleSpeeds[i].setLength(state.ModuleStates[i].speedMetersPerSecond / (2 * MaxSpeed)); - - SmartDashboard.putData("Module " + i, m_moduleMechanisms[i]); - } - - DataLogManager.getLog().appendDoubleArray(logEntry, new double[] {pose.getX(), pose.getY(), pose.getRotation().getDegrees()}, (long)(Timer.getFPGATimestamp() * 1000000)); - DataLogManager.getLog().appendDouble(odomEntry, state.OdometryPeriod, (long)(Timer.getFPGATimestamp() * 1000000)); - } + DataLogManager.getLog() + .appendDoubleArray( + logEntry, + new double[] {pose.getX(), pose.getY(), pose.getRotation().getDegrees()}, + (long) (Timer.getFPGATimestamp() * 1000000)); + DataLogManager.getLog() + .appendDouble(odomEntry, state.OdometryPeriod, (long) (Timer.getFPGATimestamp() * 1000000)); + } } diff --git a/src/main/java/frc/robot/generated/TunerConstants.java b/src/main/java/frc/robot/generated/TunerConstants.java index 28ce7ee..a66c7d8 100644 --- a/src/main/java/frc/robot/generated/TunerConstants.java +++ b/src/main/java/frc/robot/generated/TunerConstants.java @@ -10,92 +10,124 @@ import frc.robot.CommandSwerveDrivetrain; public class TunerConstants { - static class CustomSlotGains extends Slot0Configs { - public CustomSlotGains(double kP, double kI, double kD, double kV, double kS) { - this.kP = kP; - this.kI = kI; - this.kD = kD; - this.kV = kV; - this.kS = kS; - } + static class CustomSlotGains extends Slot0Configs { + public CustomSlotGains(double kP, double kI, double kD, double kV, double kS) { + this.kP = kP; + this.kI = kI; + this.kD = kD; + this.kV = kV; + this.kS = kS; } - - private static final CustomSlotGains steerGains = new CustomSlotGains(100, 0, 0.05, 0, 0); - private static final CustomSlotGains driveGains = new CustomSlotGains(3, 0, 0, 0, 0); - - private static final double kCoupleRatio = 3.5; - - private static final double kDriveGearRatio = 7.363636364; - private static final double kSteerGearRatio = 15.42857143; - private static final double kWheelRadiusInches = 2.167; // Estimated at first, then fudge-factored to make odom match record - private static final int kPigeonId = 1; - private static final boolean kSteerMotorReversed = true; - private static final String kCANbusName = "rio"; - private static final boolean kInvertLeftSide = false; - private static final boolean kInvertRightSide = true; - - - private static double kSteerInertia = 0.00001; - private static double kDriveInertia = 0.001; - - private static final SwerveDrivetrainConstants DrivetrainConstants = new SwerveDrivetrainConstants() - .withPigeon2Id(kPigeonId) - .withCANbusName(kCANbusName) - .withSupportsPro(true); - - private static final SwerveModuleConstantsFactory ConstantCreator = new SwerveModuleConstantsFactory() - .withDriveMotorGearRatio(kDriveGearRatio) - .withSteerMotorGearRatio(kSteerGearRatio) - .withWheelRadius(kWheelRadiusInches) - .withSlipCurrent(30) - .withSteerMotorGains(steerGains) - .withDriveMotorGains(driveGains) - .withSpeedAt12VoltsMps(6) // Theoretical free speed is 10 meters per second at 12v applied output - .withSteerInertia(kSteerInertia) - .withDriveInertia(kDriveInertia) - .withFeedbackSource(SwerveModuleSteerFeedbackType.FusedCANcoder) - .withCouplingGearRatio(kCoupleRatio) // Every 1 rotation of the azimuth results in couple ratio drive turns - .withSteerMotorInverted(kSteerMotorReversed); - - private static final int kFrontLeftDriveMotorId = 5; - private static final int kFrontLeftSteerMotorId = 4; - private static final int kFrontLeftEncoderId = 2; - private static final double kFrontLeftEncoderOffset = -0.83544921875; - - private static final double kFrontLeftXPosInches = 10.5; - private static final double kFrontLeftYPosInches = 10.5; - private static final int kFrontRightDriveMotorId = 7; - private static final int kFrontRightSteerMotorId = 6; - private static final int kFrontRightEncoderId = 3; - private static final double kFrontRightEncoderOffset = -0.15234375; - - private static final double kFrontRightXPosInches = 10.5; - private static final double kFrontRightYPosInches = -10.5; - private static final int kBackLeftDriveMotorId = 1; - private static final int kBackLeftSteerMotorId = 0; - private static final int kBackLeftEncoderId = 0; - private static final double kBackLeftEncoderOffset = -0.4794921875; - - private static final double kBackLeftXPosInches = -10.5; - private static final double kBackLeftYPosInches = 10.5; - private static final int kBackRightDriveMotorId = 3; - private static final int kBackRightSteerMotorId = 2; - private static final int kBackRightEncoderId = 1; - private static final double kBackRightEncoderOffset = -0.84130859375; - - private static final double kBackRightXPosInches = -10.5; - private static final double kBackRightYPosInches = -10.5; - - - private static final SwerveModuleConstants FrontLeft = ConstantCreator.createModuleConstants( - kFrontLeftSteerMotorId, kFrontLeftDriveMotorId, kFrontLeftEncoderId, kFrontLeftEncoderOffset, Units.inchesToMeters(kFrontLeftXPosInches), Units.inchesToMeters(kFrontLeftYPosInches), kInvertLeftSide); - private static final SwerveModuleConstants FrontRight = ConstantCreator.createModuleConstants( - kFrontRightSteerMotorId, kFrontRightDriveMotorId, kFrontRightEncoderId, kFrontRightEncoderOffset, Units.inchesToMeters(kFrontRightXPosInches), Units.inchesToMeters(kFrontRightYPosInches), kInvertRightSide); - private static final SwerveModuleConstants BackLeft = ConstantCreator.createModuleConstants( - kBackLeftSteerMotorId, kBackLeftDriveMotorId, kBackLeftEncoderId, kBackLeftEncoderOffset, Units.inchesToMeters(kBackLeftXPosInches), Units.inchesToMeters(kBackLeftYPosInches), kInvertLeftSide); - private static final SwerveModuleConstants BackRight = ConstantCreator.createModuleConstants( - kBackRightSteerMotorId, kBackRightDriveMotorId, kBackRightEncoderId, kBackRightEncoderOffset, Units.inchesToMeters(kBackRightXPosInches), Units.inchesToMeters(kBackRightYPosInches), kInvertRightSide); - - public static final CommandSwerveDrivetrain DriveTrain = new CommandSwerveDrivetrain(DrivetrainConstants, 250, FrontLeft, - FrontRight, BackLeft, BackRight); + } + + private static final CustomSlotGains steerGains = new CustomSlotGains(100, 0, 0.05, 0, 0); + private static final CustomSlotGains driveGains = new CustomSlotGains(3, 0, 0, 0, 0); + + private static final double kCoupleRatio = 3.5; + + private static final double kDriveGearRatio = 7.363636364; + private static final double kSteerGearRatio = 15.42857143; + private static final double kWheelRadiusInches = + 2.167; // Estimated at first, then fudge-factored to make odom match record + private static final int kPigeonId = 1; + private static final boolean kSteerMotorReversed = true; + private static final String kCANbusName = "rio"; + private static final boolean kInvertLeftSide = false; + private static final boolean kInvertRightSide = true; + + private static double kSteerInertia = 0.00001; + private static double kDriveInertia = 0.001; + + private static final SwerveDrivetrainConstants DrivetrainConstants = + new SwerveDrivetrainConstants() + .withPigeon2Id(kPigeonId) + .withCANbusName(kCANbusName) + .withSupportsPro(true); + + private static final SwerveModuleConstantsFactory ConstantCreator = + new SwerveModuleConstantsFactory() + .withDriveMotorGearRatio(kDriveGearRatio) + .withSteerMotorGearRatio(kSteerGearRatio) + .withWheelRadius(kWheelRadiusInches) + .withSlipCurrent(30) + .withSteerMotorGains(steerGains) + .withDriveMotorGains(driveGains) + .withSpeedAt12VoltsMps( + 6) // Theoretical free speed is 10 meters per second at 12v applied output + .withSteerInertia(kSteerInertia) + .withDriveInertia(kDriveInertia) + .withFeedbackSource(SwerveModuleSteerFeedbackType.FusedCANcoder) + .withCouplingGearRatio( + kCoupleRatio) // Every 1 rotation of the azimuth results in couple ratio drive turns + .withSteerMotorInverted(kSteerMotorReversed); + + private static final int kFrontLeftDriveMotorId = 5; + private static final int kFrontLeftSteerMotorId = 4; + private static final int kFrontLeftEncoderId = 2; + private static final double kFrontLeftEncoderOffset = -0.83544921875; + + private static final double kFrontLeftXPosInches = 10.5; + private static final double kFrontLeftYPosInches = 10.5; + private static final int kFrontRightDriveMotorId = 7; + private static final int kFrontRightSteerMotorId = 6; + private static final int kFrontRightEncoderId = 3; + private static final double kFrontRightEncoderOffset = -0.15234375; + + private static final double kFrontRightXPosInches = 10.5; + private static final double kFrontRightYPosInches = -10.5; + private static final int kBackLeftDriveMotorId = 1; + private static final int kBackLeftSteerMotorId = 0; + private static final int kBackLeftEncoderId = 0; + private static final double kBackLeftEncoderOffset = -0.4794921875; + + private static final double kBackLeftXPosInches = -10.5; + private static final double kBackLeftYPosInches = 10.5; + private static final int kBackRightDriveMotorId = 3; + private static final int kBackRightSteerMotorId = 2; + private static final int kBackRightEncoderId = 1; + private static final double kBackRightEncoderOffset = -0.84130859375; + + private static final double kBackRightXPosInches = -10.5; + private static final double kBackRightYPosInches = -10.5; + + private static final SwerveModuleConstants FrontLeft = + ConstantCreator.createModuleConstants( + kFrontLeftSteerMotorId, + kFrontLeftDriveMotorId, + kFrontLeftEncoderId, + kFrontLeftEncoderOffset, + Units.inchesToMeters(kFrontLeftXPosInches), + Units.inchesToMeters(kFrontLeftYPosInches), + kInvertLeftSide); + private static final SwerveModuleConstants FrontRight = + ConstantCreator.createModuleConstants( + kFrontRightSteerMotorId, + kFrontRightDriveMotorId, + kFrontRightEncoderId, + kFrontRightEncoderOffset, + Units.inchesToMeters(kFrontRightXPosInches), + Units.inchesToMeters(kFrontRightYPosInches), + kInvertRightSide); + private static final SwerveModuleConstants BackLeft = + ConstantCreator.createModuleConstants( + kBackLeftSteerMotorId, + kBackLeftDriveMotorId, + kBackLeftEncoderId, + kBackLeftEncoderOffset, + Units.inchesToMeters(kBackLeftXPosInches), + Units.inchesToMeters(kBackLeftYPosInches), + kInvertLeftSide); + private static final SwerveModuleConstants BackRight = + ConstantCreator.createModuleConstants( + kBackRightSteerMotorId, + kBackRightDriveMotorId, + kBackRightEncoderId, + kBackRightEncoderOffset, + Units.inchesToMeters(kBackRightXPosInches), + Units.inchesToMeters(kBackRightYPosInches), + kInvertRightSide); + + public static final CommandSwerveDrivetrain DriveTrain = + new CommandSwerveDrivetrain( + DrivetrainConstants, 250, FrontLeft, FrontRight, BackLeft, BackRight); } From 79ee7be39bb3b3838df745170a38969818d8c03f Mon Sep 17 00:00:00 2001 From: Ian Tapply Date: Thu, 23 Nov 2023 08:20:16 -0500 Subject: [PATCH 05/14] Update 2024-beta branch (#35) Updated 2024-beta branch to be synced with main --------- Co-authored-by: Cole MacPhail --- .github/workflows/format.yml | 3 +++ README.md | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 251b269..c563087 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -17,6 +17,9 @@ jobs: - uses: actions/checkout@v3 with: ref: ${{ github.event.pull_request.head.ref }} + - uses: actions/setup-java@v2 + with: + java-version: 17 - uses: axel-op/googlejavaformat-action@v3 with: args: "--skip-sorting-imports --replace" diff --git a/README.md b/README.md index b07a9e9..c3afaab 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ +[![Build](https://github.com/Simbotics/Simbot-Base/actions/workflows/build.yml/badge.svg)](https://github.com/Simbotics/Simbot-Base/actions/workflows/build.yml) + # Simbot-Base -A base robot with the most up to date utilities and starting components to get started on robot code as fast as possible +A base robot with the most up-to-date utilities and starting components to get started on robot code as fast as possible ## Contributors and Credits Thank you to the following who has contributed, supplied the tools to make this project even possible, or has supported this project along the way. From 73ea7dfe66630aed1c5f2d8a012da5a38fe97474 Mon Sep 17 00:00:00 2001 From: Cobblestone Date: Sat, 2 Dec 2023 00:00:17 -0500 Subject: [PATCH 06/14] Create example subsystem (#32) # Description This pull request adds an example subsystem for students to use to learn about how subsystems and commands are used in a robot environment. Fixes #18 ## Type of change Please delete options that are not relevant. - [X] New feature (non-breaking change which adds functionality) # Checklist: - [x] My code follows the style guidelines of this project - [X] I have performed a self-review of my code - [X] I have commented my code, particularly in hard-to-understand areas - [X] I have made corresponding changes to the documentation, if any - [X] My changes generate no new warnings - [ ] I have performed tests that prove my fix is effective or that my feature works, if necessary - [ ] New and existing unit tests pass locally with my changes --------- Co-authored-by: Ian Tapply Co-authored-by: Cole MacPhail --- .../subsystems/intake/IntakeConstants.java | 36 ++++ .../subsystems/intake/IntakeSubsystem.java | 193 ++++++++++++++++++ .../intake/enums/IntakeGamepieces.java | 6 + .../intake/enums/IntakeScoreType.java | 14 ++ 4 files changed, 249 insertions(+) create mode 100644 src/main/java/frc/robot/subsystems/intake/IntakeConstants.java create mode 100644 src/main/java/frc/robot/subsystems/intake/IntakeSubsystem.java create mode 100644 src/main/java/frc/robot/subsystems/intake/enums/IntakeGamepieces.java create mode 100644 src/main/java/frc/robot/subsystems/intake/enums/IntakeScoreType.java diff --git a/src/main/java/frc/robot/subsystems/intake/IntakeConstants.java b/src/main/java/frc/robot/subsystems/intake/IntakeConstants.java new file mode 100644 index 0000000..9667bad --- /dev/null +++ b/src/main/java/frc/robot/subsystems/intake/IntakeConstants.java @@ -0,0 +1,36 @@ +package frc.robot.subsystems.intake; + +public class IntakeConstants { + + /** Used when scoring/outtaking */ + public class OuttakeSpeeds { + public static final double LOW_CUBE = 0.3; + public static final double LOW_CONE = 0.6; + public static final double LOW_CUBE_AUTO = 0.3; + public static final double LOW_CONE_AUTO = 1.0; + + public static final double MID_CONE = 0.1; + public static final double MID_CONE_TIPPED = 0.8; + public static final double MID_CUBE = 0.25; + public static final double MID_CUBE_AUTO = 0.2; + + public static final double HIGH_CONE = 0.15; + public static final double HIGH_CONE_AUTO = 0.15; + public static final double HIGH_CUBE = 0.55; + public static final double HIGH_CUBE_AUTO = 0.6; + } + + public static final double INTAKING_SPEED = 0.95; + + public static final int INTAKE_MOTOR_CHANNEL = 6; + public static final double INTAKE_AMP_THRESHOLD = 16; + + // 20ms per periodic cycle * number of periodic cycles / 1000 to get as milliseconds + public static final double INTAKE_CUBE_DELAY = (20 * 15) / 1000; + public static final double INTAKE_CONE_DELAY = (20 * 18) / 1000; + + public static final double HOLD_CONE_SPEED = 0.18; + public static final double HOLD_CUBE_SPEED = 0.15; + + +} diff --git a/src/main/java/frc/robot/subsystems/intake/IntakeSubsystem.java b/src/main/java/frc/robot/subsystems/intake/IntakeSubsystem.java new file mode 100644 index 0000000..9b5a3df --- /dev/null +++ b/src/main/java/frc/robot/subsystems/intake/IntakeSubsystem.java @@ -0,0 +1,193 @@ +package frc.robot.subsystems.intake; + +import edu.wpi.first.wpilibj.PowerDistribution; +import edu.wpi.first.wpilibj.motorcontrol.VictorSP; +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.SubsystemBase; +import frc.robot.subsystems.intake.enums.IntakeGamepieces; +import frc.robot.subsystems.intake.enums.IntakeScoreType; + +/** + * This an example implementation of our + * intake subsystem from 2023. + * + * Each subsystem represents a different purpose of the robot + * such as arms, LEDs, drivetrains, shooter, etc. + */ +public class IntakeSubsystem extends SubsystemBase { + + private VictorSP intakeMotor = new VictorSP(IntakeConstants.INTAKE_MOTOR_CHANNEL); + private PowerDistribution pdp; + + public IntakeSubsystem(PowerDistribution pdp) { + this.pdp = pdp; + } + + /** + * This method runs once every 20ms. + * + * This is useful for updating subsystem-specific + * state that you don't want to offload to a Command. + * + * Teams should try to be consistent within their + * own codebases about which responsibilities will + * be handled by Commands, and which will be handled here. + */ + @Override + public void periodic() { + SmartDashboard.putNumber("INTAKE CURRENT", this.pdp.getCurrent(IntakeConstants.INTAKE_MOTOR_CHANNEL)); + SmartDashboard.putNumber("INTAKE SPEED", this.intakeMotor.get()); + } + + /** + * Runs a command that stops the intake + * + * @return a command that stops the intake + */ + public Command stopIntakeCommand() { + return runOnce(() -> this.intakeMotor.set(0)) + .ignoringDisable(true); + } + + /** + * Runs a command to score a gamepiece. + * + * @param type the type of the score you want to make + * @param expectedPiece the type of gamepiece to expect when scoring + * @return a command that scores a gamepiece + */ + public Command intakeScoreCommand(IntakeScoreType type, IntakeGamepieces expectedPiece) { + return run(() -> { + + switch (type) { + case MID_CONE -> { + Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.MID_CONE), this) + .andThen(Commands.waitSeconds(0.5)) + .andThen(stopIntakeCommand()); + } + + case MID_CONE_TIPPED -> { + Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.MID_CONE_TIPPED), this) + .andThen(Commands.waitSeconds(0.5)) + .andThen(stopIntakeCommand()); + } + + case MID_CUBE -> { + Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.MID_CUBE), this) + .andThen(Commands.waitSeconds(0.5)) + .andThen(stopIntakeCommand()); + } + + case MID_CUBE_AUTO -> { + Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.MID_CUBE_AUTO), this) + .andThen(Commands.waitSeconds(0.5)) + .andThen(stopIntakeCommand()); + } + + case HIGH_CONE -> { + Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.HIGH_CONE), this) + .andThen(Commands.waitSeconds(0.5)) + .andThen(stopIntakeCommand()); + } + + case HIGH_CONE_AUTO -> { + Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.HIGH_CONE_AUTO), this) + .andThen(Commands.waitSeconds(0.5)) + .andThen(stopIntakeCommand()); + } + + case HIGH_CUBE -> { + Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.HIGH_CUBE), this) + .andThen(Commands.waitSeconds(0.5)) + .andThen(stopIntakeCommand()); + } + + case HIGH_CUBE_AUTO -> { + Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.HIGH_CUBE_AUTO), this) + .andThen(Commands.waitSeconds(0.5)) + .andThen(stopIntakeCommand()); + } + + case LOW -> { + if (expectedPiece.equals(IntakeGamepieces.CUBE)) { + Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.LOW_CUBE), this) + .andThen(Commands.waitSeconds(1)) + .andThen(stopIntakeCommand()); + } else { + Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.LOW_CONE), this) + .andThen(Commands.waitSeconds(1)) + .andThen(stopIntakeCommand()); + } + } + + case LOW_AUTO -> { + if (expectedPiece.equals(IntakeGamepieces.CUBE)) { + Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.LOW_CUBE_AUTO), this) + .andThen(Commands.waitSeconds(1)) + .andThen(stopIntakeCommand()); + } else { + Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.LOW_CONE_AUTO), this) + .andThen(Commands.waitSeconds(1)) + .andThen(stopIntakeCommand()); + } + } + } + }).andThen(stopIntakeCommand()); + } + + /** + * Runs a command that intakes and holds a gamepiece + * + * @param gamepiece the type of gamepiece to expect + * @return a command that forces the intake to hold the specified gamepiece + */ + public Command intakeHoldCommand(IntakeGamepieces gamepiece) { + return run(() -> { + this.intakeMotor.set(IntakeConstants.INTAKING_SPEED); + + // this will keep the motor running as long as the current is low enough + // this results in something similar to an if statement + Commands.waitUntil(() -> this.pdp + .getCurrent(IntakeConstants.INTAKE_MOTOR_CHANNEL) < IntakeConstants.INTAKE_AMP_THRESHOLD); + + if (gamepiece.equals(IntakeGamepieces.CUBE)) { + // wait a short amount of time so the gamepiece gets pulled in + Commands.waitSeconds(IntakeConstants.INTAKE_CUBE_DELAY); + this.intakeMotor.set(IntakeConstants.HOLD_CUBE_SPEED); + } + if (gamepiece.equals(IntakeGamepieces.CONE)) { + // we have a cone, so run the motor at a higher speed + Commands.waitSeconds(IntakeConstants.INTAKE_CONE_DELAY); + this.intakeMotor.set(IntakeConstants.HOLD_CONE_SPEED); + } + }).finallyDo(() -> this.intakeMotor.set(0)) + .ignoringDisable(false); + } + + /** + * Runs a command that spits out its gamepiece + * + * @param speed speed to run the motor at, this is pre-inverted + * @return a command that forces the intake to spit out its gamepiece + */ + public Command outtakeCommand(double speed) { + return runOnce(() -> this.intakeMotor.set(-speed)); + } + + /** + * This method is similar to periodic, + * but only runs in simulation mode. + * + * Useful for updating subsystem-specific state + * that needs to be maintained for simulations, such as + * for updating edu.wpi.first.wpilibj.simulation classes + * and setting simulated sensor readings. + */ + @Override + public void simulationPeriodic() { + SmartDashboard.putNumber("INTAKE CURRENT", this.pdp.getCurrent(IntakeConstants.INTAKE_MOTOR_CHANNEL)); + SmartDashboard.putNumber("INTAKE SPEED", this.intakeMotor.get()); + } +} diff --git a/src/main/java/frc/robot/subsystems/intake/enums/IntakeGamepieces.java b/src/main/java/frc/robot/subsystems/intake/enums/IntakeGamepieces.java new file mode 100644 index 0000000..c5eb73b --- /dev/null +++ b/src/main/java/frc/robot/subsystems/intake/enums/IntakeGamepieces.java @@ -0,0 +1,6 @@ +package frc.robot.subsystems.intake.enums; + +public enum IntakeGamepieces { + CONE, + CUBE +} diff --git a/src/main/java/frc/robot/subsystems/intake/enums/IntakeScoreType.java b/src/main/java/frc/robot/subsystems/intake/enums/IntakeScoreType.java new file mode 100644 index 0000000..12e6a64 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/intake/enums/IntakeScoreType.java @@ -0,0 +1,14 @@ +package frc.robot.subsystems.intake.enums; + +public enum IntakeScoreType { + HIGH_CONE, + HIGH_CONE_AUTO, + HIGH_CUBE, + HIGH_CUBE_AUTO, + MID_CONE, + MID_CONE_TIPPED, + MID_CUBE, + MID_CUBE_AUTO, + LOW, + LOW_AUTO +} From 85dcd5e5f0b80e18267af0a82b04b72e19d799f3 Mon Sep 17 00:00:00 2001 From: Ian Tapply Date: Mon, 4 Dec 2023 12:33:03 -0500 Subject: [PATCH 07/14] Port over LEDs with segment support and LED modes (#29) # Description This pull request adds support for addressable LEDs. It also allows you to create LEDs with multiple segments so you can set the mode of each segment to your desire and allows you to seamlessly add your own custom LED modes. Fixes #28 ## Type of change Please delete options that are not relevant. - [x] New feature (non-breaking change which adds functionality) - [x] This change requires a documentation update # How Has This Been Tested? So far it hasn't, right now this will stay as a draft PR until tested further # Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation, if any - [x] My changes generate no new warnings - [ ] I have performed tests that prove my fix is effective or that my feature works, if necessary - [ ] New and existing unit tests pass locally with my changes --------- Co-authored-by: github-actions <> Co-authored-by: Cole MacPhail --- src/main/java/frc/robot/RobotContainer.java | 5 ++ .../frc/robot/subsystems/led/LEDColour.java | 89 +++++++++++++++++++ .../robot/subsystems/led/LEDConstants.java | 7 ++ .../frc/robot/subsystems/led/LEDSegment.java | 76 ++++++++++++++++ .../robot/subsystems/led/LEDSubsystem.java | 60 +++++++++++++ .../java/frc/robot/subsystems/led/README.md | 36 ++++++++ .../InvalidLEDSegmentException.java | 7 ++ .../robot/subsystems/led/modes/Breathing.java | 41 +++++++++ .../robot/subsystems/led/modes/LEDMode.java | 10 +++ .../frc/robot/subsystems/led/modes/Solid.java | 35 ++++++++ .../subsystems/led/modes/special/Off.java | 35 ++++++++ .../subsystems/led/modes/special/PrideBi.java | 58 ++++++++++++ .../led/modes/special/RGBSplit.java | 52 +++++++++++ .../subsystems/led/modes/special/Rainbow.java | 35 ++++++++ .../led/modes/special/StaticPride.java | 29 ++++++ 15 files changed, 575 insertions(+) create mode 100644 src/main/java/frc/robot/subsystems/led/LEDColour.java create mode 100644 src/main/java/frc/robot/subsystems/led/LEDConstants.java create mode 100644 src/main/java/frc/robot/subsystems/led/LEDSegment.java create mode 100644 src/main/java/frc/robot/subsystems/led/LEDSubsystem.java create mode 100644 src/main/java/frc/robot/subsystems/led/README.md create mode 100644 src/main/java/frc/robot/subsystems/led/exceptions/InvalidLEDSegmentException.java create mode 100644 src/main/java/frc/robot/subsystems/led/modes/Breathing.java create mode 100644 src/main/java/frc/robot/subsystems/led/modes/LEDMode.java create mode 100644 src/main/java/frc/robot/subsystems/led/modes/Solid.java create mode 100644 src/main/java/frc/robot/subsystems/led/modes/special/Off.java create mode 100644 src/main/java/frc/robot/subsystems/led/modes/special/PrideBi.java create mode 100644 src/main/java/frc/robot/subsystems/led/modes/special/RGBSplit.java create mode 100644 src/main/java/frc/robot/subsystems/led/modes/special/Rainbow.java create mode 100644 src/main/java/frc/robot/subsystems/led/modes/special/StaticPride.java diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java index 556d128..040986b 100644 --- a/src/main/java/frc/robot/RobotContainer.java +++ b/src/main/java/frc/robot/RobotContainer.java @@ -11,6 +11,7 @@ import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.InstantCommand; import frc.robot.generated.TunerConstants; +import frc.robot.subsystems.led.LEDSubsystem; public class RobotContainer { final double MaxSpeed = 6; // 6 meters per second desired top speed @@ -20,6 +21,7 @@ public class RobotContainer { CommandController driver = new CommandController(0); // Driver Controller CommandController operator = new CommandController(1); // Operator Controller CommandSwerveDrivetrain drivetrain = TunerConstants.DriveTrain; // My drivetrain + private final LEDSubsystem ledSubsystem; SwerveRequest.FieldCentric drive = new SwerveRequest.FieldCentric() .withIsOpenLoop(true) @@ -39,6 +41,8 @@ public class RobotContainer { Pose2d odomStart = new Pose2d(0, 0, new Rotation2d(0, 0)); private void configureBindings() { + ledSubsystem.setDefaultCommand(new InstantCommand(() -> ledSubsystem.periodic(), ledSubsystem)); + drivetrain.setDefaultCommand( // Drivetrain will execute this command periodically drivetrain .applyRequest( @@ -78,6 +82,7 @@ private void configureBindings() { public RobotContainer() { configureBindings(); + ledSubsystem = new LEDSubsystem(); } public Command getAutonomousCommand() { diff --git a/src/main/java/frc/robot/subsystems/led/LEDColour.java b/src/main/java/frc/robot/subsystems/led/LEDColour.java new file mode 100644 index 0000000..c908bb2 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/led/LEDColour.java @@ -0,0 +1,89 @@ +package frc.robot.subsystems.led; + +/** + * This is only used for setting solid colour states. Please see the LEDModes.java class for special + * LED modes + */ +public class LEDColour { + private double red, green, blue; + + public LEDColour(int red, int green, int blue) { + this.red = red; + this.green = green; + this.blue = blue; + } + + public LEDColour(double red, double green, double blue) { + this.red = red; + this.green = green; + this.blue = blue; + } + + /** + * Copies the RGB values from an LEDState object to the current one + * + * @param ledState The LEDState to copy the RGB values from + * @return The current instane of LEDColour with the new RGB values + */ + public LEDColour copy(LEDColour ledState) { + this.red = ledState.red; + this.green = ledState.green; + this.blue = ledState.blue; + + return this; + } + + /** + * Gets the red value of an LED state object + * + * @return The double value of the red value in the RGB sequence + */ + public double getRed() { + return this.red; + } + + /** + * Gets the red value of an LED state object as an integer + * + * @return The integer value of the red value in the RGB sequence + */ + public int getRedInt() { + return (int) this.red; + } + + /** + * Gets the green value of an LED state object + * + * @return The double value of the green value in the RGB sequence + */ + public double getGreen() { + return this.green; + } + + /** + * Gets the green value of an LED state object as an integer + * + * @return The integer value of the green value in the RGB sequence + */ + public int getGreenInt() { + return (int) this.green; + } + + /** + * Gets the blue value of an LED state object + * + * @return The integer value of the blue value in the RGB sequence + */ + public double getBlue() { + return this.blue; + } + + /** + * Gets the blue value of an LED state object as an integer + * + * @return The integer value of the blue value in the RGB sequence + */ + public int getBlueInt() { + return (int) this.blue; + } +} diff --git a/src/main/java/frc/robot/subsystems/led/LEDConstants.java b/src/main/java/frc/robot/subsystems/led/LEDConstants.java new file mode 100644 index 0000000..970777a --- /dev/null +++ b/src/main/java/frc/robot/subsystems/led/LEDConstants.java @@ -0,0 +1,7 @@ +package frc.robot.subsystems.led; + +public class LEDConstants { + + public static final int ledsPerSegment = + 16; // The number LEDs (actual diodes) there are on each segment +} diff --git a/src/main/java/frc/robot/subsystems/led/LEDSegment.java b/src/main/java/frc/robot/subsystems/led/LEDSegment.java new file mode 100644 index 0000000..9c6e9c7 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/led/LEDSegment.java @@ -0,0 +1,76 @@ +package frc.robot.subsystems.led; + +import frc.robot.subsystems.led.exceptions.InvalidLEDSegmentException; +import frc.robot.subsystems.led.modes.Breathing; +import frc.robot.subsystems.led.modes.LEDMode; +import frc.robot.subsystems.led.modes.Solid; + +/** + * We are going to attach a name to each index in the LED modes array here. Remember that all + * indexes start at 0 and NOT 1 + */ +public enum LEDSegment { + + // Add all aliases for segments below + FrontLeft(0, new Solid(new LEDColour(255, 0, 0))), // Set the LEDs to solid red + BackLeft(1, new Breathing(new LEDColour(0, 255, 0))), // Create a green breathing effect + BackRight(2, new Breathing(new LEDColour(0, 255, 0))), // Create a green breathing effect + FrontRight(3, new Solid(new LEDColour(255, 0, 0))); // Set the LEDs to solid red + + public final int segmentNumber; // The segment number of the LED strip (starts at 1 and goes up) + public LEDMode ledMode; // The mode of the LED strip + + private LEDSegment(int segmentNumber, LEDMode defaultLedMode) { + this.segmentNumber = segmentNumber; + this.ledMode = defaultLedMode; + } + + /** + * Checks if the LED segment is a valid segment based off of the number of segments in + * LEDConstants.java + * + * @return True if it is within the index bounds, false if it isn't + */ + private boolean isValid() { + // Return a boolean based on if the segment number is greater than number of + // segments + return (this.getSegmentIdentifier() < LEDSubsystem.ledSegments.size()); + } + + /** + * Sets the mode of the current segment + * + * @param ledMode The mode to set the LEDs to + */ + public void setLedMode(LEDMode ledMode) { + // Guard clause to check if the segment is within the bounds of the number of + // available segments + if (!this.isValid()) { + throw new InvalidLEDSegmentException( + String.format( + "Invalid LED segment: %d. Number of segments: %d", + this.getSegmentIdentifier(), + LEDSubsystem.ledSegments.size())); // Throw an exception with the segment information + } + + this.ledMode = ledMode; + } + + /** + * Retrieves the true segment number of an LED segment + * + * @return The segment number of the LED segment + */ + public int getSegmentIdentifier() { + return this.segmentNumber; + } + + /** + * Gets the LED mode for this segment + * + * @return The LED mode of the current segment as an LEDMode object + */ + public LEDMode getLedMode() { + return this.ledMode; + } +} diff --git a/src/main/java/frc/robot/subsystems/led/LEDSubsystem.java b/src/main/java/frc/robot/subsystems/led/LEDSubsystem.java new file mode 100644 index 0000000..7d16a49 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/led/LEDSubsystem.java @@ -0,0 +1,60 @@ +package frc.robot.subsystems.led; + +import java.util.ArrayList; +import java.util.List; + +import edu.wpi.first.wpilibj.AddressableLED; +import edu.wpi.first.wpilibj.AddressableLEDBuffer; +import edu.wpi.first.wpilibj2.command.SubsystemBase; +import frc.robot.subsystems.led.modes.LEDMode; + +public class LEDSubsystem extends SubsystemBase { + + public static List ledSegments = new ArrayList<>(); + + public static AddressableLED leds = new AddressableLED(0); // The PWM port the LEDs are plugged into + public static AddressableLEDBuffer ledBuffer = new AddressableLEDBuffer( + (ledSegments.size() * LEDConstants.ledsPerSegment)); // The buffer that holds the LED data + + @Override + public void periodic() { + // For every segment that is registered, run the periodic function + for (LEDSegment ledSegment : ledSegments) { + ledSegment.getLedMode().periodic(ledSegment.getSegmentIdentifier()); + } + } + + /** Does the basic initialization process of setting the length of the LEDs */ + public void initialize() { + // Register all LED segments into the array + ledSegments.add(LEDSegment.FrontLeft); + ledSegments.add(LEDSegment.BackLeft); + ledSegments.add(LEDSegment.BackRight); + ledSegments.add(LEDSegment.FrontRight); + + leds.setLength( + (ledSegments.size() + * LEDConstants.ledsPerSegment)); // Set the length of the LED strip + + leds.start(); // Start the LED strip + } + + /** + * Sets the mode for all segments available + * + * @param ledMode The mode to set them all as. Please see the modes directory + * for all available + * modes + */ + public void setAllSegmentModesCommand(LEDMode ledMode) { + // For every segment we can set the mode of, set the mode as the one provided + for (LEDSegment ledSegment : ledSegments) { + ledSegment.setLedMode(ledMode); + } + } + + /** Initialize the LED subsystem when we create an instance of it */ + public LEDSubsystem() { + this.initialize(); + } +} diff --git a/src/main/java/frc/robot/subsystems/led/README.md b/src/main/java/frc/robot/subsystems/led/README.md new file mode 100644 index 0000000..eaea1a3 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/led/README.md @@ -0,0 +1,36 @@ +## How do I create a new segment of LEDs? +Creating a new segment could not be easier. Simply just do the following: +1. Open the `LEDSegment` class in this subsystem directory. +2. Add a new segment with a proper name at the top of the class with the rest of the segments. +3. Set the segment number to what number of segment it is. This number will start at 1 from the first segment and will go up by one for each segment you create. +4. Set the default mode to be something like `SolidRed` or to another mode you've created. +5. Add the following block of code to the initialize function in the `LEDSubsystem` class, be sure to replace segment name with the name of your segment as specified previously : +``` +LEDConstants.ledSegments.add(LEDSegment.); +``` + +You're all set now! You've successfully created a new segment of LEDs for your robot. + +## How do I create a new LED mode? +We've made it simple and easy to create LED modes with this subsystem. Just follow the following steps: +1. Create a new Java class in the `modes` directory. +2. Extend that class from the `LEDMode` class. +3. Add any code you want to repeat every 20ms to the periodic section! This should update your LED's constantly when they're on that mode. +4. Add any code you want to run initially into the initialize section. + +You're done! Our goal was to make this as simple and easy to do as a beginner and with limited Java knowledge. + +## I'm getting an "Invalid LED segment" error, how do I fix it? +It seems that the index for the segment is higher than the number of segments specified in the `LEDConstants` class. Be aware that an index is different than the actual number of segments, it will always be one less than what the segment number is. + +Be sure to either change the value of the number of segments, or update the index of your segment. + +## Why is my LED segment not turning on? +This could be for several reason. Please see the following for possible causes: +- Does your console have any errors in it regarding LEDs? +- Is the segment registered and added to the ledSegments array in the initialize function in `LEDSubsystem` class? +- Is the segment created in the `LEDSegment` class and have a valid index and LED mode? +- Is the number of LEDs per segment the actual number on the physical LED strip? +- Is the number of segments the correct number? + +Hopefully this gets the main points of failure. If for some reason the issue is due to our codebase, feel free to open an issue via our [issue tracker](https://github.com/Simbotics/Simbot-Base/issues). Be sure to follow our issue template when opening an issue. \ No newline at end of file diff --git a/src/main/java/frc/robot/subsystems/led/exceptions/InvalidLEDSegmentException.java b/src/main/java/frc/robot/subsystems/led/exceptions/InvalidLEDSegmentException.java new file mode 100644 index 0000000..d4d82f1 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/led/exceptions/InvalidLEDSegmentException.java @@ -0,0 +1,7 @@ +package frc.robot.subsystems.led.exceptions; + +public class InvalidLEDSegmentException extends RuntimeException { + public InvalidLEDSegmentException(String message) { + super(message); + } +} diff --git a/src/main/java/frc/robot/subsystems/led/modes/Breathing.java b/src/main/java/frc/robot/subsystems/led/modes/Breathing.java new file mode 100644 index 0000000..35cafb1 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/led/modes/Breathing.java @@ -0,0 +1,41 @@ +package frc.robot.subsystems.led.modes; + +import edu.wpi.first.wpilibj.util.Color; +import frc.robot.subsystems.led.LEDColour; +import frc.robot.subsystems.led.LEDConstants; +import frc.robot.subsystems.led.LEDSubsystem; + +public class Breathing extends LEDMode { + private int cycle; + private LEDColour ledColor; + + public Breathing(LEDColour ledColour) { + this.ledColor = ledColour; + } + + @Override + public void initialize() { + this.cycle = 1; + System.out.println("Starting the Breathing mode"); + } + + @Override + public void periodic(int segmentIndex) { + int minSegWindow = segmentIndex * LEDConstants.ledsPerSegment; + int maxSegWindow = minSegWindow + LEDConstants.ledsPerSegment; + + for (int i = minSegWindow; i < maxSegWindow; i++) { + LEDSubsystem.ledBuffer.setLED(i, calculateBreathingColor()); + } + + this.cycle++; + } + + private Color calculateBreathingColor() { + double breathingValue = (Math.sin(Math.PI * this.cycle / 80.0) + 1.0) / 2.0; + return new Color( + this.ledColor.getRed() * breathingValue, + this.ledColor.getGreen() * breathingValue, + this.ledColor.getBlue() * breathingValue); + } +} diff --git a/src/main/java/frc/robot/subsystems/led/modes/LEDMode.java b/src/main/java/frc/robot/subsystems/led/modes/LEDMode.java new file mode 100644 index 0000000..1037284 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/led/modes/LEDMode.java @@ -0,0 +1,10 @@ +package frc.robot.subsystems.led.modes; + +public abstract class LEDMode { + + /** Runs when the mode is first called. This can be used to set constants/variabled */ + public abstract void initialize(); + + /** Runs constantly every 20ms, this is where you want to calculate what your LEDs do */ + public abstract void periodic(int segmentIndex); +} diff --git a/src/main/java/frc/robot/subsystems/led/modes/Solid.java b/src/main/java/frc/robot/subsystems/led/modes/Solid.java new file mode 100644 index 0000000..60587db --- /dev/null +++ b/src/main/java/frc/robot/subsystems/led/modes/Solid.java @@ -0,0 +1,35 @@ +package frc.robot.subsystems.led.modes; + +import frc.robot.subsystems.led.LEDColour; +import frc.robot.subsystems.led.LEDConstants; +import frc.robot.subsystems.led.LEDSubsystem; + +public class Solid extends LEDMode { + private LEDColour ledColour; + + public Solid(LEDColour ledColour) { + this.ledColour = ledColour; + } + + @Override + public void initialize() { + System.out.println("Starting the Solid LED mode"); + } + + @Override + public void periodic(int segmentIndex) { + int minSegWindow = + segmentIndex + * LEDConstants.ledsPerSegment; // Set the start of the segment to display the LEDs from + int maxSegWindow = + minSegWindow + + LEDConstants + .ledsPerSegment; // Set the end of the segment so we know where to stop displaying + // LEDs + // For every LED in the segment window, we are going to set its colour + for (int i = minSegWindow; i < maxSegWindow; i++) { + LEDSubsystem.ledBuffer.setRGB( + i, ledColour.getRedInt(), ledColour.getGreenInt(), ledColour.getBlueInt()); + } + } +} diff --git a/src/main/java/frc/robot/subsystems/led/modes/special/Off.java b/src/main/java/frc/robot/subsystems/led/modes/special/Off.java new file mode 100644 index 0000000..072db3c --- /dev/null +++ b/src/main/java/frc/robot/subsystems/led/modes/special/Off.java @@ -0,0 +1,35 @@ +package frc.robot.subsystems.led.modes.special; + +import frc.robot.subsystems.led.LEDColour; +import frc.robot.subsystems.led.LEDConstants; +import frc.robot.subsystems.led.LEDSubsystem; +import frc.robot.subsystems.led.modes.LEDMode; + +public class Off extends LEDMode { + + @Override + public void initialize() { + System.out.println("Starting the Off LED mode"); + } + + @Override + public void periodic(int segmentIndex) { + int minSegWindow = + segmentIndex + * LEDConstants.ledsPerSegment; // Set the start of the segment to display the LEDs from + int maxSegWindow = + minSegWindow + + LEDConstants + .ledsPerSegment; // Set the end of the segment so we know where to stop displaying + // LEDs + + LEDColour offLedColour = + new LEDColour(0, 0, 0); // Create a new RGB sequence with all values set to 0 + + // For every LED in the segment window, we are going to set its colour + for (int i = minSegWindow; i < maxSegWindow; i++) { + LEDSubsystem.ledBuffer.setRGB( + i, offLedColour.getRedInt(), offLedColour.getGreenInt(), offLedColour.getBlueInt()); + } + } +} diff --git a/src/main/java/frc/robot/subsystems/led/modes/special/PrideBi.java b/src/main/java/frc/robot/subsystems/led/modes/special/PrideBi.java new file mode 100644 index 0000000..5f8fb7e --- /dev/null +++ b/src/main/java/frc/robot/subsystems/led/modes/special/PrideBi.java @@ -0,0 +1,58 @@ +package frc.robot.subsystems.led.modes.special; + +import frc.robot.subsystems.led.LEDColour; +import frc.robot.subsystems.led.LEDConstants; +import frc.robot.subsystems.led.LEDSubsystem; +import frc.robot.subsystems.led.modes.LEDMode; + +public class PrideBi extends LEDMode { + + @Override + public void initialize() { + System.out.println("Starting the PrideBi LED mode"); + } + + @Override + public void periodic(int segmentIndex) { + int minSegWindow = + segmentIndex + * LEDConstants.ledsPerSegment; // Set the start of the segment to display the LEDs from + int maxSegWindow = + minSegWindow + + LEDConstants + .ledsPerSegment; // Set the end of the segment so we know where to stop displaying + // LEDs + + // Divide the segment into three equal parts + int segmentSize = LEDConstants.ledsPerSegment / 3; + + // Set the color for each part + LEDColour magentaLedColour = new LEDColour(214, 2, 112); + LEDColour purpleLedColour = new LEDColour(155, 79, 150); + LEDColour royalBlueLedColour = new LEDColour(0, 56, 168); + + // For every LED in the segment window, set its color based on the part of the segment it + // belongs to + for (int i = minSegWindow; i < maxSegWindow; i++) { + if (i < minSegWindow + segmentSize) { + LEDSubsystem.ledBuffer.setRGB( + i, + magentaLedColour.getRedInt(), + magentaLedColour.getGreenInt(), + magentaLedColour.getBlueInt()); + } else if (i < minSegWindow + 2 * segmentSize) { + LEDSubsystem.ledBuffer.setRGB( + i, + purpleLedColour.getRedInt(), + purpleLedColour.getGreenInt(), + purpleLedColour.getBlueInt()); + } else { + LEDSubsystem.ledBuffer.setRGB( + i, + royalBlueLedColour.getRedInt(), + royalBlueLedColour.getGreenInt(), + royalBlueLedColour.getBlueInt()); + } + } + } +} diff --git a/src/main/java/frc/robot/subsystems/led/modes/special/RGBSplit.java b/src/main/java/frc/robot/subsystems/led/modes/special/RGBSplit.java new file mode 100644 index 0000000..d19fd22 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/led/modes/special/RGBSplit.java @@ -0,0 +1,52 @@ +package frc.robot.subsystems.led.modes.special; + +import frc.robot.subsystems.led.LEDColour; +import frc.robot.subsystems.led.LEDConstants; +import frc.robot.subsystems.led.LEDSubsystem; +import frc.robot.subsystems.led.modes.LEDMode; + +public class RGBSplit extends LEDMode { + + @Override + public void initialize() { + System.out.println("Starting the RGBSplit LED mode"); + } + + @Override + public void periodic(int segmentIndex) { + int minSegWindow = + segmentIndex + * LEDConstants.ledsPerSegment; // Set the start of the segment to display the LEDs from + int maxSegWindow = + minSegWindow + + LEDConstants + .ledsPerSegment; // Set the end of the segment so we know where to stop displaying + // LEDs + + // Divide the segment into three equal parts + int segmentSize = LEDConstants.ledsPerSegment / 3; + + // Set the color for each part + LEDColour redLedColour = new LEDColour(255, 0, 0); + LEDColour greenLedColour = new LEDColour(0, 255, 0); + LEDColour blueLedColour = new LEDColour(0, 0, 255); + + // For every LED in the segment window, set its color based on the part of the segment it + // belongs to + for (int i = minSegWindow; i < maxSegWindow; i++) { + if (i < minSegWindow + segmentSize) { + LEDSubsystem.ledBuffer.setRGB( + i, redLedColour.getRedInt(), redLedColour.getGreenInt(), redLedColour.getBlueInt()); + } else if (i < minSegWindow + 2 * segmentSize) { + LEDSubsystem.ledBuffer.setRGB( + i, + greenLedColour.getRedInt(), + greenLedColour.getGreenInt(), + greenLedColour.getBlueInt()); + } else { + LEDSubsystem.ledBuffer.setRGB( + i, blueLedColour.getRedInt(), blueLedColour.getGreenInt(), blueLedColour.getBlueInt()); + } + } + } +} diff --git a/src/main/java/frc/robot/subsystems/led/modes/special/Rainbow.java b/src/main/java/frc/robot/subsystems/led/modes/special/Rainbow.java new file mode 100644 index 0000000..b4ebafa --- /dev/null +++ b/src/main/java/frc/robot/subsystems/led/modes/special/Rainbow.java @@ -0,0 +1,35 @@ +package frc.robot.subsystems.led.modes.special; + +import frc.robot.subsystems.led.LEDConstants; +import frc.robot.subsystems.led.LEDSubsystem; +import frc.robot.subsystems.led.modes.LEDMode; + +public class Rainbow extends LEDMode { + + @Override + public void initialize() { + System.out.println("Starting the Rainbow LED mode"); + } + + @Override + public void periodic(int segmentIndex) { + int minSegWindow = + segmentIndex + * LEDConstants.ledsPerSegment; // Set the start of the segment to display the LEDs from + int maxSegWindow = + minSegWindow + + LEDConstants + .ledsPerSegment; // Set the end of the segment so we know where to stop displaying + // LEDs + + double rainbowSpeed = + 15.0; // Set the speed of the rainbow, the higher the number, the faster it is + double hue = + System.currentTimeMillis() + / rainbowSpeed; // Calculate the hue based off the speed and the current system time + + for (int i = minSegWindow; i < maxSegWindow; i++) { + LEDSubsystem.ledBuffer.setHSV(i, (int) ((hue + i * 5) % 180), 255, 255); + } + } +} diff --git a/src/main/java/frc/robot/subsystems/led/modes/special/StaticPride.java b/src/main/java/frc/robot/subsystems/led/modes/special/StaticPride.java new file mode 100644 index 0000000..2e8f49c --- /dev/null +++ b/src/main/java/frc/robot/subsystems/led/modes/special/StaticPride.java @@ -0,0 +1,29 @@ +package frc.robot.subsystems.led.modes.special; + +import frc.robot.subsystems.led.LEDConstants; +import frc.robot.subsystems.led.LEDSubsystem; +import frc.robot.subsystems.led.modes.LEDMode; + +public class StaticPride extends LEDMode { + + @Override + public void initialize() { + System.out.println("Starting the StaticPride LED mode"); + } + + @Override + public void periodic(int segmentIndex) { + int minSegWindow = + segmentIndex + * LEDConstants.ledsPerSegment; // Set the start of the segment to display the LEDs from + int maxSegWindow = + minSegWindow + + LEDConstants + .ledsPerSegment; // Set the end of the segment so we know where to stop displaying + // LEDs + + for (int i = minSegWindow; i < maxSegWindow; i++) { + LEDSubsystem.ledBuffer.setHSV(i, (int) ((i * 5) % 180), 255, 255); + } + } +} From 5896726dbd5fa22c134b4fdf179aaffffebd5820 Mon Sep 17 00:00:00 2001 From: Ian Tapply Date: Mon, 4 Dec 2023 17:08:13 -0500 Subject: [PATCH 08/14] Update 2024-beta branch (#39) # Description Updated `2024-beta` branch with main ## Type of change - [x] New feature (non-breaking change which adds functionality) # Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation, if any - [x] My changes generate no new warnings - [x] I have performed tests that prove my fix is effective or that my feature works, if necessary - [x] New and existing unit tests pass locally with my changes --------- Co-authored-by: Cole MacPhail Co-authored-by: github-actions <> --- .github/workflows/format.yml | 9 +- README.md | 2 +- src/main/java/frc/robot/RobotContainer.java | 1 - .../subsystems/intake/IntakeConstants.java | 60 ++- .../subsystems/intake/IntakeSubsystem.java | 354 +++++++++--------- .../intake/enums/IntakeGamepieces.java | 4 +- .../intake/enums/IntakeScoreType.java | 20 +- .../robot/subsystems/led/LEDSubsystem.java | 16 +- 8 files changed, 235 insertions(+), 231 deletions(-) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index c563087..a1d025e 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -14,12 +14,13 @@ jobs: steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.ref }} - - uses: actions/setup-java@v2 - with: - java-version: 17 + - uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'oracle' - uses: axel-op/googlejavaformat-action@v3 with: args: "--skip-sorting-imports --replace" diff --git a/README.md b/README.md index c3afaab..6aeca4c 100644 --- a/README.md +++ b/README.md @@ -9,4 +9,4 @@ Thank you to the following who has contributed, supplied the tools to make this - [CTR Electronics](https://github.com/CrossTheRoadElec) - Provided an example and starter [repository](https://github.com/CrossTheRoadElec/Phoenix6-Examples/tree/main/java/SwerveWithPathPlanner) to build from - [Ian Tapply](https://github.com/IanTapply22) - Main student contributor - [Kaleb Dodd](https://github.com/kaleb-dodd) - Project coordinator -- [Cole MacPhail](https://github.com/colemacphail) - Project coordinator \ No newline at end of file +- [Cole MacPhail](https://github.com/colemacphail) - Project coordinator diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java index 040986b..6bf31d9 100644 --- a/src/main/java/frc/robot/RobotContainer.java +++ b/src/main/java/frc/robot/RobotContainer.java @@ -71,7 +71,6 @@ private void configureBindings() { // drivetrain.seedFieldRelative(new Pose2d(new Translation2d(), Rotation2d.fromDegrees(90))); // } drivetrain.registerTelemetry(logger::telemeterize); - driver.POVUp() .whileTrue( drivetrain.applyRequest(() -> forwardStraight.withVelocityX(0.5).withVelocityY(0))); diff --git a/src/main/java/frc/robot/subsystems/intake/IntakeConstants.java b/src/main/java/frc/robot/subsystems/intake/IntakeConstants.java index 9667bad..1e00685 100644 --- a/src/main/java/frc/robot/subsystems/intake/IntakeConstants.java +++ b/src/main/java/frc/robot/subsystems/intake/IntakeConstants.java @@ -2,35 +2,33 @@ public class IntakeConstants { - /** Used when scoring/outtaking */ - public class OuttakeSpeeds { - public static final double LOW_CUBE = 0.3; - public static final double LOW_CONE = 0.6; - public static final double LOW_CUBE_AUTO = 0.3; - public static final double LOW_CONE_AUTO = 1.0; - - public static final double MID_CONE = 0.1; - public static final double MID_CONE_TIPPED = 0.8; - public static final double MID_CUBE = 0.25; - public static final double MID_CUBE_AUTO = 0.2; - - public static final double HIGH_CONE = 0.15; - public static final double HIGH_CONE_AUTO = 0.15; - public static final double HIGH_CUBE = 0.55; - public static final double HIGH_CUBE_AUTO = 0.6; - } - - public static final double INTAKING_SPEED = 0.95; - - public static final int INTAKE_MOTOR_CHANNEL = 6; - public static final double INTAKE_AMP_THRESHOLD = 16; - - // 20ms per periodic cycle * number of periodic cycles / 1000 to get as milliseconds - public static final double INTAKE_CUBE_DELAY = (20 * 15) / 1000; - public static final double INTAKE_CONE_DELAY = (20 * 18) / 1000; - - public static final double HOLD_CONE_SPEED = 0.18; - public static final double HOLD_CUBE_SPEED = 0.15; - - + /** Used when scoring/outtaking */ + public class OuttakeSpeeds { + public static final double LOW_CUBE = 0.3; + public static final double LOW_CONE = 0.6; + public static final double LOW_CUBE_AUTO = 0.3; + public static final double LOW_CONE_AUTO = 1.0; + + public static final double MID_CONE = 0.1; + public static final double MID_CONE_TIPPED = 0.8; + public static final double MID_CUBE = 0.25; + public static final double MID_CUBE_AUTO = 0.2; + + public static final double HIGH_CONE = 0.15; + public static final double HIGH_CONE_AUTO = 0.15; + public static final double HIGH_CUBE = 0.55; + public static final double HIGH_CUBE_AUTO = 0.6; + } + + public static final double INTAKING_SPEED = 0.95; + + public static final int INTAKE_MOTOR_CHANNEL = 6; + public static final double INTAKE_AMP_THRESHOLD = 16; + + // 20ms per periodic cycle * number of periodic cycles / 1000 to get as milliseconds + public static final double INTAKE_CUBE_DELAY = (20 * 15) / 1000; + public static final double INTAKE_CONE_DELAY = (20 * 18) / 1000; + + public static final double HOLD_CONE_SPEED = 0.18; + public static final double HOLD_CUBE_SPEED = 0.15; } diff --git a/src/main/java/frc/robot/subsystems/intake/IntakeSubsystem.java b/src/main/java/frc/robot/subsystems/intake/IntakeSubsystem.java index 9b5a3df..05ef9f3 100644 --- a/src/main/java/frc/robot/subsystems/intake/IntakeSubsystem.java +++ b/src/main/java/frc/robot/subsystems/intake/IntakeSubsystem.java @@ -10,184 +10,190 @@ import frc.robot.subsystems.intake.enums.IntakeScoreType; /** - * This an example implementation of our - * intake subsystem from 2023. - * - * Each subsystem represents a different purpose of the robot - * such as arms, LEDs, drivetrains, shooter, etc. + * This an example implementation of our intake subsystem from 2023. + * + *

Each subsystem represents a different purpose of the robot such as arms, LEDs, drivetrains, + * shooter, etc. */ public class IntakeSubsystem extends SubsystemBase { - private VictorSP intakeMotor = new VictorSP(IntakeConstants.INTAKE_MOTOR_CHANNEL); - private PowerDistribution pdp; - - public IntakeSubsystem(PowerDistribution pdp) { - this.pdp = pdp; - } - - /** - * This method runs once every 20ms. - * - * This is useful for updating subsystem-specific - * state that you don't want to offload to a Command. - * - * Teams should try to be consistent within their - * own codebases about which responsibilities will - * be handled by Commands, and which will be handled here. - */ - @Override - public void periodic() { - SmartDashboard.putNumber("INTAKE CURRENT", this.pdp.getCurrent(IntakeConstants.INTAKE_MOTOR_CHANNEL)); - SmartDashboard.putNumber("INTAKE SPEED", this.intakeMotor.get()); - } - - /** - * Runs a command that stops the intake - * - * @return a command that stops the intake - */ - public Command stopIntakeCommand() { - return runOnce(() -> this.intakeMotor.set(0)) - .ignoringDisable(true); - } - - /** - * Runs a command to score a gamepiece. - * - * @param type the type of the score you want to make - * @param expectedPiece the type of gamepiece to expect when scoring - * @return a command that scores a gamepiece - */ - public Command intakeScoreCommand(IntakeScoreType type, IntakeGamepieces expectedPiece) { - return run(() -> { - - switch (type) { - case MID_CONE -> { - Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.MID_CONE), this) - .andThen(Commands.waitSeconds(0.5)) - .andThen(stopIntakeCommand()); - } - - case MID_CONE_TIPPED -> { - Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.MID_CONE_TIPPED), this) - .andThen(Commands.waitSeconds(0.5)) - .andThen(stopIntakeCommand()); - } - - case MID_CUBE -> { - Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.MID_CUBE), this) - .andThen(Commands.waitSeconds(0.5)) - .andThen(stopIntakeCommand()); - } - - case MID_CUBE_AUTO -> { - Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.MID_CUBE_AUTO), this) - .andThen(Commands.waitSeconds(0.5)) - .andThen(stopIntakeCommand()); - } - - case HIGH_CONE -> { - Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.HIGH_CONE), this) - .andThen(Commands.waitSeconds(0.5)) - .andThen(stopIntakeCommand()); - } - - case HIGH_CONE_AUTO -> { - Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.HIGH_CONE_AUTO), this) - .andThen(Commands.waitSeconds(0.5)) - .andThen(stopIntakeCommand()); - } - - case HIGH_CUBE -> { - Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.HIGH_CUBE), this) - .andThen(Commands.waitSeconds(0.5)) - .andThen(stopIntakeCommand()); - } - - case HIGH_CUBE_AUTO -> { - Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.HIGH_CUBE_AUTO), this) - .andThen(Commands.waitSeconds(0.5)) - .andThen(stopIntakeCommand()); - } - - case LOW -> { - if (expectedPiece.equals(IntakeGamepieces.CUBE)) { - Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.LOW_CUBE), this) - .andThen(Commands.waitSeconds(1)) - .andThen(stopIntakeCommand()); - } else { - Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.LOW_CONE), this) - .andThen(Commands.waitSeconds(1)) - .andThen(stopIntakeCommand()); - } - } - - case LOW_AUTO -> { - if (expectedPiece.equals(IntakeGamepieces.CUBE)) { - Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.LOW_CUBE_AUTO), this) - .andThen(Commands.waitSeconds(1)) - .andThen(stopIntakeCommand()); - } else { - Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.LOW_CONE_AUTO), this) - .andThen(Commands.waitSeconds(1)) - .andThen(stopIntakeCommand()); - } - } + private VictorSP intakeMotor = new VictorSP(IntakeConstants.INTAKE_MOTOR_CHANNEL); + private PowerDistribution pdp; + + public IntakeSubsystem(PowerDistribution pdp) { + this.pdp = pdp; + } + + /** + * This method runs once every 20ms. + * + *

This is useful for updating subsystem-specific state that you don't want to offload to a + * Command. + * + *

Teams should try to be consistent within their own codebases about which responsibilities + * will be handled by Commands, and which will be handled here. + */ + @Override + public void periodic() { + SmartDashboard.putNumber( + "INTAKE CURRENT", this.pdp.getCurrent(IntakeConstants.INTAKE_MOTOR_CHANNEL)); + SmartDashboard.putNumber("INTAKE SPEED", this.intakeMotor.get()); + } + + /** + * Runs a command that stops the intake + * + * @return a command that stops the intake + */ + public Command stopIntakeCommand() { + return runOnce(() -> this.intakeMotor.set(0)).ignoringDisable(true); + } + + /** + * Runs a command to score a gamepiece. + * + * @param type the type of the score you want to make + * @param expectedPiece the type of gamepiece to expect when scoring + * @return a command that scores a gamepiece + */ + public Command intakeScoreCommand(IntakeScoreType type, IntakeGamepieces expectedPiece) { + return run(() -> { + switch (type) { + case MID_CONE -> { + Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.MID_CONE), this) + .andThen(Commands.waitSeconds(0.5)) + .andThen(stopIntakeCommand()); } - }).andThen(stopIntakeCommand()); - } - - /** - * Runs a command that intakes and holds a gamepiece - * - * @param gamepiece the type of gamepiece to expect - * @return a command that forces the intake to hold the specified gamepiece - */ - public Command intakeHoldCommand(IntakeGamepieces gamepiece) { - return run(() -> { - this.intakeMotor.set(IntakeConstants.INTAKING_SPEED); - - // this will keep the motor running as long as the current is low enough - // this results in something similar to an if statement - Commands.waitUntil(() -> this.pdp - .getCurrent(IntakeConstants.INTAKE_MOTOR_CHANNEL) < IntakeConstants.INTAKE_AMP_THRESHOLD); - - if (gamepiece.equals(IntakeGamepieces.CUBE)) { - // wait a short amount of time so the gamepiece gets pulled in - Commands.waitSeconds(IntakeConstants.INTAKE_CUBE_DELAY); - this.intakeMotor.set(IntakeConstants.HOLD_CUBE_SPEED); + + case MID_CONE_TIPPED -> { + Commands.runOnce( + () -> outtakeCommand(IntakeConstants.OuttakeSpeeds.MID_CONE_TIPPED), this) + .andThen(Commands.waitSeconds(0.5)) + .andThen(stopIntakeCommand()); + } + + case MID_CUBE -> { + Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.MID_CUBE), this) + .andThen(Commands.waitSeconds(0.5)) + .andThen(stopIntakeCommand()); + } + + case MID_CUBE_AUTO -> { + Commands.runOnce( + () -> outtakeCommand(IntakeConstants.OuttakeSpeeds.MID_CUBE_AUTO), this) + .andThen(Commands.waitSeconds(0.5)) + .andThen(stopIntakeCommand()); + } + + case HIGH_CONE -> { + Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.HIGH_CONE), this) + .andThen(Commands.waitSeconds(0.5)) + .andThen(stopIntakeCommand()); + } + + case HIGH_CONE_AUTO -> { + Commands.runOnce( + () -> outtakeCommand(IntakeConstants.OuttakeSpeeds.HIGH_CONE_AUTO), this) + .andThen(Commands.waitSeconds(0.5)) + .andThen(stopIntakeCommand()); + } + + case HIGH_CUBE -> { + Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.HIGH_CUBE), this) + .andThen(Commands.waitSeconds(0.5)) + .andThen(stopIntakeCommand()); + } + + case HIGH_CUBE_AUTO -> { + Commands.runOnce( + () -> outtakeCommand(IntakeConstants.OuttakeSpeeds.HIGH_CUBE_AUTO), this) + .andThen(Commands.waitSeconds(0.5)) + .andThen(stopIntakeCommand()); + } + + case LOW -> { + if (expectedPiece.equals(IntakeGamepieces.CUBE)) { + Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.LOW_CUBE), this) + .andThen(Commands.waitSeconds(1)) + .andThen(stopIntakeCommand()); + } else { + Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.LOW_CONE), this) + .andThen(Commands.waitSeconds(1)) + .andThen(stopIntakeCommand()); + } } - if (gamepiece.equals(IntakeGamepieces.CONE)) { - // we have a cone, so run the motor at a higher speed - Commands.waitSeconds(IntakeConstants.INTAKE_CONE_DELAY); - this.intakeMotor.set(IntakeConstants.HOLD_CONE_SPEED); + + case LOW_AUTO -> { + if (expectedPiece.equals(IntakeGamepieces.CUBE)) { + Commands.runOnce( + () -> outtakeCommand(IntakeConstants.OuttakeSpeeds.LOW_CUBE_AUTO), this) + .andThen(Commands.waitSeconds(1)) + .andThen(stopIntakeCommand()); + } else { + Commands.runOnce( + () -> outtakeCommand(IntakeConstants.OuttakeSpeeds.LOW_CONE_AUTO), this) + .andThen(Commands.waitSeconds(1)) + .andThen(stopIntakeCommand()); + } } - }).finallyDo(() -> this.intakeMotor.set(0)) - .ignoringDisable(false); - } - - /** - * Runs a command that spits out its gamepiece - * - * @param speed speed to run the motor at, this is pre-inverted - * @return a command that forces the intake to spit out its gamepiece - */ - public Command outtakeCommand(double speed) { - return runOnce(() -> this.intakeMotor.set(-speed)); - } - - /** - * This method is similar to periodic, - * but only runs in simulation mode. - * - * Useful for updating subsystem-specific state - * that needs to be maintained for simulations, such as - * for updating edu.wpi.first.wpilibj.simulation classes - * and setting simulated sensor readings. - */ - @Override - public void simulationPeriodic() { - SmartDashboard.putNumber("INTAKE CURRENT", this.pdp.getCurrent(IntakeConstants.INTAKE_MOTOR_CHANNEL)); - SmartDashboard.putNumber("INTAKE SPEED", this.intakeMotor.get()); - } + } + }) + .andThen(stopIntakeCommand()); + } + + /** + * Runs a command that intakes and holds a gamepiece + * + * @param gamepiece the type of gamepiece to expect + * @return a command that forces the intake to hold the specified gamepiece + */ + public Command intakeHoldCommand(IntakeGamepieces gamepiece) { + return run(() -> { + this.intakeMotor.set(IntakeConstants.INTAKING_SPEED); + + // this will keep the motor running as long as the current is low enough + // this results in something similar to an if statement + Commands.waitUntil( + () -> + this.pdp.getCurrent(IntakeConstants.INTAKE_MOTOR_CHANNEL) + < IntakeConstants.INTAKE_AMP_THRESHOLD); + + if (gamepiece.equals(IntakeGamepieces.CUBE)) { + // wait a short amount of time so the gamepiece gets pulled in + Commands.waitSeconds(IntakeConstants.INTAKE_CUBE_DELAY); + this.intakeMotor.set(IntakeConstants.HOLD_CUBE_SPEED); + } + if (gamepiece.equals(IntakeGamepieces.CONE)) { + // we have a cone, so run the motor at a higher speed + Commands.waitSeconds(IntakeConstants.INTAKE_CONE_DELAY); + this.intakeMotor.set(IntakeConstants.HOLD_CONE_SPEED); + } + }) + .finallyDo(() -> this.intakeMotor.set(0)) + .ignoringDisable(false); + } + + /** + * Runs a command that spits out its gamepiece + * + * @param speed speed to run the motor at, this is pre-inverted + * @return a command that forces the intake to spit out its gamepiece + */ + public Command outtakeCommand(double speed) { + return runOnce(() -> this.intakeMotor.set(-speed)); + } + + /** + * This method is similar to periodic, but only runs in simulation mode. + * + *

Useful for updating subsystem-specific state that needs to be maintained for simulations, + * such as for updating edu.wpi.first.wpilibj.simulation classes and setting simulated sensor + * readings. + */ + @Override + public void simulationPeriodic() { + SmartDashboard.putNumber( + "INTAKE CURRENT", this.pdp.getCurrent(IntakeConstants.INTAKE_MOTOR_CHANNEL)); + SmartDashboard.putNumber("INTAKE SPEED", this.intakeMotor.get()); + } } diff --git a/src/main/java/frc/robot/subsystems/intake/enums/IntakeGamepieces.java b/src/main/java/frc/robot/subsystems/intake/enums/IntakeGamepieces.java index c5eb73b..3a87c37 100644 --- a/src/main/java/frc/robot/subsystems/intake/enums/IntakeGamepieces.java +++ b/src/main/java/frc/robot/subsystems/intake/enums/IntakeGamepieces.java @@ -1,6 +1,6 @@ package frc.robot.subsystems.intake.enums; public enum IntakeGamepieces { - CONE, - CUBE + CONE, + CUBE } diff --git a/src/main/java/frc/robot/subsystems/intake/enums/IntakeScoreType.java b/src/main/java/frc/robot/subsystems/intake/enums/IntakeScoreType.java index 12e6a64..f1fe54e 100644 --- a/src/main/java/frc/robot/subsystems/intake/enums/IntakeScoreType.java +++ b/src/main/java/frc/robot/subsystems/intake/enums/IntakeScoreType.java @@ -1,14 +1,14 @@ package frc.robot.subsystems.intake.enums; public enum IntakeScoreType { - HIGH_CONE, - HIGH_CONE_AUTO, - HIGH_CUBE, - HIGH_CUBE_AUTO, - MID_CONE, - MID_CONE_TIPPED, - MID_CUBE, - MID_CUBE_AUTO, - LOW, - LOW_AUTO + HIGH_CONE, + HIGH_CONE_AUTO, + HIGH_CUBE, + HIGH_CUBE_AUTO, + MID_CONE, + MID_CONE_TIPPED, + MID_CUBE, + MID_CUBE_AUTO, + LOW, + LOW_AUTO } diff --git a/src/main/java/frc/robot/subsystems/led/LEDSubsystem.java b/src/main/java/frc/robot/subsystems/led/LEDSubsystem.java index 7d16a49..e0242d8 100644 --- a/src/main/java/frc/robot/subsystems/led/LEDSubsystem.java +++ b/src/main/java/frc/robot/subsystems/led/LEDSubsystem.java @@ -12,9 +12,11 @@ public class LEDSubsystem extends SubsystemBase { public static List ledSegments = new ArrayList<>(); - public static AddressableLED leds = new AddressableLED(0); // The PWM port the LEDs are plugged into - public static AddressableLEDBuffer ledBuffer = new AddressableLEDBuffer( - (ledSegments.size() * LEDConstants.ledsPerSegment)); // The buffer that holds the LED data + public static AddressableLED leds = + new AddressableLED(0); // The PWM port the LEDs are plugged into + public static AddressableLEDBuffer ledBuffer = + new AddressableLEDBuffer( + (ledSegments.size() * LEDConstants.ledsPerSegment)); // The buffer that holds the LED data @Override public void periodic() { @@ -33,8 +35,7 @@ public void initialize() { ledSegments.add(LEDSegment.FrontRight); leds.setLength( - (ledSegments.size() - * LEDConstants.ledsPerSegment)); // Set the length of the LED strip + (ledSegments.size() * LEDConstants.ledsPerSegment)); // Set the length of the LED strip leds.start(); // Start the LED strip } @@ -42,9 +43,8 @@ public void initialize() { /** * Sets the mode for all segments available * - * @param ledMode The mode to set them all as. Please see the modes directory - * for all available - * modes + * @param ledMode The mode to set them all as. Please see the modes directory for all available + * modes */ public void setAllSegmentModesCommand(LEDMode ledMode) { // For every segment we can set the mode of, set the mode as the one provided From 6a93d2e7ad7917286e189782102a9489cc7bfef7 Mon Sep 17 00:00:00 2001 From: Ian Tapply Date: Wed, 6 Dec 2023 19:58:04 -0800 Subject: [PATCH 09/14] Create vision subsystem (#38) # Description Created a vision subsystem that can be used for the drive odometry, tracking our position on the field, or for tracking game objects. Fixes #36 ## Type of change Please delete options that are not relevant. - [x] New feature (non-breaking change which adds functionality) # Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation, if any - [x] My changes generate no new warnings - [x] I have performed tests that prove my fix is effective or that my feature works, if necessary - [x] New and existing unit tests pass locally with my changes --------- Co-authored-by: github-actions <> --- .../robot/subsystems/limelight/Limelight.java | 142 ++++++++++++++++++ .../limelight/LimelightConstants.java | 29 ++++ .../subsystems/limelight/LimelightIO.java | 52 +++++++ .../limelight/LimelightPoseAndTimestamp.java | 21 +++ .../limelight/LimelightPoseData.java | 116 ++++++++++++++ .../limelight/LimelightSubsystem.java | 101 +++++++++++++ .../limelight/enums/LimelightLEDMode.java | 32 ++++ .../limelight/enums/LimelightStream.java | 33 ++++ 8 files changed, 526 insertions(+) create mode 100644 src/main/java/frc/robot/subsystems/limelight/Limelight.java create mode 100644 src/main/java/frc/robot/subsystems/limelight/LimelightConstants.java create mode 100644 src/main/java/frc/robot/subsystems/limelight/LimelightIO.java create mode 100644 src/main/java/frc/robot/subsystems/limelight/LimelightPoseAndTimestamp.java create mode 100644 src/main/java/frc/robot/subsystems/limelight/LimelightPoseData.java create mode 100644 src/main/java/frc/robot/subsystems/limelight/LimelightSubsystem.java create mode 100644 src/main/java/frc/robot/subsystems/limelight/enums/LimelightLEDMode.java create mode 100644 src/main/java/frc/robot/subsystems/limelight/enums/LimelightStream.java diff --git a/src/main/java/frc/robot/subsystems/limelight/Limelight.java b/src/main/java/frc/robot/subsystems/limelight/Limelight.java new file mode 100644 index 0000000..25af1c6 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/limelight/Limelight.java @@ -0,0 +1,142 @@ +package frc.robot.subsystems.limelight; + +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.networktables.NetworkTable; +import edu.wpi.first.networktables.NetworkTableEntry; +import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.first.wpilibj.Timer; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import frc.robot.subsystems.limelight.enums.LimelightLEDMode; +import frc.robot.subsystems.limelight.enums.LimelightStream; + +public class Limelight implements LimelightIO { + private String limelightName; + private NetworkTable networkTable; + private LimelightPoseData limelightPoseData; + + /** + * Creates a new limelight + * + * @param limelightName The name of the limelight + */ + public Limelight(String limelightName) { + this.limelightName = limelightName; + + // Initializes and sets camera and pipeline of the limelight + this.networkTable = NetworkTableInstance.getDefault().getTable(this.limelightName); + this.networkTable.getEntry(LimelightConstants.TableConstants.CAMERA).setNumber(0); + this.networkTable.getEntry(LimelightConstants.TableConstants.PIPELINE).setNumber(0); + } + + @Override + public void updateData(LimelightIOData limelightIOData) { + NetworkTableEntry robotPoseEntry; + + // Creates a new limelight pose data instance if it doesn't exist + if (this.limelightPoseData == null) { + this.limelightPoseData = new LimelightPoseData(new double[7]); + } + + // Sets the pose of the limelight based on what alliance we are on + if (DriverStation.getAlliance().get() == DriverStation.Alliance.Blue) { + robotPoseEntry = this.networkTable.getEntry(LimelightConstants.TableConstants.BOT_POSE_BLUE); + } else if (DriverStation.getAlliance().get() == DriverStation.Alliance.Red) { + robotPoseEntry = this.networkTable.getEntry(LimelightConstants.TableConstants.BOT_POSE_RED); + } else { + robotPoseEntry = this.networkTable.getEntry(LimelightConstants.TableConstants.BOT_POSE); + } + + // Update the limelight pose data + this.limelightPoseData.update(robotPoseEntry.getDoubleArray(new double[7])); + + // Create a 3d pose from data from the limelight + Pose3d limelightPose = limelightPoseData.toPose3d(); + + // Set if the limelight is locked onto a target + if (this.networkTable.getEntry(LimelightConstants.TableConstants.VALID_TARGET).getDouble(0) + == 1) { + limelightIOData.lockedOnTarget = true; + } else { + limelightIOData.lockedOnTarget = false; + } + + if (limelightIOData.lockedOnTarget) { + // Set the time that the limelight was updated + limelightIOData.limelightLastUpdated = + Timer.getFPGATimestamp() - limelightPoseData.getTotalLatency(); + + limelightIOData.isNewPose = true; + + // Set the target x and y + limelightIOData.targetX = + this.networkTable.getEntry(LimelightConstants.TableConstants.TARGET_X).getDouble(0); + limelightIOData.targetY = + this.networkTable.getEntry(LimelightConstants.TableConstants.TARGET_Y).getDouble(0); + + // Set the pose of the limelight (x, y, rotation) + Pose2d pose2d = limelightPose.toPose2d(); + limelightIOData.limelightX = pose2d.getX(); + limelightIOData.limelightY = pose2d.getY(); + limelightIOData.limelightRotation = pose2d.getRotation().getRadians(); + limelightIOData.limelightPose = pose2d; + + } else { + limelightIOData.isNewPose = false; + + // Set the target x and y to 0 because we don't have a target + limelightIOData.targetX = 0; + limelightIOData.targetY = 0; + } + } + + /** Gets the name of a limelight that's registered into the network table */ + @Override + public String getName() { + return this.limelightName; + } + + /** + * Sets the led mode of the limelight + * + * @param limelightLEDMode The led mode to set the limelight to (see LimelightLEDMode.java for + * options) + */ + public void setLEDMode(LimelightLEDMode limelightLEDMode) { + this.networkTable + .getEntry(LimelightConstants.TableConstants.LED_MODE) + .setNumber(limelightLEDMode.getNetworkTableValue()); + } + + /** + * Sets the streaming mode of the limelight + * + * @param stream The streaming mode to set the limelight to as a limelight stream type + */ + public void setStream(LimelightStream limelightStream) { + this.networkTable + .getEntry(LimelightConstants.TableConstants.STREAM) + .setNumber(limelightStream.getNetworkTableValue()); + } + + /** + * Sets the pipeline of the limelight based on our presets for it + * + * @param pipeline The pipeline to set the limelight to as an integer + */ + public void setPipeline(int pipeline) { + this.networkTable.getEntry(LimelightConstants.TableConstants.PIPELINE).setNumber(pipeline); + } + + /** + * Displays the pose values of the limelight on the smart dashboard + * + * @param pose The pose to retrieve and display the values from + */ + public void displayLimelightPoseValues(Pose2d pose) { + SmartDashboard.putNumber(this.limelightName + " x", pose.getX()); + SmartDashboard.putNumber(this.limelightName + " y", pose.getY()); + SmartDashboard.putNumber(this.limelightName + " angleDegrees", pose.getRotation().getDegrees()); + } +} diff --git a/src/main/java/frc/robot/subsystems/limelight/LimelightConstants.java b/src/main/java/frc/robot/subsystems/limelight/LimelightConstants.java new file mode 100644 index 0000000..96c7797 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/limelight/LimelightConstants.java @@ -0,0 +1,29 @@ +package frc.robot.subsystems.limelight; + +import edu.wpi.first.math.util.Units; + +public class LimelightConstants { + public static final double LOWEST_DISTANCE = Units.feetToMeters(10.0); + + static class TableConstants { + /** + * All tables for limelights can be found here + * https://docs.limelightvision.io/docs/docs-limelight/apis/complete-networktables-api + */ + public static final String CAMERA = "camera"; + + public static final String PIPELINE = "pipeline"; + public static final String LED_MODE = "ledMode"; + public static final String STREAM = "stream"; + + // Driverstation/botposes + public static final String BOT_POSE_BLUE = "botpose_wpiblue"; + public static final String BOT_POSE_RED = "botpose_wpired"; + public static final String BOT_POSE = "botpose"; + + // Targeting related + public static final String VALID_TARGET = "tv"; + public static final String TARGET_X = "tx"; + public static final String TARGET_Y = "ty"; + } +} diff --git a/src/main/java/frc/robot/subsystems/limelight/LimelightIO.java b/src/main/java/frc/robot/subsystems/limelight/LimelightIO.java new file mode 100644 index 0000000..903f4bb --- /dev/null +++ b/src/main/java/frc/robot/subsystems/limelight/LimelightIO.java @@ -0,0 +1,52 @@ +package frc.robot.subsystems.limelight; + +import edu.wpi.first.math.geometry.Pose2d; + +public interface LimelightIO { + + /** Data that the limelight will collect and track */ + class LimelightIOData { + public double limelightX; + public double limelightY; + public double limelightRotation; + + public double targetX; + public double targetY; + + public boolean isNewPose; + public Pose2d limelightPose; + public double limelightLastUpdated; + + public double maxDistance; + public double minDistance; + + public boolean lockedOnTarget = false; + public int singleIDUsed; + + public double translationToTargetX; + public double translationToTargetY; + } + + /** + * Updates the data of the limelight + * + * @param data + */ + default void updateData(LimelightIOData data) {} + + /** + * Gets, or sets the name of the limelight + * + * @return + */ + default String getName() { + return ""; + } + + /** + * Sets the refrence pose of the limelight + * + * @param pose + */ + default void setRefrencePose(Pose2d pose) {} +} diff --git a/src/main/java/frc/robot/subsystems/limelight/LimelightPoseAndTimestamp.java b/src/main/java/frc/robot/subsystems/limelight/LimelightPoseAndTimestamp.java new file mode 100644 index 0000000..3b22ad8 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/limelight/LimelightPoseAndTimestamp.java @@ -0,0 +1,21 @@ +package frc.robot.subsystems.limelight; + +import edu.wpi.first.math.geometry.Pose2d; + +public class LimelightPoseAndTimestamp { + Pose2d limelightPose; + double lastUpdatedTimestamp; + + public LimelightPoseAndTimestamp(Pose2d limelightPose, double lastUpdatedTimestamp) { + this.limelightPose = limelightPose; + this.lastUpdatedTimestamp = lastUpdatedTimestamp; + } + + public Pose2d getLimelightPose() { + return this.limelightPose; + } + + public double getLastUpdatedTimestamp() { + return this.lastUpdatedTimestamp; + } +} diff --git a/src/main/java/frc/robot/subsystems/limelight/LimelightPoseData.java b/src/main/java/frc/robot/subsystems/limelight/LimelightPoseData.java new file mode 100644 index 0000000..29d2cc3 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/limelight/LimelightPoseData.java @@ -0,0 +1,116 @@ +package frc.robot.subsystems.limelight; + +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Rotation3d; + +public class LimelightPoseData { + private double x; + private double y; + private double z; + private double roll; + private double pitch; + private double yaw; + private double totalLatency; + + public LimelightPoseData(double[] data) { + if (data.length != 7) { + throw new IllegalArgumentException("Data array must have 7 elements"); + } + + this.x = data[0]; + this.y = data[1]; + this.z = data[2]; + this.roll = data[3]; + this.pitch = data[4]; + this.yaw = data[5]; + this.totalLatency = data[6]; + } + + /** + * Updates the pose data without creating a new instance of limelight pose data + * + * @param data The data to update the pose data with + */ + public void update(double[] data) { + if (data.length != 7) { + throw new IllegalArgumentException("Data array must have 7 elements"); + } + + this.x = data[0]; + this.y = data[1]; + this.z = data[2]; + this.roll = data[3]; + this.pitch = data[4]; + this.yaw = data[5]; + this.totalLatency = data[6]; + } + + /** + * Gets the x position of the limelight + * + * @return The x position of the limelight + */ + public double getX() { + return this.x; + } + + /** + * Gets the y position of the limelight + * + * @return The y position of the limelight + */ + public double getY() { + return this.y; + } + + /** + * Gets the z position of the limelight + * + * @return The z position of the limelight + */ + public double getZ() { + return this.z; + } + + /** + * Gets a 3d rotation position of the limelight + * + * @return The 3d rotation position of the limelight + */ + public Rotation3d getRotation() { + return new Rotation3d( + Math.toRadians(this.roll), Math.toRadians(this.pitch), Math.toRadians(this.yaw)); + } + + /** + * Gets the raw latency of the limelight straight from the network table + * + * @return The raw latency of the limelight straight from the network table + */ + public double getRawTotalLatency() { + return this.totalLatency; + } + + /** + * Gets the total latency of the limelight in seconds + * + * @return The total latency of the limelight in seconds + */ + public double getTotalLatency() { + return this.totalLatency / 1000; + } + + /** + * Converts the limelight pose data to a 3d pose + * + * @return A 3d pose of the limelight pose data + */ + public Pose3d toPose3d() { + return new Pose3d( + this.x, + this.y, + this.z, + new Rotation3d( + Math.toRadians(this.roll), Math.toRadians(this.pitch), Math.toRadians(this.yaw))); + } +} diff --git a/src/main/java/frc/robot/subsystems/limelight/LimelightSubsystem.java b/src/main/java/frc/robot/subsystems/limelight/LimelightSubsystem.java new file mode 100644 index 0000000..82a2696 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/limelight/LimelightSubsystem.java @@ -0,0 +1,101 @@ +package frc.robot.subsystems.limelight; + +import java.util.ArrayList; +import java.util.List; + +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.first.wpilibj2.command.SubsystemBase; +import frc.robot.subsystems.limelight.LimelightIO.LimelightIOData; + +public class LimelightSubsystem extends SubsystemBase { + private final Limelight[] limelights; + private final LimelightIOData[] limelightData; + + private final List results = new ArrayList<>(); + + private int acceptableTagID; + private boolean useSingleTag = false; + + /** + * Initializes cameras and input loggers + * + * @param cameras Array of cameras being used + */ + public LimelightSubsystem(Limelight[] limelights) { + this.limelights = limelights; + this.limelightData = new LimelightIOData[this.limelights.length]; + + for (int i = 0; i < this.limelights.length; i++) { + this.limelightData[i] = new LimelightIOData(); + } + } + + @Override + public void periodic() { + // Clear results from last periodic + this.results.clear(); + + for (int i = 0; i < this.limelightData.length; i++) { + // update and process new inputs + this.limelights[i].updateData(limelightData[i]); + + if (this.limelightData[i].lockedOnTarget + && this.limelightData[i].isNewPose + && !DriverStation.isAutonomous() + && this.limelightData[i].maxDistance < LimelightConstants.LOWEST_DISTANCE) { + if (this.useSingleTag) { + if (this.limelightData[i].singleIDUsed == this.acceptableTagID) { + this.processLimelight(i); + } + } else { + this.processLimelight(i); + } + } + } + } + + /** + * Processes the limelight data and adds the pose to the list of poses + * + * @param limelightNumber The numerical ID of the limelight + */ + public void processLimelight(int limelightNumber) { + // Create a new pose based off the new limelight data + Pose2d currentPose = + new Pose2d( + this.limelightData[limelightNumber].limelightX, + this.limelightData[limelightNumber].limelightY, + new Rotation2d(this.limelightData[limelightNumber].limelightRotation)); + + // Add the new pose to the list of poses + this.results.add( + new LimelightPoseAndTimestamp( + currentPose, limelightData[limelightNumber].limelightLastUpdated)); + } + + /** Returns the last recorded pose */ + public List getLimelightOdometry() { + return this.results; + } + + public void setUseSingleTag(boolean useSingleTag) { + this.setUseSingleTag(useSingleTag, 0); + } + + public void setUseSingleTag(boolean useSingleTag, int acceptableTagID) { + this.useSingleTag = useSingleTag; + this.acceptableTagID = acceptableTagID; + } + + public void setReferencePose(Pose2d pose) { + for (Limelight limelight : this.limelights) { + limelight.setRefrencePose(pose); + } + } + + public double getMinDistance(int camera) { + return this.limelightData[camera].minDistance; + } +} diff --git a/src/main/java/frc/robot/subsystems/limelight/enums/LimelightLEDMode.java b/src/main/java/frc/robot/subsystems/limelight/enums/LimelightLEDMode.java new file mode 100644 index 0000000..db542a1 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/limelight/enums/LimelightLEDMode.java @@ -0,0 +1,32 @@ +package frc.robot.subsystems.limelight.enums; + +import java.util.HashMap; +import java.util.Map; + +public enum LimelightLEDMode { + PIPELINE_DEFAULT(0), + OFF(1), + BLINK(2), + ON(3); + + private int networkTableValue; + private static Map ledModeMap = new HashMap<>(); + + private LimelightLEDMode(int networkTableValue) { + this.networkTableValue = networkTableValue; + } + + static { + for (LimelightLEDMode limelightLEDMode : LimelightLEDMode.values()) { + ledModeMap.put(limelightLEDMode.networkTableValue, limelightLEDMode); + } + } + + public static LimelightLEDMode valueOf(int limelightNetworkTableValue) { + return ledModeMap.get(limelightNetworkTableValue); + } + + public int getNetworkTableValue() { + return this.networkTableValue; + } +} diff --git a/src/main/java/frc/robot/subsystems/limelight/enums/LimelightStream.java b/src/main/java/frc/robot/subsystems/limelight/enums/LimelightStream.java new file mode 100644 index 0000000..e21a936 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/limelight/enums/LimelightStream.java @@ -0,0 +1,33 @@ +package frc.robot.subsystems.limelight.enums; + +import java.util.HashMap; +import java.util.Map; + +public enum LimelightStream { + STANDARD(0), // side-by-side streams if a webcam is attached to Limelight + PIP_MAIN(1), // secondary camera stream in the lower-right corner of the + // primary camera stream + PIP_SECONDARY(2); // primary camera stream in the lower-right corner of the + // secondary camera stream + + private int networkTableValue; + private static Map streamMap = new HashMap<>(); + + private LimelightStream(int networkTableValue) { + this.networkTableValue = networkTableValue; + } + + static { + for (LimelightStream limelightStream : LimelightStream.values()) { + streamMap.put(limelightStream.networkTableValue, limelightStream); + } + } + + public static LimelightStream valueOf(int limelightNetworkTableValue) { + return streamMap.get(limelightNetworkTableValue); + } + + public int getNetworkTableValue() { + return this.networkTableValue; + } +} From d44bb89f935cbca2d535885d2e8df34e0aec5e93 Mon Sep 17 00:00:00 2001 From: Ian Tapply Date: Thu, 7 Dec 2023 05:45:39 -0800 Subject: [PATCH 10/14] Update 2024-beta with main (#47) Updated the `2024-beta` branch to be synced with the `main` branch --------- Co-authored-by: Cole MacPhail --- .github/workflows/pr-guidelines.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/pr-guidelines.yml diff --git a/.github/workflows/pr-guidelines.yml b/.github/workflows/pr-guidelines.yml new file mode 100644 index 0000000..6ba628b --- /dev/null +++ b/.github/workflows/pr-guidelines.yml @@ -0,0 +1,22 @@ +name: 'PR Guidelines' + +on: + pull_request: + types: [ opened ] + branches: + - main + - 2024-beta + +jobs: + build: + name: Send PR guidelines + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Comment a pull_request + uses: mb2dev/github-action-comment-pull-request@1.0.0 + with: + message: "Thank you for making a pull request! Please make sure to follow the [pull request guidelines](https://dev.to/karaluton/a-guide-to-perfecting-pull-requests-2b66) for this PR. This applies to your commit messages, pull request code, and more!" + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From b269ec60c2b538bd67b8291a8ed55bcc5c75262c Mon Sep 17 00:00:00 2001 From: Ian Tapply Date: Fri, 5 Jan 2024 20:07:45 -0500 Subject: [PATCH 11/14] Port drive subsystem to command robot (#58) # Description - Updated the drive-related dependencies to the latest beta - Updated drive telemetry to the new CTRE suggested way - Created a new drive subsystem and deleted the old one Fixes #8 ## Type of change - [x] New feature (non-breaking change which adds functionality) # How Has This Been Tested? N/A # Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation, if any - [ ] My changes generate no new warnings - [ ] I have performed tests that prove my fix is effective or that my feature works, if necessary - [ ] New and existing unit tests pass locally with my changes --------- Co-authored-by: github-actions <> --- src/main/java/frc/robot/Robot.java | 4 - src/main/java/frc/robot/RobotContainer.java | 81 ++++------ src/main/java/frc/robot/Telemetry.java | 49 +++--- .../frc/robot/generated/TunerConstants.java | 133 ---------------- .../subsystems/drive/DriveConstants.java | 142 ++++++++++++++++++ .../subsystems/drive/DriveSlotGains.java | 13 ++ .../drive/DriveSubsystem.java} | 122 +++++++++++---- .../drive/swerve/SwerveModuleLocation.java | 54 +++++++ .../drive/swerve/SwerveModules.java | 53 +++++++ vendordeps/PathplannerLib.json | 6 +- vendordeps/Phoenix6.json | 48 +++--- 11 files changed, 431 insertions(+), 274 deletions(-) delete mode 100644 src/main/java/frc/robot/generated/TunerConstants.java create mode 100644 src/main/java/frc/robot/subsystems/drive/DriveConstants.java create mode 100644 src/main/java/frc/robot/subsystems/drive/DriveSlotGains.java rename src/main/java/frc/robot/{CommandSwerveDrivetrain.java => subsystems/drive/DriveSubsystem.java} (51%) create mode 100644 src/main/java/frc/robot/subsystems/drive/swerve/SwerveModuleLocation.java create mode 100644 src/main/java/frc/robot/subsystems/drive/swerve/SwerveModules.java diff --git a/src/main/java/frc/robot/Robot.java b/src/main/java/frc/robot/Robot.java index 8bafc7b..f9fc442 100644 --- a/src/main/java/frc/robot/Robot.java +++ b/src/main/java/frc/robot/Robot.java @@ -4,8 +4,6 @@ package frc.robot; -import com.ctre.phoenix6.SignalLogger; - import edu.wpi.first.math.geometry.Pose2d; import edu.wpi.first.wpilibj.TimedRobot; import edu.wpi.first.wpilibj.Timer; @@ -24,8 +22,6 @@ public void robotInit() { m_robotContainer = new RobotContainer(); m_robotContainer.drivetrain.getDaqThread().setThreadPriority(99); - - SignalLogger.start(); } @Override diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java index 6bf31d9..d786c88 100644 --- a/src/main/java/frc/robot/RobotContainer.java +++ b/src/main/java/frc/robot/RobotContainer.java @@ -4,79 +4,59 @@ package frc.robot; +import com.ctre.phoenix6.mechanisms.swerve.SwerveModule.DriveRequestType; import com.ctre.phoenix6.mechanisms.swerve.SwerveRequest; -import edu.wpi.first.math.geometry.Pose2d; -import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.InstantCommand; -import frc.robot.generated.TunerConstants; +import frc.robot.subsystems.drive.DriveConstants; +import frc.robot.subsystems.drive.DriveSubsystem; import frc.robot.subsystems.led.LEDSubsystem; public class RobotContainer { - final double MaxSpeed = 6; // 6 meters per second desired top speed - final double MaxAngularRate = Math.PI; // Half a rotation per second max angular velocity + private class Controller { + public static final CommandController driver = new CommandController(0); + public static final CommandController operator = new CommandController(1); + } - /* Setting up bindings for necessary control of the swerve drive platform */ - CommandController driver = new CommandController(0); // Driver Controller - CommandController operator = new CommandController(1); // Operator Controller - CommandSwerveDrivetrain drivetrain = TunerConstants.DriveTrain; // My drivetrain private final LEDSubsystem ledSubsystem; - SwerveRequest.FieldCentric drive = + + // Set up the base for the drive and drivetrain + final DriveSubsystem drivetrain = DriveConstants.DriveTrain; + private final SwerveRequest.FieldCentric drive = new SwerveRequest.FieldCentric() - .withIsOpenLoop(true) - .withDeadband(MaxSpeed * 0.1) - .withRotationalDeadband(MaxAngularRate * 0.1); // I want field-centric - // driving in open loop - SwerveRequest.SwerveDriveBrake brake = new SwerveRequest.SwerveDriveBrake(); - SwerveRequest.RobotCentric forwardStraight = - new SwerveRequest.RobotCentric().withIsOpenLoop(true); - SwerveRequest.PointWheelsAt point = new SwerveRequest.PointWheelsAt(); + .withDeadband(DriveConstants.kMaxAngularRate * 0.1) + .withRotationalDeadband(DriveConstants.kMaxAngularRate * 0.1) + .withDriveRequestType(DriveRequestType.OpenLoopVoltage); /* Path follower */ - // Command runAuto = drivetrain.getAutoPath("Tests"); - - Telemetry logger = new Telemetry(MaxSpeed); + private Command runAuto = drivetrain.getAutoPath("Tests"); - Pose2d odomStart = new Pose2d(0, 0, new Rotation2d(0, 0)); + private final Telemetry logger = new Telemetry(DriveConstants.kMaxSpeed); private void configureBindings() { ledSubsystem.setDefaultCommand(new InstantCommand(() -> ledSubsystem.periodic(), ledSubsystem)); + drivetrain.registerTelemetry(logger::telemeterize); - drivetrain.setDefaultCommand( // Drivetrain will execute this command periodically + drivetrain.setDefaultCommand( drivetrain .applyRequest( () -> drive - .withVelocityX(-driver.getLeftY() * MaxSpeed) // Drive forward with + .withVelocityX( + -Controller.driver.getLeftY() + * DriveConstants.kMaxSpeed) // Drive forward with // negative Y (forward) .withVelocityY( - -driver.getLeftX() * MaxSpeed) // Drive left with negative X (left) + -Controller.driver.getLeftX() + * DriveConstants.kMaxSpeed) // Drive left with negative X (left) .withRotationalRate( - -driver.getRightX() - * MaxAngularRate) // Drive counterclockwise with negative X (left) + -Controller.driver.getRightX() + * DriveConstants + .kMaxAngularRate) // Drive counterclockwise with negative X + // (left) ) .ignoringDisable(true)); - - driver.greenButton().whileTrue(drivetrain.applyRequest(() -> brake)); - driver - .yellowButton() - .whileTrue( - drivetrain.applyRequest( - () -> - point.withModuleDirection( - new Rotation2d(-driver.getLeftY(), -driver.getLeftX())))); - - // if (Utils.isSimulation()) { - // drivetrain.seedFieldRelative(new Pose2d(new Translation2d(), Rotation2d.fromDegrees(90))); - // } - drivetrain.registerTelemetry(logger::telemeterize); - driver.POVUp() - .whileTrue( - drivetrain.applyRequest(() -> forwardStraight.withVelocityX(0.5).withVelocityY(0))); - driver.POVDown() - .whileTrue( - drivetrain.applyRequest(() -> forwardStraight.withVelocityX(-0.5).withVelocityY(0))); } public RobotContainer() { @@ -85,11 +65,6 @@ public RobotContainer() { } public Command getAutonomousCommand() { - /* First put the drivetrain into auto run mode, then run the auto */ - return new InstantCommand(() -> {}); - } - - public boolean seedPoseButtonDown() { - return driver.leftBumper().getAsBoolean(); + return runAuto; } } diff --git a/src/main/java/frc/robot/Telemetry.java b/src/main/java/frc/robot/Telemetry.java index 16e9089..bb0861a 100644 --- a/src/main/java/frc/robot/Telemetry.java +++ b/src/main/java/frc/robot/Telemetry.java @@ -1,5 +1,6 @@ package frc.robot; +import com.ctre.phoenix6.SignalLogger; import com.ctre.phoenix6.Utils; import com.ctre.phoenix6.mechanisms.swerve.SwerveDrivetrain.SwerveDriveState; @@ -10,8 +11,6 @@ import edu.wpi.first.networktables.NetworkTable; import edu.wpi.first.networktables.NetworkTableInstance; import edu.wpi.first.networktables.StringPublisher; -import edu.wpi.first.wpilibj.DataLogManager; -import edu.wpi.first.wpilibj.Timer; import edu.wpi.first.wpilibj.smartdashboard.Mechanism2d; import edu.wpi.first.wpilibj.smartdashboard.MechanismLigament2d; import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; @@ -20,8 +19,6 @@ public class Telemetry { private final double MaxSpeed; - private int logEntry; - private int odomEntry; /** * Construct a telemetry object, with the specified max speed of the robot @@ -30,36 +27,36 @@ public class Telemetry { */ public Telemetry(double maxSpeed) { MaxSpeed = maxSpeed; - logEntry = DataLogManager.getLog().start("odometry", "double[]"); - odomEntry = DataLogManager.getLog().start("odom period", "double"); + SignalLogger.start(); } /* What to publish over networktables for telemetry */ - NetworkTableInstance inst = NetworkTableInstance.getDefault(); + private final NetworkTableInstance inst = NetworkTableInstance.getDefault(); /* Robot pose for field positioning */ - NetworkTable table = inst.getTable("Pose"); - DoubleArrayPublisher fieldPub = table.getDoubleArrayTopic("robotPose").publish(); - StringPublisher fieldTypePub = table.getStringTopic(".type").publish(); + private final NetworkTable table = inst.getTable("Pose"); + private final DoubleArrayPublisher fieldPub = table.getDoubleArrayTopic("robotPose").publish(); + private final StringPublisher fieldTypePub = table.getStringTopic(".type").publish(); /* Robot speeds for general checking */ - NetworkTable driveStats = inst.getTable("Drive"); - DoublePublisher velocityX = driveStats.getDoubleTopic("Velocity X").publish(); - DoublePublisher velocityY = driveStats.getDoubleTopic("Velocity Y").publish(); - DoublePublisher speed = driveStats.getDoubleTopic("Speed").publish(); - DoublePublisher odomPeriod = driveStats.getDoubleTopic("Odometry Period").publish(); + private final NetworkTable driveStats = inst.getTable("Drive"); + private final DoublePublisher velocityX = driveStats.getDoubleTopic("Velocity X").publish(); + private final DoublePublisher velocityY = driveStats.getDoubleTopic("Velocity Y").publish(); + private final DoublePublisher speed = driveStats.getDoubleTopic("Speed").publish(); + private final DoublePublisher odomFreq = + driveStats.getDoubleTopic("Odometry Frequency").publish(); /* Keep a reference of the last pose to calculate the speeds */ - Pose2d m_lastPose = new Pose2d(); - double lastTime = Utils.getCurrentTimeSeconds(); + private Pose2d m_lastPose = new Pose2d(); + private double lastTime = Utils.getCurrentTimeSeconds(); /* Mechanisms to represent the swerve module states */ - Mechanism2d[] m_moduleMechanisms = + private final Mechanism2d[] m_moduleMechanisms = new Mechanism2d[] { new Mechanism2d(1, 1), new Mechanism2d(1, 1), new Mechanism2d(1, 1), new Mechanism2d(1, 1), }; /* A direction and length changing ligament for speed representation */ - MechanismLigament2d[] m_moduleSpeeds = + private final MechanismLigament2d[] m_moduleSpeeds = new MechanismLigament2d[] { m_moduleMechanisms[0] .getRoot("RootSpeed", 0.5, 0.5) @@ -75,7 +72,7 @@ public Telemetry(double maxSpeed) { .append(new MechanismLigament2d("Speed", 0.5, 0)), }; /* A direction changing and length constant ligament for module direction */ - MechanismLigament2d[] m_moduleDirections = + private final MechanismLigament2d[] m_moduleDirections = new MechanismLigament2d[] { m_moduleMechanisms[0] .getRoot("RootDirection", 0.5, 0.5) @@ -110,7 +107,7 @@ public void telemeterize(SwerveDriveState state) { speed.set(velocities.getNorm()); velocityX.set(velocities.getX()); velocityY.set(velocities.getY()); - odomPeriod.set(1.0 / state.OdometryPeriod); + odomFreq.set(1.0 / state.OdometryPeriod); /* Telemeterize the module's states */ for (int i = 0; i < 4; ++i) { @@ -121,12 +118,8 @@ public void telemeterize(SwerveDriveState state) { SmartDashboard.putData("Module " + i, m_moduleMechanisms[i]); } - DataLogManager.getLog() - .appendDoubleArray( - logEntry, - new double[] {pose.getX(), pose.getY(), pose.getRotation().getDegrees()}, - (long) (Timer.getFPGATimestamp() * 1000000)); - DataLogManager.getLog() - .appendDouble(odomEntry, state.OdometryPeriod, (long) (Timer.getFPGATimestamp() * 1000000)); + SignalLogger.writeDoubleArray( + "odometry", new double[] {pose.getX(), pose.getY(), pose.getRotation().getDegrees()}); + SignalLogger.writeDouble("odom period", state.OdometryPeriod, "seconds"); } } diff --git a/src/main/java/frc/robot/generated/TunerConstants.java b/src/main/java/frc/robot/generated/TunerConstants.java deleted file mode 100644 index a66c7d8..0000000 --- a/src/main/java/frc/robot/generated/TunerConstants.java +++ /dev/null @@ -1,133 +0,0 @@ -package frc.robot.generated; - -import com.ctre.phoenix6.configs.Slot0Configs; -import com.ctre.phoenix6.mechanisms.swerve.SwerveModuleConstantsFactory; -import com.ctre.phoenix6.mechanisms.swerve.SwerveModuleConstants.SwerveModuleSteerFeedbackType; -import com.ctre.phoenix6.mechanisms.swerve.SwerveDrivetrainConstants; -import com.ctre.phoenix6.mechanisms.swerve.SwerveModuleConstants; - -import edu.wpi.first.math.util.Units; -import frc.robot.CommandSwerveDrivetrain; - -public class TunerConstants { - static class CustomSlotGains extends Slot0Configs { - public CustomSlotGains(double kP, double kI, double kD, double kV, double kS) { - this.kP = kP; - this.kI = kI; - this.kD = kD; - this.kV = kV; - this.kS = kS; - } - } - - private static final CustomSlotGains steerGains = new CustomSlotGains(100, 0, 0.05, 0, 0); - private static final CustomSlotGains driveGains = new CustomSlotGains(3, 0, 0, 0, 0); - - private static final double kCoupleRatio = 3.5; - - private static final double kDriveGearRatio = 7.363636364; - private static final double kSteerGearRatio = 15.42857143; - private static final double kWheelRadiusInches = - 2.167; // Estimated at first, then fudge-factored to make odom match record - private static final int kPigeonId = 1; - private static final boolean kSteerMotorReversed = true; - private static final String kCANbusName = "rio"; - private static final boolean kInvertLeftSide = false; - private static final boolean kInvertRightSide = true; - - private static double kSteerInertia = 0.00001; - private static double kDriveInertia = 0.001; - - private static final SwerveDrivetrainConstants DrivetrainConstants = - new SwerveDrivetrainConstants() - .withPigeon2Id(kPigeonId) - .withCANbusName(kCANbusName) - .withSupportsPro(true); - - private static final SwerveModuleConstantsFactory ConstantCreator = - new SwerveModuleConstantsFactory() - .withDriveMotorGearRatio(kDriveGearRatio) - .withSteerMotorGearRatio(kSteerGearRatio) - .withWheelRadius(kWheelRadiusInches) - .withSlipCurrent(30) - .withSteerMotorGains(steerGains) - .withDriveMotorGains(driveGains) - .withSpeedAt12VoltsMps( - 6) // Theoretical free speed is 10 meters per second at 12v applied output - .withSteerInertia(kSteerInertia) - .withDriveInertia(kDriveInertia) - .withFeedbackSource(SwerveModuleSteerFeedbackType.FusedCANcoder) - .withCouplingGearRatio( - kCoupleRatio) // Every 1 rotation of the azimuth results in couple ratio drive turns - .withSteerMotorInverted(kSteerMotorReversed); - - private static final int kFrontLeftDriveMotorId = 5; - private static final int kFrontLeftSteerMotorId = 4; - private static final int kFrontLeftEncoderId = 2; - private static final double kFrontLeftEncoderOffset = -0.83544921875; - - private static final double kFrontLeftXPosInches = 10.5; - private static final double kFrontLeftYPosInches = 10.5; - private static final int kFrontRightDriveMotorId = 7; - private static final int kFrontRightSteerMotorId = 6; - private static final int kFrontRightEncoderId = 3; - private static final double kFrontRightEncoderOffset = -0.15234375; - - private static final double kFrontRightXPosInches = 10.5; - private static final double kFrontRightYPosInches = -10.5; - private static final int kBackLeftDriveMotorId = 1; - private static final int kBackLeftSteerMotorId = 0; - private static final int kBackLeftEncoderId = 0; - private static final double kBackLeftEncoderOffset = -0.4794921875; - - private static final double kBackLeftXPosInches = -10.5; - private static final double kBackLeftYPosInches = 10.5; - private static final int kBackRightDriveMotorId = 3; - private static final int kBackRightSteerMotorId = 2; - private static final int kBackRightEncoderId = 1; - private static final double kBackRightEncoderOffset = -0.84130859375; - - private static final double kBackRightXPosInches = -10.5; - private static final double kBackRightYPosInches = -10.5; - - private static final SwerveModuleConstants FrontLeft = - ConstantCreator.createModuleConstants( - kFrontLeftSteerMotorId, - kFrontLeftDriveMotorId, - kFrontLeftEncoderId, - kFrontLeftEncoderOffset, - Units.inchesToMeters(kFrontLeftXPosInches), - Units.inchesToMeters(kFrontLeftYPosInches), - kInvertLeftSide); - private static final SwerveModuleConstants FrontRight = - ConstantCreator.createModuleConstants( - kFrontRightSteerMotorId, - kFrontRightDriveMotorId, - kFrontRightEncoderId, - kFrontRightEncoderOffset, - Units.inchesToMeters(kFrontRightXPosInches), - Units.inchesToMeters(kFrontRightYPosInches), - kInvertRightSide); - private static final SwerveModuleConstants BackLeft = - ConstantCreator.createModuleConstants( - kBackLeftSteerMotorId, - kBackLeftDriveMotorId, - kBackLeftEncoderId, - kBackLeftEncoderOffset, - Units.inchesToMeters(kBackLeftXPosInches), - Units.inchesToMeters(kBackLeftYPosInches), - kInvertLeftSide); - private static final SwerveModuleConstants BackRight = - ConstantCreator.createModuleConstants( - kBackRightSteerMotorId, - kBackRightDriveMotorId, - kBackRightEncoderId, - kBackRightEncoderOffset, - Units.inchesToMeters(kBackRightXPosInches), - Units.inchesToMeters(kBackRightYPosInches), - kInvertRightSide); - - public static final CommandSwerveDrivetrain DriveTrain = - new CommandSwerveDrivetrain( - DrivetrainConstants, 250, FrontLeft, FrontRight, BackLeft, BackRight); -} diff --git a/src/main/java/frc/robot/subsystems/drive/DriveConstants.java b/src/main/java/frc/robot/subsystems/drive/DriveConstants.java new file mode 100644 index 0000000..f0c8855 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/drive/DriveConstants.java @@ -0,0 +1,142 @@ +package frc.robot.subsystems.drive; + +import com.ctre.phoenix6.mechanisms.swerve.SwerveDrivetrainConstants; +import com.ctre.phoenix6.mechanisms.swerve.SwerveModuleConstantsFactory; +import com.ctre.phoenix6.mechanisms.swerve.SwerveModuleConstants.SteerFeedbackType; + +import frc.robot.subsystems.drive.swerve.SwerveModuleLocation; +import frc.robot.subsystems.drive.swerve.SwerveModules; + +public class DriveConstants { + /** Only touch these if you know what you're doing */ + private static final DriveSlotGains steerGains = new DriveSlotGains(100.0, 0, 0.05, 0, 0); + + private static final DriveSlotGains driveGains = new DriveSlotGains(3.0, 0, 0, 0, 0); + + public static final double kMaxSpeed = 6.0; // 6 meters per second desired top speed + public static final double kMaxAngularRate = + Math.PI; // Half a rotation per second max angular velocity + + private static final double kCoupleRatio = 3.5; + + private static final double kWheelRadiusInches = + 2.167; // Estimated at first, then fudge-factored to make odom + // match record + + private static final String kCANbusName = "rio"; + private static final int kPigeonId = 1; + + public static final double kSpeedAt12VoltsMps = 5.0; + public static final double kSimLoopPeriod = 0.005; // 5 ms + + private static final boolean kSteerMotorReversed = true; + public static final boolean kInvertLeftSide = false; + public static final boolean kInvertRightSide = true; + + /** The ratios of the drive and steer gears in the swerve modules */ + private class GearRatio { + private static final double kDriveGearRatio = 7.363636364; + private static final double kSteerGearRatio = 15.42857143; + } + + /** + * The simulated voltage necessary to overcome friction for swerve. This is only used for + * simulation + */ + private class FrictionVoltage { + private static final double kSteerFrictionVoltage = 0.25; + private static final double kDriveFrictionVoltage = 0.25; + } + + /** + * The inertia of the drive and steer motors in the swerve modules. These should be left to + * default + */ + private class Intertia { + private static final double kSteerInertia = 0.00001; + private static final double kDriveInertia = 0.001; + } + + /** + * The CAN IDs of the drive motors in the swerve modules NOTE: Be sure to change these to the + * correct values at the start of the season + */ + public class DriveMotorId { + public static final int kFrontLeftDriveMotorId = 5; + public static final int kFrontRightDriveMotorId = 7; + public static final int kBackLeftDriveMotorId = 1; + public static final int kBackRightDriveMotorId = 3; + } + + /** + * The CAN IDs of the steer motors in the swerve modules NOTE: Be sure to change these to the + * correct values at the start of the season + */ + public class SteerMotorId { + public static final int kFrontLeftSteerMotorId = 4; + public static final int kFrontRightSteerMotorId = 6; + public static final int kBackLeftSteerMotorId = 0; + public static final int kBackRightSteerMotorId = 2; + } + + /** + * The CAN IDs of the encoders in the swerve modules NOTE: Be sure to change these to the correct + * values at the start of the season + */ + public class EncoderId { + public static final int kFrontLeftEncoderId = 2; + public static final int kFrontRightEncoderId = 3; + public static final int kBackLeftEncoderId = 0; + public static final int kBackRightEncoderId = 1; + } + + public class EncoderOffset { + public static final double kFrontLeftEncoderOffset = -0.83544921875; + public static final double kFrontRightEncoderOffset = -0.15234375; + public static final double kBackLeftEncoderOffset = -0.4794921875; + public static final double kBackRightEncoderOffset = -0.84130859375; + } + + /** + * The X and Y positions of the swerve modules relative to the center of the robot in inches NOTE: + * Be sure to change these to the correct values at the start of the season + */ + public class SwerveModulePosition { + public static final SwerveModuleLocation kFrontLeft = new SwerveModuleLocation(10.5, 10.5); + public static final SwerveModuleLocation kFrontRight = new SwerveModuleLocation(10.5, -10.5); + public static final SwerveModuleLocation kBackLeft = new SwerveModuleLocation(-10.5, 10.5); + public static final SwerveModuleLocation kBackRight = new SwerveModuleLocation(-10.5, -10.5); + } + + private static final SwerveDrivetrainConstants DrivetrainConstants = + new SwerveDrivetrainConstants().withPigeon2Id(kPigeonId).withCANbusName(kCANbusName); + + public static final SwerveModuleConstantsFactory ConstantCreator = + new SwerveModuleConstantsFactory() + .withDriveMotorGearRatio(GearRatio.kDriveGearRatio) + .withSteerMotorGearRatio(GearRatio.kSteerGearRatio) + .withWheelRadius(kWheelRadiusInches) + .withSlipCurrent(30) + .withSteerMotorGains(steerGains) + .withDriveMotorGains(driveGains) + .withSpeedAt12VoltsMps( + kSpeedAt12VoltsMps) // Theoretical free speed is 10 meters per second at 12v applied + // output + .withSteerInertia(Intertia.kSteerInertia) + .withDriveInertia(Intertia.kDriveInertia) + .withFeedbackSource(SteerFeedbackType.FusedCANcoder) + .withSteerFrictionVoltage(FrictionVoltage.kSteerFrictionVoltage) + .withDriveFrictionVoltage(FrictionVoltage.kDriveFrictionVoltage) + .withCouplingGearRatio( + kCoupleRatio) // Every 1 rotation of the azimuth results in couple ratio drive turns + .withSteerMotorInverted(kSteerMotorReversed); + + /** Construct the final drivetrain object that we interact with */ + public static final DriveSubsystem DriveTrain = + new DriveSubsystem( + DrivetrainConstants, + SwerveModules.FrontLeft, + SwerveModules.FrontRight, + SwerveModules.BackLeft, + SwerveModules.BackRight); +} diff --git a/src/main/java/frc/robot/subsystems/drive/DriveSlotGains.java b/src/main/java/frc/robot/subsystems/drive/DriveSlotGains.java new file mode 100644 index 0000000..2879a83 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/drive/DriveSlotGains.java @@ -0,0 +1,13 @@ +package frc.robot.subsystems.drive; + +import com.ctre.phoenix6.configs.Slot0Configs; + +public class DriveSlotGains extends Slot0Configs { + public DriveSlotGains(double kP, double kI, double kD, double kV, double kS) { + this.kP = kP; + this.kI = kI; + this.kD = kD; + this.kV = kV; + this.kS = kS; + } +} diff --git a/src/main/java/frc/robot/CommandSwerveDrivetrain.java b/src/main/java/frc/robot/subsystems/drive/DriveSubsystem.java similarity index 51% rename from src/main/java/frc/robot/CommandSwerveDrivetrain.java rename to src/main/java/frc/robot/subsystems/drive/DriveSubsystem.java index 5211b54..33e576e 100644 --- a/src/main/java/frc/robot/CommandSwerveDrivetrain.java +++ b/src/main/java/frc/robot/subsystems/drive/DriveSubsystem.java @@ -1,7 +1,8 @@ -package frc.robot; +package frc.robot.subsystems.drive; import java.util.function.Supplier; +import com.ctre.phoenix6.Utils; import com.ctre.phoenix6.mechanisms.swerve.SwerveDrivetrain; import com.ctre.phoenix6.mechanisms.swerve.SwerveDrivetrainConstants; import com.ctre.phoenix6.mechanisms.swerve.SwerveModuleConstants; @@ -15,32 +16,33 @@ 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.wpilibj.Notifier; +import edu.wpi.first.wpilibj.RobotController; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.RunCommand; import edu.wpi.first.wpilibj2.command.Subsystem; -/** - * Class that extends the Phoenix SwerveDrivetrain class and implements subsystem so it can be used - * in command-based projects easily. - */ -public class CommandSwerveDrivetrain extends SwerveDrivetrain implements Subsystem { - private SwerveRequest.ApplyChassisSpeeds autoRequest = new SwerveRequest.ApplyChassisSpeeds(); +public class DriveSubsystem extends SwerveDrivetrain implements Subsystem { + private Notifier m_simNotifier = null; + private double m_lastSimTime; - public CommandSwerveDrivetrain( - SwerveDrivetrainConstants driveTrainConstants, - double OdometryUpdateFrequency, - SwerveModuleConstants... modules) { - super(driveTrainConstants, OdometryUpdateFrequency, modules); - configurePathPlanner(); - } + private SwerveRequest.ApplyChassisSpeeds autoRequest = new SwerveRequest.ApplyChassisSpeeds(); - public CommandSwerveDrivetrain( - SwerveDrivetrainConstants driveTrainConstants, SwerveModuleConstants... modules) { - super(driveTrainConstants, modules); - configurePathPlanner(); + @Override + public void simulationPeriodic() { + updateSimState(0.02, 12); } + /** + * Configures path planner to have a holonomic path follower so it can move in any direction. This + * is meant for holonomic drivetrains AND swerve + */ private void configurePathPlanner() { + double driveBaseRadius = 0; + for (var moduleLocation : m_moduleLocations) { + driveBaseRadius = Math.max(driveBaseRadius, moduleLocation.getNorm()); + } + AutoBuilder.configureHolonomic( () -> this.getState().Pose, // Supplier of current robot pose this::seedFieldRelative, // Consumer for seeding pose against auto @@ -51,13 +53,18 @@ private void configurePathPlanner() { new HolonomicPathFollowerConfig( new PIDConstants(10, 0, 0), new PIDConstants(10, 0, 0), - 1, - 1, - new ReplanningConfig(), - 0.004), - this); // Subsystem for requirements + DriveConstants.kSpeedAt12VoltsMps, + driveBaseRadius, + new ReplanningConfig()), + this); } + /** + * Applies and runs a command on the swerve drivetrain + * + * @param requestSupplier The supplier of the swerve request to apply (the request) + * @return A command which runs the specified request + */ public Command applyRequest(Supplier requestSupplier) { return new RunCommand( () -> { @@ -66,16 +73,16 @@ public Command applyRequest(Supplier requestSupplier) { this); } + /** + * Returns a command that will run the specified pathplanner path + * + * @param pathName The name of the path created in pathplanner + * @return A command which runs the specified path + */ public Command getAutoPath(String pathName) { return new PathPlannerAuto(pathName); } - @Override - public void simulationPeriodic() { - /* Assume */ - updateSimState(0.02, 12); - } - /** * Takes the specified location and makes it the current pose for field-relative maneuvers * @@ -93,7 +100,64 @@ public void seedFieldRelative(Pose2d location) { } } + /** + * Returns the current robot chassis speeds + * + * @return The current robot chassis speeds as a Twist2d object + */ public ChassisSpeeds getCurrentRobotChassisSpeeds() { return m_kinematics.toChassisSpeeds(getState().ModuleStates); } + + /** + * Updates the state of the drivetrain for simulation + * + * @param deltaTime The time since the last update + * @param batteryVoltage The current battery voltage + */ + private void startSimThread() { + m_lastSimTime = Utils.getCurrentTimeSeconds(); + + /* Run simulation at a faster rate so PID gains behave more reasonably */ + m_simNotifier = + new Notifier( + () -> { + final double currentTime = Utils.getCurrentTimeSeconds(); + double deltaTime = currentTime - m_lastSimTime; + m_lastSimTime = currentTime; + + /* use the measured time delta, get battery voltage from WPILib */ + updateSimState(deltaTime, RobotController.getBatteryVoltage()); + }); + m_simNotifier.startPeriodic(DriveConstants.kSimLoopPeriod); + } + + /** Add any methods you want to call when the drive subsystem is initialized and called */ + public void initialize() { + configurePathPlanner(); + } + + /** Initialize the drive subsystem when we create an instance of it and configure it * */ + public DriveSubsystem( + SwerveDrivetrainConstants driveTrainConstants, + double OdometryUpdateFrequency, + SwerveModuleConstants... modules) { + super(driveTrainConstants, OdometryUpdateFrequency, modules); + this.initialize(); + + if (Utils.isSimulation()) { + startSimThread(); + } + } + + /** Initialize the drive subsystem when we create an instance of it and configure it * */ + public DriveSubsystem( + SwerveDrivetrainConstants driveTrainConstants, SwerveModuleConstants... modules) { + super(driveTrainConstants, modules); + this.initialize(); + + if (Utils.isSimulation()) { + startSimThread(); + } + } } diff --git a/src/main/java/frc/robot/subsystems/drive/swerve/SwerveModuleLocation.java b/src/main/java/frc/robot/subsystems/drive/swerve/SwerveModuleLocation.java new file mode 100644 index 0000000..cd689e7 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/drive/swerve/SwerveModuleLocation.java @@ -0,0 +1,54 @@ +package frc.robot.subsystems.drive.swerve; + +import edu.wpi.first.math.util.Units; + +public class SwerveModuleLocation { + private double xPosition, yPosition; + + /** + * Stores the x and y position of a swerve module + * + * @param xPosition The X position of the module relative to the center of the robot + * @param yPosition The Y position of the module relative to the center of the robot + */ + public SwerveModuleLocation(double xPosition, double yPosition) { + this.xPosition = xPosition; + this.yPosition = yPosition; + } + + /** + * Returns the X position of the module relative to the center of the robot in inches + * + * @return The raw X position of the module in inches + */ + public double getXPositionRaw() { + return this.xPosition; + } + + /** + * Returns the Y position of the module relative to the center of the robot in inches + * + * @return The raw Y position of the module in inches + */ + public double getYPositionRaw() { + return this.yPosition; + } + + /** + * Returns the X position of the module relative to the center of the robot in meters + * + * @return The calculated X position of the module in meters + */ + public double getXPositionCalculated() { + return Units.inchesToMeters(this.xPosition); + } + + /** + * Returns the Y position of the module relative to the center of the robot in meters + * + * @return The calculated Y position of the module in meters + */ + public double getYPositionCalculated() { + return Units.inchesToMeters(this.yPosition); + } +} diff --git a/src/main/java/frc/robot/subsystems/drive/swerve/SwerveModules.java b/src/main/java/frc/robot/subsystems/drive/swerve/SwerveModules.java new file mode 100644 index 0000000..9afff70 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/drive/swerve/SwerveModules.java @@ -0,0 +1,53 @@ +package frc.robot.subsystems.drive.swerve; + +import com.ctre.phoenix6.mechanisms.swerve.SwerveModuleConstants; + +import frc.robot.subsystems.drive.DriveConstants; +import frc.robot.subsystems.drive.DriveConstants.DriveMotorId; +import frc.robot.subsystems.drive.DriveConstants.EncoderId; +import frc.robot.subsystems.drive.DriveConstants.EncoderOffset; +import frc.robot.subsystems.drive.DriveConstants.SteerMotorId; +import frc.robot.subsystems.drive.DriveConstants.SwerveModulePosition; + +/** This class houses the constants and assembled swerve module configurations for the robot */ +public class SwerveModules { + public static final SwerveModuleConstants FrontLeft = + DriveConstants.ConstantCreator.createModuleConstants( + SteerMotorId.kFrontLeftSteerMotorId, + DriveMotorId.kFrontLeftDriveMotorId, + EncoderId.kFrontLeftEncoderId, + EncoderOffset.kFrontLeftEncoderOffset, + SwerveModulePosition.kFrontLeft.getXPositionCalculated(), + SwerveModulePosition.kFrontLeft.getXPositionCalculated(), + DriveConstants.kInvertLeftSide); + + public static final SwerveModuleConstants FrontRight = + DriveConstants.ConstantCreator.createModuleConstants( + SteerMotorId.kFrontRightSteerMotorId, + DriveMotorId.kFrontRightDriveMotorId, + EncoderId.kFrontRightEncoderId, + EncoderOffset.kFrontRightEncoderOffset, + SwerveModulePosition.kFrontRight.getXPositionCalculated(), + SwerveModulePosition.kFrontRight.getXPositionCalculated(), + DriveConstants.kInvertRightSide); + + public static final SwerveModuleConstants BackLeft = + DriveConstants.ConstantCreator.createModuleConstants( + SteerMotorId.kBackLeftSteerMotorId, + DriveMotorId.kBackLeftDriveMotorId, + EncoderId.kBackLeftEncoderId, + EncoderOffset.kBackLeftEncoderOffset, + SwerveModulePosition.kBackLeft.getXPositionCalculated(), + SwerveModulePosition.kBackLeft.getXPositionCalculated(), + DriveConstants.kInvertLeftSide); + + public static final SwerveModuleConstants BackRight = + DriveConstants.ConstantCreator.createModuleConstants( + SteerMotorId.kBackRightSteerMotorId, + DriveMotorId.kBackRightDriveMotorId, + EncoderId.kBackRightEncoderId, + EncoderOffset.kBackRightEncoderOffset, + SwerveModulePosition.kBackRight.getXPositionCalculated(), + SwerveModulePosition.kBackRight.getXPositionCalculated(), + DriveConstants.kInvertRightSide); +} diff --git a/vendordeps/PathplannerLib.json b/vendordeps/PathplannerLib.json index aeaa3ba..a92663b 100644 --- a/vendordeps/PathplannerLib.json +++ b/vendordeps/PathplannerLib.json @@ -1,7 +1,7 @@ { "fileName": "PathplannerLib.json", "name": "PathplannerLib", - "version": "2024.0.0-beta-1", + "version": "2024.0.0-beta-6.2", "uuid": "1b42324f-17c6-4875-8e77-1c312bc8c786", "frcYear": "2024", "mavenUrls": [ @@ -12,7 +12,7 @@ { "groupId": "com.pathplanner.lib", "artifactId": "PathplannerLib-java", - "version": "2024.0.0-beta-1" + "version": "2024.0.0-beta-6.2" } ], "jniDependencies": [], @@ -20,7 +20,7 @@ { "groupId": "com.pathplanner.lib", "artifactId": "PathplannerLib-cpp", - "version": "2024.0.0-beta-1", + "version": "2024.0.0-beta-6.2", "libName": "PathplannerLib", "headerClassifier": "headers", "sharedLibrary": false, diff --git a/vendordeps/Phoenix6.json b/vendordeps/Phoenix6.json index 1f9f441..622e9c7 100644 --- a/vendordeps/Phoenix6.json +++ b/vendordeps/Phoenix6.json @@ -1,7 +1,7 @@ { "fileName": "Phoenix6.json", "name": "CTRE-Phoenix (v6)", - "version": "24.0.0-beta-1", + "version": "24.0.0-beta-7", "frcYear": 2024, "uuid": "e995de00-2c64-4df5-8831-c1441420ff19", "mavenUrls": [ @@ -19,14 +19,14 @@ { "groupId": "com.ctre.phoenix6", "artifactId": "wpiapi-java", - "version": "24.0.0-beta-1" + "version": "24.0.0-beta-7" } ], "jniDependencies": [ { "groupId": "com.ctre.phoenix6", "artifactId": "tools", - "version": "24.0.0-beta-1", + "version": "24.0.0-beta-7", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -39,7 +39,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "tools-sim", - "version": "24.0.0-beta-1", + "version": "24.0.0-beta-7", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -52,7 +52,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simTalonSRX", - "version": "24.0.0-beta-1", + "version": "24.0.0-beta-7", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -65,7 +65,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simTalonFX", - "version": "24.0.0-beta-1", + "version": "24.0.0-beta-7", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -78,7 +78,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simVictorSPX", - "version": "24.0.0-beta-1", + "version": "24.0.0-beta-7", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -91,7 +91,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simPigeonIMU", - "version": "24.0.0-beta-1", + "version": "24.0.0-beta-7", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -104,7 +104,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simCANCoder", - "version": "24.0.0-beta-1", + "version": "24.0.0-beta-7", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -117,7 +117,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProTalonFX", - "version": "24.0.0-beta-1", + "version": "24.0.0-beta-7", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -130,7 +130,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProCANcoder", - "version": "24.0.0-beta-1", + "version": "24.0.0-beta-7", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -143,7 +143,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProPigeon2", - "version": "24.0.0-beta-1", + "version": "24.0.0-beta-7", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -158,7 +158,7 @@ { "groupId": "com.ctre.phoenix6", "artifactId": "wpiapi-cpp", - "version": "24.0.0-beta-1", + "version": "24.0.0-beta-7", "libName": "CTRE_Phoenix6_WPI", "headerClassifier": "headers", "sharedLibrary": true, @@ -173,7 +173,7 @@ { "groupId": "com.ctre.phoenix6", "artifactId": "tools", - "version": "24.0.0-beta-1", + "version": "24.0.0-beta-7", "libName": "CTRE_PhoenixTools", "headerClassifier": "headers", "sharedLibrary": true, @@ -188,7 +188,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "wpiapi-cpp-sim", - "version": "24.0.0-beta-1", + "version": "24.0.0-beta-7", "libName": "CTRE_Phoenix6_WPISim", "headerClassifier": "headers", "sharedLibrary": true, @@ -203,7 +203,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "tools-sim", - "version": "24.0.0-beta-1", + "version": "24.0.0-beta-7", "libName": "CTRE_PhoenixTools_Sim", "headerClassifier": "headers", "sharedLibrary": true, @@ -218,7 +218,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simTalonSRX", - "version": "24.0.0-beta-1", + "version": "24.0.0-beta-7", "libName": "CTRE_SimTalonSRX", "headerClassifier": "headers", "sharedLibrary": true, @@ -233,7 +233,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simTalonFX", - "version": "24.0.0-beta-1", + "version": "24.0.0-beta-7", "libName": "CTRE_SimTalonFX", "headerClassifier": "headers", "sharedLibrary": true, @@ -248,7 +248,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simVictorSPX", - "version": "24.0.0-beta-1", + "version": "24.0.0-beta-7", "libName": "CTRE_SimVictorSPX", "headerClassifier": "headers", "sharedLibrary": true, @@ -263,7 +263,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simPigeonIMU", - "version": "24.0.0-beta-1", + "version": "24.0.0-beta-7", "libName": "CTRE_SimPigeonIMU", "headerClassifier": "headers", "sharedLibrary": true, @@ -278,7 +278,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simCANCoder", - "version": "24.0.0-beta-1", + "version": "24.0.0-beta-7", "libName": "CTRE_SimCANCoder", "headerClassifier": "headers", "sharedLibrary": true, @@ -293,7 +293,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProTalonFX", - "version": "24.0.0-beta-1", + "version": "24.0.0-beta-7", "libName": "CTRE_SimProTalonFX", "headerClassifier": "headers", "sharedLibrary": true, @@ -308,7 +308,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProCANcoder", - "version": "24.0.0-beta-1", + "version": "24.0.0-beta-7", "libName": "CTRE_SimProCANcoder", "headerClassifier": "headers", "sharedLibrary": true, @@ -323,7 +323,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProPigeon2", - "version": "24.0.0-beta-1", + "version": "24.0.0-beta-7", "libName": "CTRE_SimProPigeon2", "headerClassifier": "headers", "sharedLibrary": true, From 56ccc2b45036b3dc5d574e921868f403997ddfe5 Mon Sep 17 00:00:00 2001 From: Ian Tapply Date: Fri, 5 Jan 2024 20:08:50 -0500 Subject: [PATCH 12/14] Rename IntakeGamepieces enum to IntakeGamepiece (#57) # Description Renamed IntakeGamepieces enum to IntakeGamepiece to make more sense grammatically Fixes #54 ## Type of change Please delete options that are not relevant. - [x] Bug fix (non-breaking change which fixes an issue) # How Has This Been Tested? N/A # Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation, if any - [x] My changes generate no new warnings - [x] I have performed tests that prove my fix is effective or that my feature works, if necessary - [x] New and existing unit tests pass locally with my changes --------- Co-authored-by: github-actions <> --- .../subsystems/intake/IntakeSubsystem.java | 99 +++---------------- ...keGamepieces.java => IntakeGamepiece.java} | 2 +- .../intake/enums/IntakeScoreType.java | 14 --- .../intake/states/ScoringState.java | 66 +++++++++++++ .../intake/states/scoring/cone/HighCone.java | 11 +++ .../states/scoring/cone/HighConeAuto.java | 11 +++ .../intake/states/scoring/cone/MidCone.java | 11 +++ .../states/scoring/cone/MidConeTipped.java | 11 +++ .../intake/states/scoring/cube/HighCube.java | 11 +++ .../states/scoring/cube/HighCubeAuto.java | 11 +++ .../intake/states/scoring/cube/MidCube.java | 11 +++ .../states/scoring/cube/MidCubeAuto.java | 11 +++ .../intake/states/scoring/level/Low.java | 11 +++ .../intake/states/scoring/level/LowAuto.java | 14 +++ 14 files changed, 193 insertions(+), 101 deletions(-) rename src/main/java/frc/robot/subsystems/intake/enums/{IntakeGamepieces.java => IntakeGamepiece.java} (66%) delete mode 100644 src/main/java/frc/robot/subsystems/intake/enums/IntakeScoreType.java create mode 100644 src/main/java/frc/robot/subsystems/intake/states/ScoringState.java create mode 100644 src/main/java/frc/robot/subsystems/intake/states/scoring/cone/HighCone.java create mode 100644 src/main/java/frc/robot/subsystems/intake/states/scoring/cone/HighConeAuto.java create mode 100644 src/main/java/frc/robot/subsystems/intake/states/scoring/cone/MidCone.java create mode 100644 src/main/java/frc/robot/subsystems/intake/states/scoring/cone/MidConeTipped.java create mode 100644 src/main/java/frc/robot/subsystems/intake/states/scoring/cube/HighCube.java create mode 100644 src/main/java/frc/robot/subsystems/intake/states/scoring/cube/HighCubeAuto.java create mode 100644 src/main/java/frc/robot/subsystems/intake/states/scoring/cube/MidCube.java create mode 100644 src/main/java/frc/robot/subsystems/intake/states/scoring/cube/MidCubeAuto.java create mode 100644 src/main/java/frc/robot/subsystems/intake/states/scoring/level/Low.java create mode 100644 src/main/java/frc/robot/subsystems/intake/states/scoring/level/LowAuto.java diff --git a/src/main/java/frc/robot/subsystems/intake/IntakeSubsystem.java b/src/main/java/frc/robot/subsystems/intake/IntakeSubsystem.java index 05ef9f3..94ec8c9 100644 --- a/src/main/java/frc/robot/subsystems/intake/IntakeSubsystem.java +++ b/src/main/java/frc/robot/subsystems/intake/IntakeSubsystem.java @@ -6,8 +6,8 @@ import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.Commands; import edu.wpi.first.wpilibj2.command.SubsystemBase; -import frc.robot.subsystems.intake.enums.IntakeGamepieces; -import frc.robot.subsystems.intake.enums.IntakeScoreType; +import frc.robot.subsystems.intake.enums.IntakeGamepiece; +import frc.robot.subsystems.intake.states.ScoringState; /** * This an example implementation of our intake subsystem from 2023. @@ -52,91 +52,18 @@ public Command stopIntakeCommand() { /** * Runs a command to score a gamepiece. * - * @param type the type of the score you want to make + * @param scoringState the state to run the intake in * @param expectedPiece the type of gamepiece to expect when scoring * @return a command that scores a gamepiece */ - public Command intakeScoreCommand(IntakeScoreType type, IntakeGamepieces expectedPiece) { - return run(() -> { - switch (type) { - case MID_CONE -> { - Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.MID_CONE), this) - .andThen(Commands.waitSeconds(0.5)) - .andThen(stopIntakeCommand()); - } - - case MID_CONE_TIPPED -> { - Commands.runOnce( - () -> outtakeCommand(IntakeConstants.OuttakeSpeeds.MID_CONE_TIPPED), this) - .andThen(Commands.waitSeconds(0.5)) - .andThen(stopIntakeCommand()); - } - - case MID_CUBE -> { - Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.MID_CUBE), this) - .andThen(Commands.waitSeconds(0.5)) - .andThen(stopIntakeCommand()); - } - - case MID_CUBE_AUTO -> { - Commands.runOnce( - () -> outtakeCommand(IntakeConstants.OuttakeSpeeds.MID_CUBE_AUTO), this) - .andThen(Commands.waitSeconds(0.5)) - .andThen(stopIntakeCommand()); - } - - case HIGH_CONE -> { - Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.HIGH_CONE), this) - .andThen(Commands.waitSeconds(0.5)) - .andThen(stopIntakeCommand()); - } - - case HIGH_CONE_AUTO -> { - Commands.runOnce( - () -> outtakeCommand(IntakeConstants.OuttakeSpeeds.HIGH_CONE_AUTO), this) - .andThen(Commands.waitSeconds(0.5)) - .andThen(stopIntakeCommand()); - } + public Command intakeScoreCommand(ScoringState scoringState, IntakeGamepiece expectedGamepiece) { + SmartDashboard.putString("INTAKE STATE", scoringState.getStateName()); - case HIGH_CUBE -> { - Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.HIGH_CUBE), this) - .andThen(Commands.waitSeconds(0.5)) - .andThen(stopIntakeCommand()); - } - - case HIGH_CUBE_AUTO -> { - Commands.runOnce( - () -> outtakeCommand(IntakeConstants.OuttakeSpeeds.HIGH_CUBE_AUTO), this) - .andThen(Commands.waitSeconds(0.5)) - .andThen(stopIntakeCommand()); - } - - case LOW -> { - if (expectedPiece.equals(IntakeGamepieces.CUBE)) { - Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.LOW_CUBE), this) - .andThen(Commands.waitSeconds(1)) - .andThen(stopIntakeCommand()); - } else { - Commands.runOnce(() -> outtakeCommand(IntakeConstants.OuttakeSpeeds.LOW_CONE), this) - .andThen(Commands.waitSeconds(1)) - .andThen(stopIntakeCommand()); - } - } - - case LOW_AUTO -> { - if (expectedPiece.equals(IntakeGamepieces.CUBE)) { - Commands.runOnce( - () -> outtakeCommand(IntakeConstants.OuttakeSpeeds.LOW_CUBE_AUTO), this) - .andThen(Commands.waitSeconds(1)) - .andThen(stopIntakeCommand()); - } else { - Commands.runOnce( - () -> outtakeCommand(IntakeConstants.OuttakeSpeeds.LOW_CONE_AUTO), this) - .andThen(Commands.waitSeconds(1)) - .andThen(stopIntakeCommand()); - } - } - } + return run(() -> { + Commands.runOnce( + () -> outtakeCommand(scoringState.getOuttakeSpeed(expectedGamepiece)), this) + .andThen(Commands.waitSeconds(0.5)) + .andThen(stopIntakeCommand()); }) .andThen(stopIntakeCommand()); } @@ -147,7 +74,7 @@ public Command intakeScoreCommand(IntakeScoreType type, IntakeGamepieces expecte * @param gamepiece the type of gamepiece to expect * @return a command that forces the intake to hold the specified gamepiece */ - public Command intakeHoldCommand(IntakeGamepieces gamepiece) { + public Command intakeHoldCommand(IntakeGamepiece gamepiece) { return run(() -> { this.intakeMotor.set(IntakeConstants.INTAKING_SPEED); @@ -158,12 +85,12 @@ public Command intakeHoldCommand(IntakeGamepieces gamepiece) { this.pdp.getCurrent(IntakeConstants.INTAKE_MOTOR_CHANNEL) < IntakeConstants.INTAKE_AMP_THRESHOLD); - if (gamepiece.equals(IntakeGamepieces.CUBE)) { + if (gamepiece.equals(IntakeGamepiece.CUBE)) { // wait a short amount of time so the gamepiece gets pulled in Commands.waitSeconds(IntakeConstants.INTAKE_CUBE_DELAY); this.intakeMotor.set(IntakeConstants.HOLD_CUBE_SPEED); } - if (gamepiece.equals(IntakeGamepieces.CONE)) { + if (gamepiece.equals(IntakeGamepiece.CONE)) { // we have a cone, so run the motor at a higher speed Commands.waitSeconds(IntakeConstants.INTAKE_CONE_DELAY); this.intakeMotor.set(IntakeConstants.HOLD_CONE_SPEED); diff --git a/src/main/java/frc/robot/subsystems/intake/enums/IntakeGamepieces.java b/src/main/java/frc/robot/subsystems/intake/enums/IntakeGamepiece.java similarity index 66% rename from src/main/java/frc/robot/subsystems/intake/enums/IntakeGamepieces.java rename to src/main/java/frc/robot/subsystems/intake/enums/IntakeGamepiece.java index 3a87c37..4fccfd5 100644 --- a/src/main/java/frc/robot/subsystems/intake/enums/IntakeGamepieces.java +++ b/src/main/java/frc/robot/subsystems/intake/enums/IntakeGamepiece.java @@ -1,6 +1,6 @@ package frc.robot.subsystems.intake.enums; -public enum IntakeGamepieces { +public enum IntakeGamepiece { CONE, CUBE } diff --git a/src/main/java/frc/robot/subsystems/intake/enums/IntakeScoreType.java b/src/main/java/frc/robot/subsystems/intake/enums/IntakeScoreType.java deleted file mode 100644 index f1fe54e..0000000 --- a/src/main/java/frc/robot/subsystems/intake/enums/IntakeScoreType.java +++ /dev/null @@ -1,14 +0,0 @@ -package frc.robot.subsystems.intake.enums; - -public enum IntakeScoreType { - HIGH_CONE, - HIGH_CONE_AUTO, - HIGH_CUBE, - HIGH_CUBE_AUTO, - MID_CONE, - MID_CONE_TIPPED, - MID_CUBE, - MID_CUBE_AUTO, - LOW, - LOW_AUTO -} diff --git a/src/main/java/frc/robot/subsystems/intake/states/ScoringState.java b/src/main/java/frc/robot/subsystems/intake/states/ScoringState.java new file mode 100644 index 0000000..3260774 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/intake/states/ScoringState.java @@ -0,0 +1,66 @@ +package frc.robot.subsystems.intake.states; + +import frc.robot.subsystems.intake.enums.IntakeGamepiece; + +public abstract class ScoringState { + double outtakeSpeed; + double cubeOuttakeSpeed; + double coneOuttakeSpeed; + + String stateName; + + /** + * Creates a scoring state with the same outtake speed for cubes and cones + * + * @param outtakeSpeed The speed that should be used to outtake the gamepiece + * @param stateName The name of the scoring state that should be used for logging + */ + protected ScoringState(double outtakeSpeed, String stateName) { + this.outtakeSpeed = outtakeSpeed; + this.stateName = stateName; + } + + /** + * Creates a scoring state with different outtake speeds for cubes and cones + * + * @param cubeOuttakeSpeed The speed that should be used to outtake the cube gamepiece + * @param coneOuttakeSpeed The speed that should be used to outtake the cone gamepiece + * @param stateName The name of the scoring state that should be used for logging + */ + protected ScoringState(double cubeOuttakeSpeed, double coneOuttakeSpeed, String stateName) { + this.cubeOuttakeSpeed = cubeOuttakeSpeed; + this.coneOuttakeSpeed = coneOuttakeSpeed; + this.stateName = stateName; + } + + /** + * Gets the name of the scoring state + * + * @return The name of the scoring state + */ + public String getStateName() { + return this.stateName; + } + + /** + * Gets the outtake speed for the scoring state depending on the gamepiece that is expected to be + * scored + * + * @return The outtake speed for the scoring state + */ + public double getOuttakeSpeed(IntakeGamepiece expectedGamepiece) { + if (expectedGamepiece == null) { + return this.outtakeSpeed; + } + + // Based on the gamepiece that is expected to be scored, return the appropriate outtake speed + switch (expectedGamepiece) { + case CUBE: + return this.cubeOuttakeSpeed; + case CONE: + return this.coneOuttakeSpeed; + default: + return 0.0; + } + } +} diff --git a/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/HighCone.java b/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/HighCone.java new file mode 100644 index 0000000..2bbe772 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/HighCone.java @@ -0,0 +1,11 @@ +package frc.robot.subsystems.intake.states.scoring.cone; + +import frc.robot.subsystems.intake.IntakeConstants; +import frc.robot.subsystems.intake.states.ScoringState; + +public class HighCone extends ScoringState { + + HighCone() { + super(IntakeConstants.OuttakeSpeeds.HIGH_CONE, "High Cone"); + } +} diff --git a/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/HighConeAuto.java b/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/HighConeAuto.java new file mode 100644 index 0000000..f36850d --- /dev/null +++ b/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/HighConeAuto.java @@ -0,0 +1,11 @@ +package frc.robot.subsystems.intake.states.scoring.cone; + +import frc.robot.subsystems.intake.IntakeConstants; +import frc.robot.subsystems.intake.states.ScoringState; + +public class HighConeAuto extends ScoringState { + + HighConeAuto() { + super(IntakeConstants.OuttakeSpeeds.HIGH_CONE_AUTO, "High Cone Auto"); + } +} diff --git a/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/MidCone.java b/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/MidCone.java new file mode 100644 index 0000000..2e582e2 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/MidCone.java @@ -0,0 +1,11 @@ +package frc.robot.subsystems.intake.states.scoring.cone; + +import frc.robot.subsystems.intake.IntakeConstants; +import frc.robot.subsystems.intake.states.ScoringState; + +public class MidCone extends ScoringState { + + MidCone() { + super(IntakeConstants.OuttakeSpeeds.MID_CONE, "Mid Cone"); + } +} diff --git a/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/MidConeTipped.java b/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/MidConeTipped.java new file mode 100644 index 0000000..f545e37 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/MidConeTipped.java @@ -0,0 +1,11 @@ +package frc.robot.subsystems.intake.states.scoring.cone; + +import frc.robot.subsystems.intake.IntakeConstants; +import frc.robot.subsystems.intake.states.ScoringState; + +public class MidConeTipped extends ScoringState { + + MidConeTipped() { + super(IntakeConstants.OuttakeSpeeds.MID_CONE_TIPPED, "Mid Cone Tipped"); + } +} diff --git a/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/HighCube.java b/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/HighCube.java new file mode 100644 index 0000000..3f1ce58 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/HighCube.java @@ -0,0 +1,11 @@ +package frc.robot.subsystems.intake.states.scoring.cube; + +import frc.robot.subsystems.intake.IntakeConstants; +import frc.robot.subsystems.intake.states.ScoringState; + +public class HighCube extends ScoringState { + + HighCube() { + super(IntakeConstants.OuttakeSpeeds.HIGH_CUBE, "High Cube"); + } +} diff --git a/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/HighCubeAuto.java b/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/HighCubeAuto.java new file mode 100644 index 0000000..0592eed --- /dev/null +++ b/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/HighCubeAuto.java @@ -0,0 +1,11 @@ +package frc.robot.subsystems.intake.states.scoring.cube; + +import frc.robot.subsystems.intake.IntakeConstants; +import frc.robot.subsystems.intake.states.ScoringState; + +public class HighCubeAuto extends ScoringState { + + HighCubeAuto() { + super(IntakeConstants.OuttakeSpeeds.HIGH_CUBE, "High Cube Auto"); + } +} diff --git a/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/MidCube.java b/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/MidCube.java new file mode 100644 index 0000000..40758a9 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/MidCube.java @@ -0,0 +1,11 @@ +package frc.robot.subsystems.intake.states.scoring.cube; + +import frc.robot.subsystems.intake.IntakeConstants; +import frc.robot.subsystems.intake.states.ScoringState; + +public class MidCube extends ScoringState { + + MidCube() { + super(IntakeConstants.OuttakeSpeeds.MID_CUBE, "Mid Cube"); + } +} diff --git a/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/MidCubeAuto.java b/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/MidCubeAuto.java new file mode 100644 index 0000000..022b8e6 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/MidCubeAuto.java @@ -0,0 +1,11 @@ +package frc.robot.subsystems.intake.states.scoring.cube; + +import frc.robot.subsystems.intake.IntakeConstants; +import frc.robot.subsystems.intake.states.ScoringState; + +public class MidCubeAuto extends ScoringState { + + MidCubeAuto() { + super(IntakeConstants.OuttakeSpeeds.MID_CUBE, "Mid Cube Auto"); + } +} diff --git a/src/main/java/frc/robot/subsystems/intake/states/scoring/level/Low.java b/src/main/java/frc/robot/subsystems/intake/states/scoring/level/Low.java new file mode 100644 index 0000000..daad78d --- /dev/null +++ b/src/main/java/frc/robot/subsystems/intake/states/scoring/level/Low.java @@ -0,0 +1,11 @@ +package frc.robot.subsystems.intake.states.scoring.level; + +import frc.robot.subsystems.intake.IntakeConstants; +import frc.robot.subsystems.intake.states.ScoringState; + +public class Low extends ScoringState { + + Low() { + super(IntakeConstants.OuttakeSpeeds.LOW_CUBE, IntakeConstants.OuttakeSpeeds.LOW_CONE, "Low"); + } +} diff --git a/src/main/java/frc/robot/subsystems/intake/states/scoring/level/LowAuto.java b/src/main/java/frc/robot/subsystems/intake/states/scoring/level/LowAuto.java new file mode 100644 index 0000000..5135b0b --- /dev/null +++ b/src/main/java/frc/robot/subsystems/intake/states/scoring/level/LowAuto.java @@ -0,0 +1,14 @@ +package frc.robot.subsystems.intake.states.scoring.level; + +import frc.robot.subsystems.intake.IntakeConstants; +import frc.robot.subsystems.intake.states.ScoringState; + +public class LowAuto extends ScoringState { + + LowAuto() { + super( + IntakeConstants.OuttakeSpeeds.LOW_CUBE_AUTO, + IntakeConstants.OuttakeSpeeds.LOW_CONE_AUTO, + "Low Auto"); + } +} From 8ea1b85b45ab00354a2958dda822de2127d02da8 Mon Sep 17 00:00:00 2001 From: Ian Tapply Date: Fri, 5 Jan 2024 20:16:00 -0500 Subject: [PATCH 13/14] Move intake scoring states to separate classes (#56) # Description Moved the scoring states for the intake to separate classes to improve organization and to remove the large conditional block Fixes #51 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) # How Has This Been Tested? N/A # Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation, if any - [x] My changes generate no new warnings - [x] I have performed tests that prove my fix is effective or that my feature works, if necessary - [x] New and existing unit tests pass locally with my changes --------- Co-authored-by: github-actions <> Co-authored-by: kaleb-dodd <47876236+kaleb-dodd@users.noreply.github.com> --- .../java/frc/robot/subsystems/intake/IntakeSubsystem.java | 3 +++ .../frc/robot/subsystems/intake/states/ScoringState.java | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/src/main/java/frc/robot/subsystems/intake/IntakeSubsystem.java b/src/main/java/frc/robot/subsystems/intake/IntakeSubsystem.java index 94ec8c9..d4938b9 100644 --- a/src/main/java/frc/robot/subsystems/intake/IntakeSubsystem.java +++ b/src/main/java/frc/robot/subsystems/intake/IntakeSubsystem.java @@ -6,6 +6,7 @@ import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.Commands; import edu.wpi.first.wpilibj2.command.SubsystemBase; + import frc.robot.subsystems.intake.enums.IntakeGamepiece; import frc.robot.subsystems.intake.states.ScoringState; @@ -56,7 +57,9 @@ public Command stopIntakeCommand() { * @param expectedPiece the type of gamepiece to expect when scoring * @return a command that scores a gamepiece */ + public Command intakeScoreCommand(ScoringState scoringState, IntakeGamepiece expectedGamepiece) { + SmartDashboard.putString("INTAKE STATE", scoringState.getStateName()); return run(() -> { diff --git a/src/main/java/frc/robot/subsystems/intake/states/ScoringState.java b/src/main/java/frc/robot/subsystems/intake/states/ScoringState.java index 3260774..34391fa 100644 --- a/src/main/java/frc/robot/subsystems/intake/states/ScoringState.java +++ b/src/main/java/frc/robot/subsystems/intake/states/ScoringState.java @@ -1,7 +1,9 @@ package frc.robot.subsystems.intake.states; + import frc.robot.subsystems.intake.enums.IntakeGamepiece; + public abstract class ScoringState { double outtakeSpeed; double cubeOuttakeSpeed; @@ -46,9 +48,12 @@ public String getStateName() { * Gets the outtake speed for the scoring state depending on the gamepiece that is expected to be * scored * + + * @param expectedGamepiece The gamepiece that is expected to be scored * @return The outtake speed for the scoring state */ public double getOuttakeSpeed(IntakeGamepiece expectedGamepiece) { + if (expectedGamepiece == null) { return this.outtakeSpeed; } From 7dc84b09a77d5fbeebd2335aeee335a22d49f3ce Mon Sep 17 00:00:00 2001 From: Cobblestone Date: Fri, 5 Jan 2024 20:54:37 -0500 Subject: [PATCH 14/14] Add intake subsystem to RobotContainer.java (#60) # Description This pull request adds the intake subsystem to the robot container, allowing the intake to be used. Scoring states have also been updated to be publicly accessible Fixes #53 ## Type of change Please delete options that are not relevant. - [X] New feature (non-breaking change which adds functionality) # Checklist: - [X] My code follows the style guidelines of this project - [X] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [X] I have made corresponding changes to the documentation, if any - [X] My changes generate no new warnings - [X] I have performed tests that prove my fix is effective or that my feature works, if necessary - [X] New and existing unit tests pass locally with my changes --------- Co-authored-by: github-actions <> --- src/main/java/frc/robot/RobotContainer.java | 18 ++++++++++++++++++ .../subsystems/intake/IntakeSubsystem.java | 1 - .../subsystems/intake/states/ScoringState.java | 3 --- .../intake/states/scoring/cone/HighCone.java | 2 +- .../states/scoring/cone/HighConeAuto.java | 2 +- .../intake/states/scoring/cone/MidCone.java | 2 +- .../states/scoring/cone/MidConeTipped.java | 2 +- .../intake/states/scoring/cube/HighCube.java | 2 +- .../states/scoring/cube/HighCubeAuto.java | 2 +- .../intake/states/scoring/cube/MidCube.java | 2 +- .../states/scoring/cube/MidCubeAuto.java | 2 +- .../intake/states/scoring/level/Low.java | 2 +- .../intake/states/scoring/level/LowAuto.java | 2 +- 13 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java index d786c88..36551bc 100644 --- a/src/main/java/frc/robot/RobotContainer.java +++ b/src/main/java/frc/robot/RobotContainer.java @@ -7,10 +7,14 @@ import com.ctre.phoenix6.mechanisms.swerve.SwerveModule.DriveRequestType; import com.ctre.phoenix6.mechanisms.swerve.SwerveRequest; +import edu.wpi.first.wpilibj.PowerDistribution; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.InstantCommand; import frc.robot.subsystems.drive.DriveConstants; import frc.robot.subsystems.drive.DriveSubsystem; +import frc.robot.subsystems.intake.IntakeSubsystem; +import frc.robot.subsystems.intake.enums.IntakeGamepiece; +import frc.robot.subsystems.intake.states.scoring.cone.HighCone; import frc.robot.subsystems.led.LEDSubsystem; public class RobotContainer { @@ -20,6 +24,9 @@ private class Controller { } private final LEDSubsystem ledSubsystem; + private final IntakeSubsystem intakeSubsystem; + + private final PowerDistribution pdp = new PowerDistribution(); // Set up the base for the drive and drivetrain final DriveSubsystem drivetrain = DriveConstants.DriveTrain; @@ -57,11 +64,22 @@ private void configureBindings() { // (left) ) .ignoringDisable(true)); + + Controller.operator + .yellowButton() + .toggleOnTrue(intakeSubsystem.intakeHoldCommand(IntakeGamepiece.CONE)); + Controller.operator + .blueButton() + .toggleOnTrue(intakeSubsystem.intakeHoldCommand(IntakeGamepiece.CUBE)); + Controller.operator + .greenButton() + .toggleOnTrue(intakeSubsystem.intakeScoreCommand(new HighCone(), IntakeGamepiece.CONE)); } public RobotContainer() { configureBindings(); ledSubsystem = new LEDSubsystem(); + intakeSubsystem = new IntakeSubsystem(pdp); } public Command getAutonomousCommand() { diff --git a/src/main/java/frc/robot/subsystems/intake/IntakeSubsystem.java b/src/main/java/frc/robot/subsystems/intake/IntakeSubsystem.java index d4938b9..ad21f11 100644 --- a/src/main/java/frc/robot/subsystems/intake/IntakeSubsystem.java +++ b/src/main/java/frc/robot/subsystems/intake/IntakeSubsystem.java @@ -57,7 +57,6 @@ public Command stopIntakeCommand() { * @param expectedPiece the type of gamepiece to expect when scoring * @return a command that scores a gamepiece */ - public Command intakeScoreCommand(ScoringState scoringState, IntakeGamepiece expectedGamepiece) { SmartDashboard.putString("INTAKE STATE", scoringState.getStateName()); diff --git a/src/main/java/frc/robot/subsystems/intake/states/ScoringState.java b/src/main/java/frc/robot/subsystems/intake/states/ScoringState.java index 34391fa..0326901 100644 --- a/src/main/java/frc/robot/subsystems/intake/states/ScoringState.java +++ b/src/main/java/frc/robot/subsystems/intake/states/ScoringState.java @@ -1,9 +1,7 @@ package frc.robot.subsystems.intake.states; - import frc.robot.subsystems.intake.enums.IntakeGamepiece; - public abstract class ScoringState { double outtakeSpeed; double cubeOuttakeSpeed; @@ -48,7 +46,6 @@ public String getStateName() { * Gets the outtake speed for the scoring state depending on the gamepiece that is expected to be * scored * - * @param expectedGamepiece The gamepiece that is expected to be scored * @return The outtake speed for the scoring state */ diff --git a/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/HighCone.java b/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/HighCone.java index 2bbe772..b8a2032 100644 --- a/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/HighCone.java +++ b/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/HighCone.java @@ -5,7 +5,7 @@ public class HighCone extends ScoringState { - HighCone() { + public HighCone() { super(IntakeConstants.OuttakeSpeeds.HIGH_CONE, "High Cone"); } } diff --git a/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/HighConeAuto.java b/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/HighConeAuto.java index f36850d..95c87c5 100644 --- a/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/HighConeAuto.java +++ b/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/HighConeAuto.java @@ -5,7 +5,7 @@ public class HighConeAuto extends ScoringState { - HighConeAuto() { + public HighConeAuto() { super(IntakeConstants.OuttakeSpeeds.HIGH_CONE_AUTO, "High Cone Auto"); } } diff --git a/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/MidCone.java b/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/MidCone.java index 2e582e2..4bc00f9 100644 --- a/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/MidCone.java +++ b/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/MidCone.java @@ -5,7 +5,7 @@ public class MidCone extends ScoringState { - MidCone() { + public MidCone() { super(IntakeConstants.OuttakeSpeeds.MID_CONE, "Mid Cone"); } } diff --git a/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/MidConeTipped.java b/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/MidConeTipped.java index f545e37..6be2495 100644 --- a/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/MidConeTipped.java +++ b/src/main/java/frc/robot/subsystems/intake/states/scoring/cone/MidConeTipped.java @@ -5,7 +5,7 @@ public class MidConeTipped extends ScoringState { - MidConeTipped() { + public MidConeTipped() { super(IntakeConstants.OuttakeSpeeds.MID_CONE_TIPPED, "Mid Cone Tipped"); } } diff --git a/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/HighCube.java b/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/HighCube.java index 3f1ce58..b260fd8 100644 --- a/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/HighCube.java +++ b/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/HighCube.java @@ -5,7 +5,7 @@ public class HighCube extends ScoringState { - HighCube() { + public HighCube() { super(IntakeConstants.OuttakeSpeeds.HIGH_CUBE, "High Cube"); } } diff --git a/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/HighCubeAuto.java b/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/HighCubeAuto.java index 0592eed..65d5964 100644 --- a/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/HighCubeAuto.java +++ b/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/HighCubeAuto.java @@ -5,7 +5,7 @@ public class HighCubeAuto extends ScoringState { - HighCubeAuto() { + public HighCubeAuto() { super(IntakeConstants.OuttakeSpeeds.HIGH_CUBE, "High Cube Auto"); } } diff --git a/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/MidCube.java b/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/MidCube.java index 40758a9..dea9eac 100644 --- a/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/MidCube.java +++ b/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/MidCube.java @@ -5,7 +5,7 @@ public class MidCube extends ScoringState { - MidCube() { + public MidCube() { super(IntakeConstants.OuttakeSpeeds.MID_CUBE, "Mid Cube"); } } diff --git a/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/MidCubeAuto.java b/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/MidCubeAuto.java index 022b8e6..75cc262 100644 --- a/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/MidCubeAuto.java +++ b/src/main/java/frc/robot/subsystems/intake/states/scoring/cube/MidCubeAuto.java @@ -5,7 +5,7 @@ public class MidCubeAuto extends ScoringState { - MidCubeAuto() { + public MidCubeAuto() { super(IntakeConstants.OuttakeSpeeds.MID_CUBE, "Mid Cube Auto"); } } diff --git a/src/main/java/frc/robot/subsystems/intake/states/scoring/level/Low.java b/src/main/java/frc/robot/subsystems/intake/states/scoring/level/Low.java index daad78d..de19cf1 100644 --- a/src/main/java/frc/robot/subsystems/intake/states/scoring/level/Low.java +++ b/src/main/java/frc/robot/subsystems/intake/states/scoring/level/Low.java @@ -5,7 +5,7 @@ public class Low extends ScoringState { - Low() { + public Low() { super(IntakeConstants.OuttakeSpeeds.LOW_CUBE, IntakeConstants.OuttakeSpeeds.LOW_CONE, "Low"); } } diff --git a/src/main/java/frc/robot/subsystems/intake/states/scoring/level/LowAuto.java b/src/main/java/frc/robot/subsystems/intake/states/scoring/level/LowAuto.java index 5135b0b..cf1cc1f 100644 --- a/src/main/java/frc/robot/subsystems/intake/states/scoring/level/LowAuto.java +++ b/src/main/java/frc/robot/subsystems/intake/states/scoring/level/LowAuto.java @@ -5,7 +5,7 @@ public class LowAuto extends ScoringState { - LowAuto() { + public LowAuto() { super( IntakeConstants.OuttakeSpeeds.LOW_CUBE_AUTO, IntakeConstants.OuttakeSpeeds.LOW_CONE_AUTO,