From 2c1ee1b3b80ad11cdfe2245aa2af13a4c03eef04 Mon Sep 17 00:00:00 2001 From: Sriman Achanta <68172138+srimanachanta@users.noreply.github.com> Date: Thu, 2 Nov 2023 22:47:56 -0400 Subject: [PATCH 1/2] Rewrite code for 2024 beta --- .github/workflows/build-robot-code.yml | 17 - .github/workflows/build.yml | 18 + .../{formatting-check.yml => lint-format.yml} | 7 +- .gitignore | 19 +- .vscode/launch.json | 17 + .vscode/settings.json | 41 ++ .wpilib/wpilib_preferences.json | 4 +- build.gradle | 102 +++-- gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 35 +- gradlew.bat | 1 + settings.gradle | 5 +- src/main/java/frc/lib/LoggedIO.java | 17 - src/main/java/frc/lib/LoggerUtil.java | 100 ----- src/main/java/frc/lib/PoseEstimator.java | 34 -- .../java/frc/lib/SparkMaxBurnManager.java | 67 --- .../frc/lib/SparkMaxPeriodicFrameConfig.java | 35 -- src/main/java/frc/robot/Main.java | 2 +- src/main/java/frc/robot/Robot.java | 79 ++-- src/main/java/frc/robot/RobotContainer.java | 149 +++--- .../commands/drive/DriveCommandFactory.java | 42 ++ .../drive/FeedForwardCharacterization.java | 106 +++++ .../java/frc/robot/constants/Constants.java | 146 +++--- .../frc/robot/constants/HardwareDevices.java | 35 -- .../java/frc/robot/constants/HardwareIds.java | 26 ++ src/main/java/frc/robot/drive/DriveBase.java | 176 -------- .../java/frc/robot/drive/SwerveModuleIO.java | 42 -- .../drive/SwerveModuleIOMK4DualSparkMax.java | 166 ------- .../frc/robot/drive/SwerveModuleIOSim.java | 46 -- .../robot/drive/commands/DriveControl.java | 32 -- .../java/frc/robot/drive/gyro/GyroIO.java | 38 -- .../frc/robot/drive/gyro/GyroIOPigeon2.java | 46 -- .../java/frc/robot/drive/gyro/GyroIOSim.java | 29 -- .../java/frc/robot/oi/DriverInterface.java | 13 - .../java/frc/robot/oi/DualJoystickDriver.java | 35 -- src/main/java/frc/robot/oi/OIManager.java | 20 - src/main/java/frc/robot/oi/PS4Driver.java | 33 -- src/main/java/frc/robot/oi/XboxDriver.java | 33 -- .../frc/robot/subsystems/drive/DriveBase.java | 148 ++++++ .../frc/robot/subsystems/drive/GyroIO.java | 27 ++ .../robot/subsystems/drive/GyroIOPigeon2.java | 83 ++++ .../frc/robot/subsystems/drive/Module.java | 188 ++++++++ .../frc/robot/subsystems/drive/ModuleIO.java | 38 ++ .../robot/subsystems/drive/ModuleIOSim.java | 61 +++ .../subsystems/drive/ModuleIOSparkMax.java | 135 ++++++ .../drive/SparkMaxOdometryThread.java | 59 +++ src/main/java/frc/robot/util/LoggerUtil.java | 27 ++ .../frc/robot/util/PolynomialRegression.java | 205 +++++++++ .../java/frc/robot/util/PoseEstimator.java | 33 ++ vendordeps/AdvantageKit.json | 81 ++-- vendordeps/Phoenix.json | 423 ------------------ vendordeps/Phoenix6.json | 339 ++++++++++++++ vendordeps/REVLib.json | 145 +++--- vendordeps/WPILibNewCommands.json | 75 ++-- 55 files changed, 2007 insertions(+), 1877 deletions(-) delete mode 100644 .github/workflows/build-robot-code.yml create mode 100644 .github/workflows/build.yml rename .github/workflows/{formatting-check.yml => lint-format.yml} (82%) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json delete mode 100644 src/main/java/frc/lib/LoggedIO.java delete mode 100644 src/main/java/frc/lib/LoggerUtil.java delete mode 100644 src/main/java/frc/lib/PoseEstimator.java delete mode 100644 src/main/java/frc/lib/SparkMaxBurnManager.java delete mode 100644 src/main/java/frc/lib/SparkMaxPeriodicFrameConfig.java create mode 100644 src/main/java/frc/robot/commands/drive/DriveCommandFactory.java create mode 100644 src/main/java/frc/robot/commands/drive/FeedForwardCharacterization.java delete mode 100644 src/main/java/frc/robot/constants/HardwareDevices.java create mode 100644 src/main/java/frc/robot/constants/HardwareIds.java delete mode 100644 src/main/java/frc/robot/drive/DriveBase.java delete mode 100644 src/main/java/frc/robot/drive/SwerveModuleIO.java delete mode 100644 src/main/java/frc/robot/drive/SwerveModuleIOMK4DualSparkMax.java delete mode 100644 src/main/java/frc/robot/drive/SwerveModuleIOSim.java delete mode 100644 src/main/java/frc/robot/drive/commands/DriveControl.java delete mode 100644 src/main/java/frc/robot/drive/gyro/GyroIO.java delete mode 100644 src/main/java/frc/robot/drive/gyro/GyroIOPigeon2.java delete mode 100644 src/main/java/frc/robot/drive/gyro/GyroIOSim.java delete mode 100644 src/main/java/frc/robot/oi/DriverInterface.java delete mode 100644 src/main/java/frc/robot/oi/DualJoystickDriver.java delete mode 100644 src/main/java/frc/robot/oi/OIManager.java delete mode 100644 src/main/java/frc/robot/oi/PS4Driver.java delete mode 100644 src/main/java/frc/robot/oi/XboxDriver.java create mode 100644 src/main/java/frc/robot/subsystems/drive/DriveBase.java create mode 100644 src/main/java/frc/robot/subsystems/drive/GyroIO.java create mode 100644 src/main/java/frc/robot/subsystems/drive/GyroIOPigeon2.java create mode 100644 src/main/java/frc/robot/subsystems/drive/Module.java create mode 100644 src/main/java/frc/robot/subsystems/drive/ModuleIO.java create mode 100644 src/main/java/frc/robot/subsystems/drive/ModuleIOSim.java create mode 100644 src/main/java/frc/robot/subsystems/drive/ModuleIOSparkMax.java create mode 100644 src/main/java/frc/robot/subsystems/drive/SparkMaxOdometryThread.java create mode 100644 src/main/java/frc/robot/util/LoggerUtil.java create mode 100644 src/main/java/frc/robot/util/PolynomialRegression.java create mode 100644 src/main/java/frc/robot/util/PoseEstimator.java delete mode 100644 vendordeps/Phoenix.json create mode 100644 vendordeps/Phoenix6.json diff --git a/.github/workflows/build-robot-code.yml b/.github/workflows/build-robot-code.yml deleted file mode 100644 index 2b283a1..0000000 --- a/.github/workflows/build-robot-code.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Build Robot Code - -on: - push: - -jobs: - build: - runs-on: ubuntu-latest - container: wpilib/roborio-cross-ubuntu:2023-22.04 - steps: - - uses: actions/checkout@v3 - - name: Add repository to git safe directories - run: git config --global --add safe.directory $GITHUB_WORKSPACE - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - name: Compile and run tests on robot code - run: ./gradlew build -x spotlessCheck \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..ba0370f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,18 @@ +name: Build + +on: + push: + pull_request: + +jobs: + build: + name: Build + runs-on: ubuntu-latest + container: wpilib/roborio-cross-ubuntu:2024-22.04 + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - name: Grant execute permission + run: chmod +x gradlew + - name: Build robot code + run: ./gradlew build diff --git a/.github/workflows/formatting-check.yml b/.github/workflows/lint-format.yml similarity index 82% rename from .github/workflows/formatting-check.yml rename to .github/workflows/lint-format.yml index 24214cf..6a3a84d 100644 --- a/.github/workflows/formatting-check.yml +++ b/.github/workflows/lint-format.yml @@ -1,7 +1,8 @@ -name: Check Formatting +name: Lint and Format on: push: + pull_request: concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} @@ -9,7 +10,7 @@ concurrency: jobs: javaformat: - name: "Check Java Format" + name: "Java Formatting" runs-on: ubuntu-22.04 container: wpilib/ubuntu-base:22.04 steps: @@ -20,4 +21,4 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Run Java format - run: ./gradlew spotlessCheck \ No newline at end of file + run: ./gradlew spotlessCheck diff --git a/.gitignore b/.gitignore index d45f8ce..0cce371 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# This gitignore has been specially created by the WPILib team. +# If you remove items from this file, intellisense might break. + ### C++ ### # Prerequisites *.d @@ -106,7 +109,6 @@ Temporary Items !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json -!.vscode/style.xml ### Windows ### # Windows thumbnail cache files @@ -156,12 +158,19 @@ gradle-app.setting .settings/ bin/ -.idea +# IntelliJ +*.iml +*.ipr +*.iws +.idea/ +out/ + +# Fleet +.fleet -# Simulation GUI and other tools window save files -*-window.json +# Simulation GUI and other tools window save file simgui*.json networktables.json # Version file -src/main/java/frc/generated \ No newline at end of file +src/main/java/frc/generated diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b8c1920 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "wpilib", + "name": "WPILib Desktop Debug", + "request": "launch", + "desktop": true + }, + { + "type": "wpilib", + "name": "WPILib roboRIO Debug", + "request": "launch", + "desktop": false + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6543015 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,41 @@ +{ + "java.configuration.updateBuildConfiguration": "automatic", + "java.server.launchMode": "Standard", + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "bin/": true, + "**/.classpath": true, + "**/.project": true, + "**/.settings": true, + "**/.factorypath": true, + "**/*~": true + }, + "java.test.config": [ + { + "name": "WPIlibUnitTests", + "workingDirectory": "${workspaceFolder}/build/jni/release", + "vmargs": [ + "-Djava.library.path=${workspaceFolder}/build/jni/release" + ], + "env": { + "LD_LIBRARY_PATH": "${workspaceFolder}/build/jni/release", + "DYLD_LIBRARY_PATH": "${workspaceFolder}/build/jni/release" + } + }, + null + ], + "java.test.defaultConfig": "WPIlibUnitTests", + "spotlessGradle.format.enable": true, + "spotlessGradle.diagnostics.enable": false, + "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle", + "[json]": { + "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle" + }, + "[java]": { + "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle" + } +} diff --git a/.wpilib/wpilib_preferences.json b/.wpilib/wpilib_preferences.json index 5bd203a..2a7d784 100644 --- a/.wpilib/wpilib_preferences.json +++ b/.wpilib/wpilib_preferences.json @@ -1,6 +1,6 @@ { "enableCppIntellisense": false, "currentLanguage": "java", - "projectYear": "2023", + "projectYear": "2024beta", "teamNumber": 540 -} \ No newline at end of file +} diff --git a/build.gradle b/build.gradle index e5151f0..c9beb0e 100755 --- a/build.gradle +++ b/build.gradle @@ -1,31 +1,37 @@ -//file:noinspection DependencyNotationArgument -import edu.wpi.first.gradlerio.GradleRIOPlugin -import groovy.json.JsonSlurper - plugins { id "java" - id "edu.wpi.first.GradleRIO" version "2023.4.3" - id 'com.diffplug.spotless' version "6.12.0" + id "edu.wpi.first.GradleRIO" version "2024.1.1-beta-3" id "com.peterabeles.gversion" version "1.10" + id "com.diffplug.spotless" version "6.12.0" } -sourceCompatibility = JavaVersion.VERSION_17 -targetCompatibility = JavaVersion.VERSION_17 +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} def ROBOT_MAIN_CLASS = "frc.robot.Main" +// Define my targets (RoboRIO) and artifacts (deployable files) +// This is added by GradleRIO's backing project DeployUtils. deploy { targets { roborio(getTargetTypeClass('RoboRIO')) { + // Team number is loaded either from the .wpilib/wpilib_preferences.json + // or from command line. If not found an exception will be thrown. + // You can use getTeamOrDefault(team) instead of getTeamNumber if you + // want to store a team number in this file. team = project.frc.getTeamOrDefault(540) debug = project.frc.getDebugOrDefault(false) artifacts { + // First part is artifact name, 2nd is artifact type + // getTargetTypeClass is a shortcut to get the class type using a string + frcJava(getArtifactTypeClass('FRCJavaArtifact')) { - gcType = edu.wpi.first.gradlerio.deploy.roborio.GarbageCollectorType.Other - jvmArgs << '-XX:+UseG1GC' } + // Static files artifact frcStaticFileDeploy(getArtifactTypeClass('FileTreeArtifact')) { files = project.fileTree('src/main/deploy') directory = '/home/lvuser/deploy' @@ -37,12 +43,14 @@ deploy { def deployArtifact = deploy.targets.roborio.artifacts.frcJava +// Set to true to use debug for JNI. wpi.java.debugJni = false +// Set this to true to enable desktop support. def includeDesktopSupport = true +// Configuration for AdvantageKit repositories { - mavenLocal() maven { url = uri("https://maven.pkg.github.com/Mechanical-Advantage/AdvantageKit") credentials { @@ -50,12 +58,15 @@ repositories { password = "\u0067\u0068\u0070\u005f\u006e\u0056\u0051\u006a\u0055\u004f\u004c\u0061\u0079\u0066\u006e\u0078\u006e\u0037\u0051\u0049\u0054\u0042\u0032\u004c\u004a\u006d\u0055\u0070\u0073\u0031\u006d\u0037\u004c\u005a\u0030\u0076\u0062\u0070\u0063\u0051" } } + mavenLocal() } -configurations.configureEach { +configurations.all { exclude group: "edu.wpi.first.wpilibj" } +// Defining my dependencies. In this case, WPILib (+ friends), and vendor libraries. +// Also defines JUnit 4. dependencies { implementation wpi.java.deps.wpilib() implementation wpi.java.vendor.java() @@ -74,12 +85,14 @@ dependencies { nativeRelease wpi.java.vendor.jniRelease(wpi.platforms.desktop) simulationRelease wpi.sim.enableRelease() - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2' - testImplementation 'org.junit.jupiter:junit-jupiter-params:5.4.2' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.2' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' + + implementation "gov.nist.math:jama:1.0.3" - def AdvantageKitJSON = new JsonSlurper().parseText(new File(projectDir.getAbsolutePath() + "/vendordeps/AdvantageKit.json").text) - annotationProcessor "org.littletonrobotics.akit.junction:junction-autolog:$AdvantageKitJSON.version" + def akitJson = new groovy.json.JsonSlurper().parseText(new File(projectDir.getAbsolutePath() + "/vendordeps/AdvantageKit.json").text) + annotationProcessor "org.littletonrobotics.akit.junction:junction-autolog:$akitJson.version" } test { @@ -87,27 +100,31 @@ test { systemProperty 'junit.jupiter.extensions.autodetection.enabled', 'true' } -wpi.sim.addGui() +// Simulation configuration (e.g. environment variables). +wpi.sim.addGui().defaultEnabled = true wpi.sim.addDriverstation() +// Setting up my Jar File. In this case, adding all libraries into the main jar ('fat jar') +// in order to make them all available at runtime. Also adding the manifest so WPILib +// knows where to look for our Robot Class. jar { - from { - configurations.runtimeClasspath.collect { - it.isDirectory() ? it : zipTree(it) - } - } - manifest GradleRIOPlugin.javaManifest(ROBOT_MAIN_CLASS) + 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 } +// Configure jar and deploy tasks deployArtifact.jarTask = jar wpi.java.configureExecutableTasks(jar) wpi.java.configureTestTasks(test) -tasks.withType(JavaCompile).configureEach { +// Configure string concat to always inline compile +tasks.withType(JavaCompile) { options.compilerArgs.add '-XDstringConcat=inline' } +// Create version file project.compileJava.dependsOn(createVersionFile) gversion { srcDir = "src/main/java/" @@ -118,11 +135,13 @@ gversion { indent = " " } +// Spotless formatting +project.compileJava.dependsOn(spotlessApply) spotless { java { - target fileTree('.') { - include '**/*.java' - exclude '**/build/**', '**/build-*/**', '.idea', '**/generated/**' + target fileTree(".") { + include "**/*.java" + exclude "**/build/**", "**/build-*/**" } toggleOffOn() googleJavaFormat() @@ -131,29 +150,26 @@ spotless { endWithNewline() } groovyGradle { - target fileTree('.') { - include '**/*.gradle' - exclude '**/build/**', '**/build-*/**', '.idea', '**/generated/**' + target fileTree(".") { + include "**/*.gradle" + exclude "**/build/**", "**/build-*/**" } greclipse() indentWithSpaces(4) trimTrailingWhitespace() endWithNewline() } - format 'xml', { - target fileTree('.') { - include '**/*.xml' - exclude '**/build/**', '**/build-*/**', '.idea', '**/generated/**' + json { + target fileTree(".") { + include "**/*.json" + exclude "**/build/**", "**/build-*/**" } - eclipseWtp('xml') - trimTrailingWhitespace() - indentWithSpaces(2) - endWithNewline() + gson().indentWithSpaces(2) } - format 'misc', { - target fileTree('.') { - include '**/*.md', '**/.gitignore' - exclude '**/build/**', '**/build-*/**', '.idea', '**/generated/**' + format "misc", { + target fileTree(".") { + include "**/*.md", "**/.gitignore" + exclude "**/build/**", "**/build-*/**" } trimTrailingWhitespace() indentWithSpaces(2) 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 53a6b23..6689b85 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/src/main/java/frc/lib/LoggedIO.java b/src/main/java/frc/lib/LoggedIO.java deleted file mode 100644 index fee545f..0000000 --- a/src/main/java/frc/lib/LoggedIO.java +++ /dev/null @@ -1,17 +0,0 @@ -package frc.lib; - -/** - * Represents an IO interface that can control a {@link - * org.littletonrobotics.junction.inputs.LoggableInputs}. - * - * @param Loggable inputs that are updated. - */ -public interface LoggedIO { - - /** - * Update the inputs with the current state of the IO. - * - * @param inputs inputs to update. - */ - default void updateInputs(T inputs) {} -} diff --git a/src/main/java/frc/lib/LoggerUtil.java b/src/main/java/frc/lib/LoggerUtil.java deleted file mode 100644 index 246ac68..0000000 --- a/src/main/java/frc/lib/LoggerUtil.java +++ /dev/null @@ -1,100 +0,0 @@ -package frc.lib; - -import edu.wpi.first.networktables.ConnectionInfo; -import edu.wpi.first.networktables.NetworkTableInstance; -import edu.wpi.first.wpilibj.RobotBase; -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.CommandScheduler; -import frc.generated.BuildConstants; -import frc.robot.constants.Constants; -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.BiConsumer; -import org.littletonrobotics.junction.Logger; - -/** Utilities used by the AdvantageKit Logger. */ -@SuppressWarnings("DataFlowIssue") -public class LoggerUtil { - /** - * Initialize the Logger with the auto-generated data from the build. - * - * @param logger logger to update. - */ - public static void initializeLoggerMetadata(Logger logger) { - // Record metadata from generated state file. - logger.recordMetadata("ROBOT_NAME", Constants.getRobotType().toString()); - logger.recordMetadata("RUNTIME_ENVIRONMENT", RobotBase.getRuntimeType().toString()); - logger.recordMetadata("PROJECT_NAME", BuildConstants.MAVEN_NAME); - logger.recordMetadata("BUILD_DATE", BuildConstants.BUILD_DATE); - logger.recordMetadata("GIT_SHA", BuildConstants.GIT_SHA); - logger.recordMetadata("GIT_DATE", BuildConstants.GIT_DATE); - logger.recordMetadata("GIT_BRANCH", BuildConstants.GIT_BRANCH); - - // Set the current GIT state of the robot (helps manage the logs that are saved). - switch (BuildConstants.DIRTY) { - case 0 -> logger.recordMetadata("GIT_STATUS", "All changes committed"); - case 1 -> logger.recordMetadata("GIT_STATUS", "Uncommitted changes"); - default -> logger.recordMetadata("GIT_STATUS", "Unknown"); - } - } - - /** - * Get the path of the USB drive if it is plugged into the roboRIO. If one isn't found, it will - * return null. - * - * @return path of the USB drive. - */ - public static String getUSBPath() { - // Return the path of the USB drive it is plugged in, else, return null. - try { - Path drivePath = Path.of("/u").toRealPath(); - return drivePath.toString(); - } catch (IOException e) { - return null; - } - } - - /** - * Log all the currently connected NetworkTables clients. This method should be called - * periodically. - */ - public static void logNTClients() { - List clientNames = new ArrayList<>(); - List clientAddresses = new ArrayList<>(); - - for (ConnectionInfo client : NetworkTableInstance.getDefault().getConnections()) { - clientNames.add(client.remote_id); - clientAddresses.add(client.remote_ip); - } - - Logger.getInstance().recordOutput("NTClients/Names", clientNames.toArray(new String[0])); - Logger.getInstance() - .recordOutput("NTClients/Addresses", clientAddresses.toArray(new String[0])); - } - - /** Log Commands scheduled by the CommandScheduler to telemetry automatically. */ - public static void initCommandLogging() { - // Log active commands - Map commandCounts = new HashMap<>(); - BiConsumer logCommandFunction = - (Command command, Boolean active) -> { - String name = command.getName(); - int count = commandCounts.getOrDefault(name, 0) + (active ? 1 : -1); - commandCounts.put(name, count); - Logger.getInstance() - .recordOutput( - "CommandsUnique/" + name + "_" + Integer.toHexString(command.hashCode()), active); - Logger.getInstance().recordOutput("CommandsAll/" + name, count > 0); - }; - CommandScheduler.getInstance() - .onCommandInitialize((Command command) -> logCommandFunction.accept(command, true)); - CommandScheduler.getInstance() - .onCommandFinish((Command command) -> logCommandFunction.accept(command, false)); - CommandScheduler.getInstance() - .onCommandInterrupt((Command command) -> logCommandFunction.accept(command, false)); - } -} diff --git a/src/main/java/frc/lib/PoseEstimator.java b/src/main/java/frc/lib/PoseEstimator.java deleted file mode 100644 index 7aa2521..0000000 --- a/src/main/java/frc/lib/PoseEstimator.java +++ /dev/null @@ -1,34 +0,0 @@ -package frc.lib; - -import edu.wpi.first.math.estimator.SwerveDrivePoseEstimator; -import edu.wpi.first.math.geometry.Pose2d; -import edu.wpi.first.math.geometry.Rotation2d; -import edu.wpi.first.math.kinematics.SwerveDriveKinematics; -import edu.wpi.first.math.kinematics.SwerveModulePosition; - -public class PoseEstimator extends SwerveDrivePoseEstimator { - private static PoseEstimator instance; - - public static void createInstance( - SwerveDriveKinematics kinematics, - Rotation2d gyroHeading, - SwerveModulePosition... modulePositions) { - instance = new PoseEstimator(kinematics, gyroHeading, modulePositions); - } - - public static PoseEstimator getInstance() { - if (instance == null) { - throw new IllegalStateException( - "The PoseEstimator hasn't been created yet. Create it first using PoseEstimator::createInstance"); - } else { - return instance; - } - } - - private PoseEstimator( - SwerveDriveKinematics kinematics, - Rotation2d gyroHeading, - SwerveModulePosition... modulePositions) { - super(kinematics, gyroHeading, modulePositions, new Pose2d()); - } -} diff --git a/src/main/java/frc/lib/SparkMaxBurnManager.java b/src/main/java/frc/lib/SparkMaxBurnManager.java deleted file mode 100644 index 43801ae..0000000 --- a/src/main/java/frc/lib/SparkMaxBurnManager.java +++ /dev/null @@ -1,67 +0,0 @@ -package frc.lib; - -import edu.wpi.first.wpilibj.RobotBase; -import frc.generated.BuildConstants; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; - -/** - * SparkMax's memory, like most NAND based memory, has a limited number of write commands it can - * sustain, in order to prevent this from hitting that maximum, we can check if the code was even - * changed, and if so, burn the new settings to the SparkMax. - */ -public class SparkMaxBurnManager { - public static final int kConfigurationStatusTimeoutMs = 500; - private static final String buildDateFile = "/home/lvuser/build-date.txt"; - private static boolean shouldBurn = false; - - private SparkMaxBurnManager() {} - - public static void checkBuildStatus() { - // TODO, make sure read and write functions work - if (RobotBase.isSimulation()) { - shouldBurn = false; - return; - } - - File file = new File(buildDateFile); - if (!file.exists()) { - // No build date file, burn flash - shouldBurn = true; - } else { - // Read previous build date - String previousBuildDate = ""; - try { - previousBuildDate = Files.readString(Paths.get(buildDateFile)); - } catch (IOException error) { - error.printStackTrace(); - } - - shouldBurn = !previousBuildDate.equals(BuildConstants.BUILD_DATE); - } - - // Write the current Build Time - try { - FileWriter fileWriter = new FileWriter(buildDateFile); - fileWriter.write(BuildConstants.BUILD_DATE); - fileWriter.close(); - } catch (IOException error) { - error.printStackTrace(); - } - - if (shouldBurn) { - System.out.println( - "[SparkMaxBurnManager] Build date changed, allowing SparkMaxes to burn flash"); - } else { - System.out.println( - "[SparkMaxBurnManager] Build date unchanged, will not burn SparkMax flash"); - } - } - - public static boolean shouldBurnFlash() { - return shouldBurn; - } -} diff --git a/src/main/java/frc/lib/SparkMaxPeriodicFrameConfig.java b/src/main/java/frc/lib/SparkMaxPeriodicFrameConfig.java deleted file mode 100644 index f0554de..0000000 --- a/src/main/java/frc/lib/SparkMaxPeriodicFrameConfig.java +++ /dev/null @@ -1,35 +0,0 @@ -package frc.lib; - -import com.revrobotics.CANSparkMax; -import com.revrobotics.CANSparkMaxLowLevel.PeriodicFrame; - -/** - * Configures the rates at which different status frames are sent from SparkMaxes. See here - * for different status frames. - */ -public class SparkMaxPeriodicFrameConfig { - public static void configureLeader(CANSparkMax motorController) { - motorController.setPeriodicFramePeriod(PeriodicFrame.kStatus0, 10); - motorController.setPeriodicFramePeriod(PeriodicFrame.kStatus3, 65535); - motorController.setPeriodicFramePeriod(PeriodicFrame.kStatus4, 65535); - motorController.setPeriodicFramePeriod(PeriodicFrame.kStatus5, 65535); - motorController.setPeriodicFramePeriod(PeriodicFrame.kStatus6, 65535); - } - - public static void configureFollower(CANSparkMax motorController) { - motorController.setPeriodicFramePeriod(PeriodicFrame.kStatus0, 10); - motorController.setPeriodicFramePeriod(PeriodicFrame.kStatus3, 65535); - motorController.setPeriodicFramePeriod(PeriodicFrame.kStatus4, 65535); - motorController.setPeriodicFramePeriod(PeriodicFrame.kStatus5, 65535); - motorController.setPeriodicFramePeriod(PeriodicFrame.kStatus6, 65535); - } - - public static void configureIsolated(CANSparkMax motorController) { - motorController.setPeriodicFramePeriod(PeriodicFrame.kStatus0, 100); - motorController.setPeriodicFramePeriod(PeriodicFrame.kStatus3, 65535); - motorController.setPeriodicFramePeriod(PeriodicFrame.kStatus4, 65535); - motorController.setPeriodicFramePeriod(PeriodicFrame.kStatus5, 65535); - motorController.setPeriodicFramePeriod(PeriodicFrame.kStatus6, 65535); - } -} diff --git a/src/main/java/frc/robot/Main.java b/src/main/java/frc/robot/Main.java index 9b6d81d..8e389ea 100644 --- a/src/main/java/frc/robot/Main.java +++ b/src/main/java/frc/robot/Main.java @@ -5,7 +5,7 @@ public final class Main { private Main() {} - public static void main(String[] args) { + public static void main(String... args) { RobotBase.startRobot(Robot::new); } } diff --git a/src/main/java/frc/robot/Robot.java b/src/main/java/frc/robot/Robot.java index 7ec48b2..23e5b28 100644 --- a/src/main/java/frc/robot/Robot.java +++ b/src/main/java/frc/robot/Robot.java @@ -1,92 +1,67 @@ package frc.robot; -import edu.wpi.first.hal.AllianceStationID; -import edu.wpi.first.wpilibj.DriverStation; -import edu.wpi.first.wpilibj.simulation.DriverStationSim; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.CommandScheduler; -import frc.lib.LoggerUtil; import frc.robot.constants.Constants; +import frc.robot.util.LoggerUtil; import org.littletonrobotics.junction.LogFileUtil; import org.littletonrobotics.junction.LoggedRobot; import org.littletonrobotics.junction.Logger; -import org.littletonrobotics.junction.inputs.LoggedDriverStation; -import org.littletonrobotics.junction.inputs.LoggedPowerDistribution; -import org.littletonrobotics.junction.inputs.LoggedSystemStats; import org.littletonrobotics.junction.networktables.NT4Publisher; import org.littletonrobotics.junction.wpilog.WPILOGReader; import org.littletonrobotics.junction.wpilog.WPILOGWriter; public class Robot extends LoggedRobot { - private RobotContainer m_robotContainer; - private Command m_autonomousCommand; + private Command autonomousCommand; + private RobotContainer robotContainer; @Override public void robotInit() { - Logger logger = Logger.getInstance(); - - LoggerUtil.initializeLoggerMetadata(logger); + LoggerUtil.initializeLoggerMetadata(); // Set up data receivers & replay source switch (Constants.getRobotMode()) { - case REAL -> { - String path = LoggerUtil.getUSBPath(); - - if (path != null) { - logger.addDataReceiver(new WPILOGWriter(path)); - } else { - DriverStation.reportWarning( - "Unable to locate a USB drive plugged into the roboRIO. Hardware storage logging will be disabled.", - false); - } - - logger.addDataReceiver(new NT4Publisher()); - - LoggedPowerDistribution.getInstance(); - LoggedDriverStation.getInstance(); - LoggedSystemStats.getInstance(); - } - case SIM -> logger.addDataReceiver(new NT4Publisher()); - case REPLAY -> { - setUseTiming(false); - String path = LogFileUtil.findReplayLog(); - logger.setReplaySource(new WPILOGReader(path)); - logger.addDataReceiver(new WPILOGWriter(LogFileUtil.addPathSuffix(path, "_sim"))); - } + case REAL: + // Running on a real robot, log to a USB stick + Logger.addDataReceiver(new WPILOGWriter("/U")); + Logger.addDataReceiver(new NT4Publisher()); + break; + case SIM: + // Running a physics simulator, log to NT + Logger.addDataReceiver(new NT4Publisher()); + break; + case REPLAY: + // Replaying a log, set up replay source + setUseTiming(false); // Run as fast as possible + String logPath = LogFileUtil.findReplayLog(); + Logger.setReplaySource(new WPILOGReader(logPath)); + Logger.addDataReceiver(new WPILOGWriter(LogFileUtil.addPathSuffix(logPath, "_sim"))); + break; } - // Start AdvantageKit logger - logger.start(); + Logger.start(); - LoggerUtil.initCommandLogging(); - - if (Constants.getRobotMode() == Constants.RobotMode.SIM) - DriverStationSim.setAllianceStationId(AllianceStationID.Blue1); - - m_robotContainer = new RobotContainer(); + robotContainer = new RobotContainer(); } @Override public void robotPeriodic() { CommandScheduler.getInstance().run(); - if (Constants.kAdvancedLoggingEnabled) { - LoggerUtil.logNTClients(); - } } @Override public void autonomousInit() { - m_autonomousCommand = m_robotContainer.getAutonomousCommand(); + autonomousCommand = robotContainer.getAutonomousCommand(); - if (m_autonomousCommand != null) { - m_autonomousCommand.schedule(); + if (autonomousCommand != null) { + autonomousCommand.schedule(); } } @Override public void teleopInit() { - if (m_autonomousCommand != null) { - m_autonomousCommand.cancel(); + if (autonomousCommand != null) { + autonomousCommand.cancel(); } } diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java index 1ed66a1..b87aeec 100644 --- a/src/main/java/frc/robot/RobotContainer.java +++ b/src/main/java/frc/robot/RobotContainer.java @@ -1,98 +1,91 @@ package frc.robot; import edu.wpi.first.wpilibj2.command.Command; -import frc.lib.SparkMaxBurnManager; +import edu.wpi.first.wpilibj2.command.Commands; +import edu.wpi.first.wpilibj2.command.button.CommandPS4Controller; +import frc.robot.commands.drive.DriveCommandFactory; import frc.robot.constants.Constants; -import frc.robot.constants.HardwareDevices; -import frc.robot.constants.HardwareDevices.SwerveBase.Drivetrain.*; -import frc.robot.drive.*; -import frc.robot.drive.commands.DriveControl; -import frc.robot.drive.gyro.GyroIO; -import frc.robot.drive.gyro.GyroIOPigeon2; -import frc.robot.drive.gyro.GyroIOSim; -import frc.robot.oi.OIManager; +import frc.robot.constants.HardwareIds; +import frc.robot.subsystems.drive.*; +import frc.robot.subsystems.drive.DriveBase; +import org.littletonrobotics.junction.networktables.LoggedDashboardChooser; public class RobotContainer { - private DriveBase m_driveBase; + // Subsystems + private final DriveBase m_drive; - private final OIManager m_OIManager = new OIManager(); + // Controller + private final CommandPS4Controller controller = new CommandPS4Controller(0); - public RobotContainer() { - SparkMaxBurnManager.checkBuildStatus(); + // Dashboard inputs + private final LoggedDashboardChooser autoChooser = + new LoggedDashboardChooser<>("Auto Choices"); - if (Constants.getRobotMode() != Constants.RobotMode.REPLAY) { - switch (Constants.getRobotType()) { - case ROBOT_SWERVE -> { - m_driveBase = - new DriveBase( - new GyroIOPigeon2(HardwareDevices.SwerveBase.kGyroId), - // FRONT LEFT - new SwerveModuleIOMK4DualSparkMax( - FrontLeft.kDriveMotorId, - FrontLeft.kSteerMotorId, - FrontLeft.kAbsoluteEncoderId, - Constants.Drivetrain.kSteerMotorInverted, - Constants.Drivetrain.kDriveMotorConversionFactor, - Constants.Drivetrain.kSteerMotorConversionFactor, - Constants.Drivetrain.FrontLeft.kMagneticOffsetDegrees), - // FRONT RIGHT - new SwerveModuleIOMK4DualSparkMax( - FrontRight.kDriveMotorId, - FrontRight.kSteerMotorId, - FrontRight.kAbsoluteEncoderId, - Constants.Drivetrain.kSteerMotorInverted, - Constants.Drivetrain.kDriveMotorConversionFactor, - Constants.Drivetrain.kSteerMotorConversionFactor, - Constants.Drivetrain.FrontRight.kMagneticOffsetDegrees), - // BACK LEFT - new SwerveModuleIOMK4DualSparkMax( - BackLeft.kDriveMotorId, - BackLeft.kSteerMotorId, - BackLeft.kAbsoluteEncoderId, - Constants.Drivetrain.kSteerMotorInverted, - Constants.Drivetrain.kDriveMotorConversionFactor, - Constants.Drivetrain.kSteerMotorConversionFactor, - Constants.Drivetrain.BackLeft.kMagneticOffsetDegrees), - // BACK RIGHT - new SwerveModuleIOMK4DualSparkMax( - BackRight.kDriveMotorId, - BackRight.kSteerMotorId, - BackRight.kAbsoluteEncoderId, - Constants.Drivetrain.kSteerMotorInverted, - Constants.Drivetrain.kDriveMotorConversionFactor, - Constants.Drivetrain.kSteerMotorConversionFactor, - Constants.Drivetrain.BackRight.kMagneticOffsetDegrees)); - } - case ROBOT_SIMBOT -> { - m_driveBase = - new DriveBase( - new GyroIOSim(), - new SwerveModuleIOSim(), - new SwerveModuleIOSim(), - new SwerveModuleIOSim(), - new SwerveModuleIOSim()); - } + public RobotContainer() { + switch (Constants.getRobotMode()) { + case REAL -> { + m_drive = + new DriveBase( + new GyroIOPigeon2(HardwareIds.kPigeonId), + new ModuleIOSparkMax( + HardwareIds.kFrontLeftDriveId, + HardwareIds.kFrontLeftTurnId, + HardwareIds.kFrontLeftEncoderId), + new ModuleIOSparkMax( + HardwareIds.kFrontRightDriveId, + HardwareIds.kFrontRightTurnId, + HardwareIds.kFrontRightEncoderId), + new ModuleIOSparkMax( + HardwareIds.kBackLeftDriveId, + HardwareIds.kBackLeftTurnId, + HardwareIds.kBackLeftEncoderId), + new ModuleIOSparkMax( + HardwareIds.kBackRightDriveId, + HardwareIds.kBackRightTurnId, + HardwareIds.kBackRightEncoderId)); + } + case SIM -> { + m_drive = + new DriveBase( + new GyroIO() {}, + new ModuleIOSim(), + new ModuleIOSim(), + new ModuleIOSim(), + new ModuleIOSim()); + } + default -> { + m_drive = + new DriveBase( + new GyroIO() {}, + new ModuleIO() {}, + new ModuleIO() {}, + new ModuleIO() {}, + new ModuleIO() {}); } } - m_driveBase = - m_driveBase != null - ? m_driveBase - : new DriveBase( - new GyroIO() {}, - new SwerveModuleIO() {}, - new SwerveModuleIO() {}, - new SwerveModuleIO() {}, - new SwerveModuleIO() {}); + // // Set up FF characterization routines + // autoChooser.addOption( + // "DriveBase FF Characterization", + // new FeedForwardCharacterization( + // m_drive, m_drive::runCharacterizationVolts, m_drive::getCharacterizationVelocity)); - configureBindings(); + // Configure the button bindings + configureButtonBindings(); } - private void configureBindings() { - m_driveBase.setDefaultCommand(new DriveControl(m_driveBase, m_OIManager.getDriverInterface())); + private void configureButtonBindings() { + m_drive.setDefaultCommand( + DriveCommandFactory.joystickDrive( + m_drive, + () -> -controller.getLeftY(), + () -> -controller.getLeftX(), + () -> -controller.getRightX(), + 0.1)); + controller.cross().onTrue(Commands.runOnce(m_drive::stopWithX, m_drive)); } public Command getAutonomousCommand() { - return null; + return autoChooser.get(); } } diff --git a/src/main/java/frc/robot/commands/drive/DriveCommandFactory.java b/src/main/java/frc/robot/commands/drive/DriveCommandFactory.java new file mode 100644 index 0000000..9250678 --- /dev/null +++ b/src/main/java/frc/robot/commands/drive/DriveCommandFactory.java @@ -0,0 +1,42 @@ +package frc.robot.commands.drive; + +import edu.wpi.first.math.MathUtil; +import edu.wpi.first.math.kinematics.ChassisSpeeds; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; +import frc.robot.constants.Constants; +import frc.robot.subsystems.drive.DriveBase; +import java.util.function.DoubleSupplier; + +public class DriveCommandFactory { + public static Command joystickDrive( + DriveBase driveBase, + DoubleSupplier xSupplier, + DoubleSupplier ySupplier, + DoubleSupplier omegaSupplier, + double deadband) { + return Commands.run( + () -> { + double x_val = xSupplier.getAsDouble(); + double y_val = ySupplier.getAsDouble(); + double omega_val = omegaSupplier.getAsDouble(); + + double x = Math.copySign(Math.pow(MathUtil.applyDeadband(x_val, deadband), 2), x_val); + double y = Math.copySign(Math.pow(MathUtil.applyDeadband(y_val, deadband), 2), y_val); + double omega = + Math.copySign(Math.pow(MathUtil.applyDeadband(omega_val, deadband), 2), omega_val); + + driveBase.runVelocity( + // ChassisSpeeds.fromFieldRelativeSpeeds( + // x * Constants.Drivetrain.kMaxLinearVelocityMetersPerSecond, + // y * Constants.Drivetrain.kMaxLinearVelocityMetersPerSecond, + // omega * Constants.Drivetrain.kMaxLAngularVelocityRadiansPerSecond, + // PoseEstimator.getInstance().getPose().getRotation()) + new ChassisSpeeds( + x * Constants.Drivetrain.kMaxLinearVelocityMetersPerSecond, + y * Constants.Drivetrain.kMaxLinearVelocityMetersPerSecond, + omega * Constants.Drivetrain.kMaxLAngularVelocityRadiansPerSecond)); + }, + driveBase); + } +} diff --git a/src/main/java/frc/robot/commands/drive/FeedForwardCharacterization.java b/src/main/java/frc/robot/commands/drive/FeedForwardCharacterization.java new file mode 100644 index 0000000..0fd234f --- /dev/null +++ b/src/main/java/frc/robot/commands/drive/FeedForwardCharacterization.java @@ -0,0 +1,106 @@ +// Copyright 2021-2023 FRC 6328 +// http://github.com/Mechanical-Advantage +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// version 3 as published by the Free Software Foundation or +// available in the root directory of this project. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +package frc.robot.commands.drive; + +import edu.wpi.first.wpilibj.Timer; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Subsystem; +import frc.robot.util.PolynomialRegression; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class FeedForwardCharacterization extends Command { + private static final double START_DELAY_SECS = 2.0; + private static final double RAMP_VOLTS_PER_SEC = 0.1; + + private FeedForwardCharacterizationData data; + private final Consumer voltageConsumer; + private final Supplier velocitySupplier; + + private final Timer timer = new Timer(); + + /** Creates a new FeedForwardCharacterization command. */ + public FeedForwardCharacterization( + Subsystem subsystem, Consumer voltageConsumer, Supplier velocitySupplier) { + addRequirements(subsystem); + this.voltageConsumer = voltageConsumer; + this.velocitySupplier = velocitySupplier; + } + + // Called when the command is initially scheduled. + @Override + public void initialize() { + data = new FeedForwardCharacterizationData(); + timer.reset(); + timer.start(); + } + + // Called every time the scheduler runs while the command is scheduled. + @Override + public void execute() { + if (timer.get() < START_DELAY_SECS) { + voltageConsumer.accept(0.0); + } else { + double voltage = (timer.get() - START_DELAY_SECS) * RAMP_VOLTS_PER_SEC; + voltageConsumer.accept(voltage); + data.add(velocitySupplier.get(), voltage); + } + } + + // Called once the command ends or is interrupted. + @Override + public void end(boolean interrupted) { + voltageConsumer.accept(0.0); + timer.stop(); + data.print(); + } + + // Returns true when the command should end. + @Override + public boolean isFinished() { + return false; + } + + public static class FeedForwardCharacterizationData { + private final List velocityData = new LinkedList<>(); + private final List voltageData = new LinkedList<>(); + + public void add(double velocity, double voltage) { + if (Math.abs(velocity) > 1E-4) { + velocityData.add(Math.abs(velocity)); + voltageData.add(Math.abs(voltage)); + } + } + + public void print() { + if (velocityData.size() == 0 || voltageData.size() == 0) { + return; + } + + PolynomialRegression regression = + new PolynomialRegression( + velocityData.stream().mapToDouble(Double::doubleValue).toArray(), + voltageData.stream().mapToDouble(Double::doubleValue).toArray(), + 1); + + System.out.println("FF Characterization Results:"); + System.out.println("\tCount=" + Integer.toString(velocityData.size()) + ""); + System.out.println(String.format("\tR2=%.5f", regression.R2())); + System.out.println(String.format("\tkS=%.5f", regression.beta(0))); + System.out.println(String.format("\tkV=%.5f", regression.beta(1))); + } + } +} diff --git a/src/main/java/frc/robot/constants/Constants.java b/src/main/java/frc/robot/constants/Constants.java index 9781300..dd72731 100644 --- a/src/main/java/frc/robot/constants/Constants.java +++ b/src/main/java/frc/robot/constants/Constants.java @@ -1,6 +1,5 @@ package frc.robot.constants; -import com.revrobotics.CANSparkMax; import edu.wpi.first.math.geometry.Translation2d; import edu.wpi.first.math.kinematics.SwerveDriveKinematics; import edu.wpi.first.math.util.Units; @@ -8,31 +7,27 @@ import edu.wpi.first.wpilibj.RobotBase; public final class Constants { - /** - * Whether advanced logging should be enabled. This can be disabled if there is too much going on. - */ - public static final boolean kAdvancedLoggingEnabled = true; - private static RobotType kRobotType = RobotType.ROBOT_SIMBOT; - public static final double loopPeriodSecs = 0.02; + + public static double kLoopPeriodSecs = 0.02; public enum RobotMode { REAL, - REPLAY, - SIM + SIM, + REPLAY } public enum RobotType { - ROBOT_SWERVE, + ROBOT_2023_OFFSEASON_SWERVE, ROBOT_SIMBOT } public static RobotType getRobotType() { if (RobotBase.isReal() && kRobotType == RobotType.ROBOT_SIMBOT) { DriverStation.reportError( - "Robot is set to SIM but it isn't a SIM, setting it to Swerve (Real) Robot as redundancy.", + "Robot is set to SIM but it isn't a SIM, setting it to Competition Robot as redundancy.", false); - kRobotType = RobotType.ROBOT_SWERVE; + kRobotType = RobotType.ROBOT_2023_OFFSEASON_SWERVE; } if (RobotBase.isSimulation() && kRobotType != RobotType.ROBOT_SIMBOT) { @@ -46,96 +41,63 @@ public static RobotType getRobotType() { public static RobotMode getRobotMode() { return switch (getRobotType()) { - case ROBOT_SWERVE -> RobotBase.isReal() ? RobotMode.REAL : RobotMode.REPLAY; + case ROBOT_2023_OFFSEASON_SWERVE -> RobotBase.isReal() ? RobotMode.REAL : RobotMode.REPLAY; case ROBOT_SIMBOT -> RobotMode.SIM; }; } public static class Drivetrain { - // MODULE LOAD ORDER IS FRONT LEFT -> FRONT RIGHT -> BACK LEFT -> BACK RIGHT - - public static final SwerveDriveKinematics kKinematics = - new SwerveDriveKinematics( - FrontLeft.kCenterOffset, // FRONT LEFT - FrontRight.kCenterOffset, // FRONT RIGHT - BackLeft.kCenterOffset, // BACK LEFT - BackRight.kCenterOffset // BACK RIGHT - ); - - public static final boolean kSteerMotorInverted = true; - - public static final double kMaxVelocityMetersPerSecond = Units.feetToMeters(12.0); // SDS MK4 L1 - // public static final double kMaxVelocityMetersPerSecond = Units.feetToMeters(14.5); // SDS MK4 - // L2 - public static final double kMaxAccelerationMetersPerSecondSquared = 3.5; - public static final double kMaxRotationVelocityRadPerSecond = 2 * Math.PI; - - public static final double kDriveGearRatio = 8.14; // SDS MK4 L1 - // public static final double kDriveGearRatio = 6.75; // SDS MK4 L2 - - public static final double kSteerGearRatio = 12.8; - - public static final double kWheelRadiusInches = 2; - public static final double KWheelRadiusMeters = Units.inchesToMeters(kWheelRadiusInches); - - public static final double kDriveMotorConversionFactor = - 2 * Math.PI * KWheelRadiusMeters / kDriveGearRatio; - public static final double kSteerMotorConversionFactor = 2 * Math.PI / kSteerGearRatio; - - public static class FrontLeft { - public static final Translation2d kCenterOffset = - new Translation2d(Units.inchesToMeters(8.825180), Units.inchesToMeters(8.825180)); - public static final double kMagneticOffsetDegrees = 0.0; // TODO - } - - public static class FrontRight { - public static final Translation2d kCenterOffset = - new Translation2d(Units.inchesToMeters(8.825180), -Units.inchesToMeters(8.825180)); - public static final double kMagneticOffsetDegrees = 0.0; // TODO - } - - public static class BackLeft { - public static final Translation2d kCenterOffset = - new Translation2d(-Units.inchesToMeters(8.825180), Units.inchesToMeters(8.825180)); - public static final double kMagneticOffsetDegrees = 0.0; // TODO + // L2 gearing + public static final double kDriveGearing = (50.0 / 14.0) * (17.0 / 27.0) * (45.0 / 15.0); + public static final double kTurnGearing = 12.8; + + public static final boolean kTurnMotorInverted = false; + + public static final double kWheelRadiusMeters = Units.inchesToMeters(2.0); + + public static final double kTrackWidthXMeters = Units.inchesToMeters(20.5); // TODO + public static final double kTrackWidthYMeters = Units.inchesToMeters(20.5); // TODO + public static final double kDriveBaseRadiusMeters = + Math.hypot(kTrackWidthXMeters / 2.0, kTrackWidthYMeters / 2.0); + + public static final double kMaxLinearVelocityMetersPerSecond = Units.feetToMeters(14.5); + public static final double kMaxLAngularVelocityRadiansPerSecond = + kMaxLinearVelocityMetersPerSecond / kDriveBaseRadiusMeters; + + public static final class DriveCoefficients { + public static final double kS = 0.0; // TODO + public static final double kV = 0.0; // TODO + public static final double kA = 0.0; // TODO + public static final double kP = 0.0; // TODO + public static final double kI = 0.0; // TODO + public static final double kD = 0.0; // TODO } - public static class BackRight { - public static final Translation2d kCenterOffset = - new Translation2d(-Units.inchesToMeters(8.825180), -Units.inchesToMeters(8.825180)); - public static final double kMagneticOffsetDegrees = 0.0; // TODO + public static final class TurnCoefficients { + public static final double kP = 0.0; // TODO + public static final double kI = 0.0; // TODO + public static final double kD = 0.0; // TODO } - public static class ControlValues { - public static class Drive { - public static final double kP = 0; // TODO - public static final double kI = 0; // TODO - public static final double kD = 0; // TODO - public static final double kFF = 0; // TODO - } - - public static class Steer { - public static final double kP = 0; // TODO - public static final double kI = 0; // TODO - public static final double kD = 0; // TODO - } - } - } - - public enum NeutralMode { - BRAKE, - COAST; - - /** - * Convert the Neutral mode to one used by the SparkMax API. - * - * @return SparkMax Idle Mode. - */ - public CANSparkMax.IdleMode toIdleMode() { - return switch (this) { - case BRAKE -> CANSparkMax.IdleMode.kBrake; - case COAST -> CANSparkMax.IdleMode.kCoast; + /** Returns an array of module translations. */ + public static Translation2d[] getModuleTranslations() { + return new Translation2d[] { + new Translation2d( + Constants.Drivetrain.kTrackWidthXMeters / 2.0, + Constants.Drivetrain.kTrackWidthYMeters / 2.0), + new Translation2d( + Constants.Drivetrain.kTrackWidthXMeters / 2.0, + -Constants.Drivetrain.kTrackWidthYMeters / 2.0), + new Translation2d( + -Constants.Drivetrain.kTrackWidthXMeters / 2.0, + Constants.Drivetrain.kTrackWidthYMeters / 2.0), + new Translation2d( + -Constants.Drivetrain.kTrackWidthXMeters / 2.0, + -Constants.Drivetrain.kTrackWidthYMeters / 2.0) }; } + + public static final SwerveDriveKinematics m_kinematics = + new SwerveDriveKinematics(getModuleTranslations()); } } diff --git a/src/main/java/frc/robot/constants/HardwareDevices.java b/src/main/java/frc/robot/constants/HardwareDevices.java deleted file mode 100644 index 1a4f24b..0000000 --- a/src/main/java/frc/robot/constants/HardwareDevices.java +++ /dev/null @@ -1,35 +0,0 @@ -package frc.robot.constants; - -public class HardwareDevices { - public static final int kDriverControllerPort = 0; - - public static class SwerveBase { - public static final int kGyroId = 0; // TODO - - public static class Drivetrain { - public static class FrontLeft { - public static final int kDriveMotorId = 0; // TODO - public static final int kSteerMotorId = 0; // TODO - public static final int kAbsoluteEncoderId = 0; // TODO - } - - public static class FrontRight { - public static final int kDriveMotorId = 0; // TODO - public static final int kSteerMotorId = 0; // TODO - public static final int kAbsoluteEncoderId = 0; // TODO - } - - public static class BackLeft { - public static final int kDriveMotorId = 0; // TODO - public static final int kSteerMotorId = 0; // TODO - public static final int kAbsoluteEncoderId = 0; // TODO - } - - public static class BackRight { - public static final int kDriveMotorId = 0; // TODO - public static final int kSteerMotorId = 0; // TODO - public static final int kAbsoluteEncoderId = 0; // TODO - } - } - } -} diff --git a/src/main/java/frc/robot/constants/HardwareIds.java b/src/main/java/frc/robot/constants/HardwareIds.java new file mode 100644 index 0000000..39129ac --- /dev/null +++ b/src/main/java/frc/robot/constants/HardwareIds.java @@ -0,0 +1,26 @@ +package frc.robot.constants; + +public class HardwareIds { + public static final int kPigeonId = 0; // TODO + + public static final int kFrontLeftTurnId = 0; // TODO + public static final int kFrontLeftDriveId = 0; // TODO + public static final int kFrontLeftEncoderId = 0; // TODO + + public static final int kBackLeftTurnId = 0; // TODO + public static final int kBackLeftDriveId = 0; // TODO + public static final int kBackLeftEncoderId = 0; // TODO + + public static final int kFrontRightTurnId = 0; // TODO + public static final int kFrontRightDriveId = 0; // TODO + public static final int kFrontRightEncoderId = 0; // TODO + + public static final int kBackRightTurnId = 0; // TODO + public static final int kBackRightDriveId = 0; // TODO + public static final int kBackRightEncoderId = 0; // TODO + + public static final String kCamOneName = ""; // TODO + public static final String kCamTwoName = ""; // TODO + public static final String kCamThreeName = ""; // TODO + public static final String kCamFourName = ""; // TODO +} diff --git a/src/main/java/frc/robot/drive/DriveBase.java b/src/main/java/frc/robot/drive/DriveBase.java deleted file mode 100644 index aaf1926..0000000 --- a/src/main/java/frc/robot/drive/DriveBase.java +++ /dev/null @@ -1,176 +0,0 @@ -package frc.robot.drive; - -import edu.wpi.first.math.geometry.Rotation2d; -import edu.wpi.first.math.geometry.Translation2d; -import edu.wpi.first.math.kinematics.ChassisSpeeds; -import edu.wpi.first.math.kinematics.SwerveDriveKinematics; -import edu.wpi.first.math.kinematics.SwerveModulePosition; -import edu.wpi.first.math.kinematics.SwerveModuleState; -import edu.wpi.first.wpilibj2.command.SubsystemBase; -import frc.lib.PoseEstimator; -import frc.robot.constants.Constants; -import frc.robot.drive.gyro.GyroIO; -import frc.robot.drive.gyro.GyroInputsAutoLogged; -import org.littletonrobotics.junction.Logger; - -public class DriveBase extends SubsystemBase { - private final SwerveModuleIO[] m_moduleIOs; - private final SwerveModuleInputsAutoLogged[] m_moduleInputs = - new SwerveModuleInputsAutoLogged[] { - new SwerveModuleInputsAutoLogged(), - new SwerveModuleInputsAutoLogged(), - new SwerveModuleInputsAutoLogged(), - new SwerveModuleInputsAutoLogged() - }; - - private final GyroIO m_gyroIO; - private final GyroInputsAutoLogged m_gyroInputs = new GyroInputsAutoLogged(); - - public DriveBase( - GyroIO gyro, - SwerveModuleIO frontLeft, - SwerveModuleIO frontRight, - SwerveModuleIO backLeft, - SwerveModuleIO backRight) { - m_moduleIOs = new SwerveModuleIO[] {frontLeft, frontRight, backLeft, backRight}; - m_gyroIO = gyro; - - m_gyroIO.resetHeading(); - - for (SwerveModuleIO module : m_moduleIOs) { - module.resyncEncoders(); - } - - PoseEstimator.createInstance( - Constants.Drivetrain.kKinematics, m_gyroIO.getHeading(), getModulePositions()); - } - - @Override - public void periodic() { - for (int i = 0; i < 4; i++) { - m_moduleIOs[i].updateInputs(m_moduleInputs[i]); - Logger.getInstance().processInputs("Drive/Module" + i, m_moduleInputs[i]); - } - - m_gyroIO.updateInputs(m_gyroInputs); - Logger.getInstance().processInputs("Drive/Gyro", m_gyroInputs); - - // Update PoseEstimator with drive data - PoseEstimator.getInstance().update(m_gyroIO.getHeading(), getModulePositions()); - Logger.getInstance() - .recordOutput("EstimatedPose", PoseEstimator.getInstance().getEstimatedPosition()); - } - - @Override - public void simulationPeriodic() { - m_gyroIO.incrementHeading(getChassisSpeeds().omegaRadiansPerSecond * Constants.loopPeriodSecs); - } - - public Rotation2d getHeading() { - return m_gyroIO.getHeading(); - } - - public ChassisSpeeds getChassisSpeeds() { - return Constants.Drivetrain.kKinematics.toChassisSpeeds(getModuleStates()); - } - - /** - * Set the drivetrain modules based on a series of speeds and spinning about a non-center of the - * robot given as a {@link Translation2d} object and following its conventions. - * - * @param xSpeed Horizontal speed to derive module states in meters per second - * @param ySpeed Vertical speed to derive module states in meters per second - * @param rotationalSpeed Rotational speed in radians per second the drivetrain spins in - * @param driveMode Control input method to derive module states from: {@code DriveMode.FIELD} or - * {@code DriveMode.DRIVER} (default FIELD) - */ - public final void setFromForces( - double xSpeed, - double ySpeed, - double rotationalSpeed, - Translation2d centerOfRobot, - DriveMode driveMode) { - - setModuleStates( - Constants.Drivetrain.kKinematics.toSwerveModuleStates( - switch (driveMode) { - case kRobot -> new ChassisSpeeds(xSpeed, ySpeed, rotationalSpeed); - case kField -> ChassisSpeeds.fromFieldRelativeSpeeds( - xSpeed, ySpeed, rotationalSpeed, getHeading()); - }, - centerOfRobot)); - } - - /** - * Control method to convert input stuff to drive stuff for all the modules - * - * @param xSpeed Horizontal speed to derive module states in meters per second - * @param ySpeed Vertical speed to derive module states in meters per second - * @param rotationalSpeed Rotational speed in radians per second the drivetrain spins in - * @param driveMode Control input method to derive module states from: {@code DriveMode.FIELD} or - * {@code DriveMode.DRIVER} - */ - public final void setFromForces( - double xSpeed, double ySpeed, double rotationalSpeed, DriveMode driveMode) { - setFromForces(xSpeed, ySpeed, rotationalSpeed, new Translation2d(), driveMode); - } - - /** Apply module states to modules based on a ChassisSpeed object */ - public final void setChassisSpeed(ChassisSpeeds chassisSpeed) { - setModuleStates(Constants.Drivetrain.kKinematics.toSwerveModuleStates(chassisSpeed)); - } - - /** - * Apply module states to modules based on an array of module states. - * - * @implNote MODULES MUST BE GIVEN IN ORDER: FRONT LEFT -> FRONT RIGHT -> BACK LEFT -> BACK RIGHT - */ - public final void setModuleStates(SwerveModuleState[] states) { - SwerveDriveKinematics.desaturateWheelSpeeds( - states, Constants.Drivetrain.kMaxVelocityMetersPerSecond); - - for (int i = 0; i < 4; i++) { - m_moduleIOs[i].setState(states[i]); - } - } - - public void stop() { - for (SwerveModuleIO module : m_moduleIOs) { - module.stop(); - } - } - - public void stopWithX() { - double[] stopAngles = - new double[] {-Math.PI / 4.0, Math.PI / 4.0, Math.PI / 4.0, -Math.PI / 4.0}; - - for (int i = 0; i < m_moduleIOs.length; i++) { - m_moduleIOs[i].setState(new SwerveModuleState(0, Rotation2d.fromRadians(stopAngles[i]))); - } - } - - /** Return module states in order of kinematic initialization from modules */ - private SwerveModuleState[] getModuleStates() { - return new SwerveModuleState[] { - m_moduleIOs[0].getState(), - m_moduleIOs[1].getState(), - m_moduleIOs[2].getState(), - m_moduleIOs[3].getState() - }; - } - - /** Return module positions in order of kinematic initialization from modules */ - private SwerveModulePosition[] getModulePositions() { - return new SwerveModulePosition[] { - m_moduleIOs[0].getPosition(), - m_moduleIOs[1].getPosition(), - m_moduleIOs[2].getPosition(), - m_moduleIOs[3].getPosition() - }; - } - - public enum DriveMode { - kRobot, - kField - } -} diff --git a/src/main/java/frc/robot/drive/SwerveModuleIO.java b/src/main/java/frc/robot/drive/SwerveModuleIO.java deleted file mode 100644 index 6eff6e2..0000000 --- a/src/main/java/frc/robot/drive/SwerveModuleIO.java +++ /dev/null @@ -1,42 +0,0 @@ -package frc.robot.drive; - -import edu.wpi.first.math.kinematics.SwerveModulePosition; -import edu.wpi.first.math.kinematics.SwerveModuleState; -import frc.lib.LoggedIO; -import frc.robot.constants.Constants; -import org.littletonrobotics.junction.AutoLog; - -public interface SwerveModuleIO extends LoggedIO { - @AutoLog - public class SwerveModuleInputs { - public double DriveCurrentAmps; - public double DriveTempCelsius; - public double DriveAppliedVoltage; - public double SteerCurrentAmps; - public double SteerTempCelsius; - public double SteerAppliedVoltage; - - public double VelocityRadPerSec; - public double VelocityMetersPerSec; - public double AbsolutePositionRad; - public double RelativePositionRad; - public double DistanceTraveledMeters; - } - - default SwerveModuleState getState() { - return new SwerveModuleState(); - } - - default SwerveModulePosition getPosition() { - return new SwerveModulePosition(); - } - - default void setState(SwerveModuleState state) {} - - default void resyncEncoders() {} - - default void stop() {} - - default void setNeutralMode( - Constants.NeutralMode driveNeutralMode, Constants.NeutralMode steerNeutralMode) {} -} diff --git a/src/main/java/frc/robot/drive/SwerveModuleIOMK4DualSparkMax.java b/src/main/java/frc/robot/drive/SwerveModuleIOMK4DualSparkMax.java deleted file mode 100644 index 1797fbe..0000000 --- a/src/main/java/frc/robot/drive/SwerveModuleIOMK4DualSparkMax.java +++ /dev/null @@ -1,166 +0,0 @@ -package frc.robot.drive; - -import com.ctre.phoenix.sensors.AbsoluteSensorRange; -import com.ctre.phoenix.sensors.CANCoderConfiguration; -import com.ctre.phoenix.sensors.WPI_CANCoder; -import com.revrobotics.*; -import edu.wpi.first.math.geometry.Rotation2d; -import edu.wpi.first.math.kinematics.SwerveModulePosition; -import edu.wpi.first.math.kinematics.SwerveModuleState; -import frc.lib.SparkMaxBurnManager; -import frc.lib.SparkMaxPeriodicFrameConfig; -import frc.robot.constants.Constants; - -public class SwerveModuleIOMK4DualSparkMax implements SwerveModuleIO { - private final CANSparkMax m_driveMotor; - private final CANSparkMax m_steerMotor; - - private final RelativeEncoder m_driveEncoder; - private final RelativeEncoder m_steerEncoder; - - private final SparkMaxPIDController m_driveController; - private final SparkMaxPIDController m_steerController; - - private final WPI_CANCoder m_absoluteEncoder; - - public SwerveModuleIOMK4DualSparkMax( - int driveMotorId, - int steerMotorId, - int absoluteEncoderId, - boolean steerInverted, - double driveConversionFactor, - double steerConversionFactor, - double absoluteEncoderOffsetDegrees) { - m_driveMotor = new CANSparkMax(driveMotorId, CANSparkMaxLowLevel.MotorType.kBrushless); - m_steerMotor = new CANSparkMax(steerMotorId, CANSparkMaxLowLevel.MotorType.kBrushless); - - if (SparkMaxBurnManager.shouldBurnFlash()) { - m_driveMotor.restoreFactoryDefaults(); - m_steerMotor.restoreFactoryDefaults(); - } - - m_driveMotor.setCANTimeout(SparkMaxBurnManager.kConfigurationStatusTimeoutMs); - m_steerMotor.setCANTimeout(SparkMaxBurnManager.kConfigurationStatusTimeoutMs); - - m_driveEncoder = m_driveMotor.getEncoder(); - m_steerEncoder = m_steerMotor.getEncoder(); - - m_driveController = m_driveMotor.getPIDController(); - m_steerController = m_steerMotor.getPIDController(); - - SparkMaxPeriodicFrameConfig.configureIsolated(m_driveMotor); - SparkMaxPeriodicFrameConfig.configureIsolated(m_steerMotor); - - m_steerMotor.setInverted(steerInverted); - - m_driveMotor.setSmartCurrentLimit(40); - m_driveMotor.enableVoltageCompensation(12.0); - - m_steerMotor.setSmartCurrentLimit(20); - m_steerMotor.enableVoltageCompensation(12.0); - - m_driveEncoder.setPositionConversionFactor(driveConversionFactor); - m_driveEncoder.setVelocityConversionFactor(driveConversionFactor / 60.0); - m_driveEncoder.setPosition(0.0); - m_driveEncoder.setMeasurementPeriod(10); - m_driveEncoder.setAverageDepth(2); - - m_steerEncoder.setPositionConversionFactor(steerConversionFactor); - m_steerEncoder.setVelocityConversionFactor(steerConversionFactor / 60.0); - m_steerEncoder.setPosition(0.0); - m_steerEncoder.setMeasurementPeriod(10); - m_steerEncoder.setAverageDepth(2); - - m_driveController.setP(Constants.Drivetrain.ControlValues.Drive.kP); - m_driveController.setI(Constants.Drivetrain.ControlValues.Drive.kI); - m_driveController.setD(Constants.Drivetrain.ControlValues.Drive.kD); - m_driveController.setFF(Constants.Drivetrain.ControlValues.Drive.kFF); - - m_steerController.setP(Constants.Drivetrain.ControlValues.Steer.kP); - m_steerController.setI(Constants.Drivetrain.ControlValues.Steer.kI); - m_steerController.setD(Constants.Drivetrain.ControlValues.Steer.kD); - - m_steerController.setPositionPIDWrappingEnabled(true); - m_steerController.setPositionPIDWrappingMinInput(-Math.PI); - m_steerController.setPositionPIDWrappingMaxInput(Math.PI); - - m_driveMotor.setCANTimeout(0); - m_steerMotor.setCANTimeout(0); - - if (SparkMaxBurnManager.shouldBurnFlash()) { - m_driveMotor.burnFlash(); - m_steerMotor.burnFlash(); - } - - m_absoluteEncoder = new WPI_CANCoder(absoluteEncoderId); - - CANCoderConfiguration config = new CANCoderConfiguration(); - config.sensorCoefficient = 2 * Math.PI / 4096.0; - config.unitString = "rad"; - config.magnetOffsetDegrees = absoluteEncoderOffsetDegrees; - config.absoluteSensorRange = AbsoluteSensorRange.Signed_PlusMinus180; - - m_absoluteEncoder.configAllSettings(config); - - setNeutralMode(Constants.NeutralMode.COAST, Constants.NeutralMode.BRAKE); - } - - @Override - public void updateInputs(SwerveModuleInputs inputs) { - inputs.DriveCurrentAmps = m_driveMotor.getOutputCurrent(); - inputs.DriveTempCelsius = m_driveMotor.getMotorTemperature(); - inputs.DriveAppliedVoltage = m_driveMotor.getAppliedOutput() * m_driveMotor.getBusVoltage(); - inputs.SteerCurrentAmps = m_steerMotor.getOutputCurrent(); - inputs.SteerTempCelsius = m_steerMotor.getMotorTemperature(); - inputs.SteerAppliedVoltage = m_steerMotor.getAppliedOutput() * m_steerMotor.getBusVoltage(); - - inputs.VelocityRadPerSec = m_steerEncoder.getVelocity(); - inputs.VelocityMetersPerSec = m_driveEncoder.getVelocity(); - inputs.AbsolutePositionRad = m_absoluteEncoder.getAbsolutePosition(); - inputs.RelativePositionRad = m_steerEncoder.getPosition(); - inputs.DistanceTraveledMeters = m_driveEncoder.getPosition(); - } - - @Override - public SwerveModuleState getState() { - return new SwerveModuleState( - m_driveEncoder.getVelocity(), - Rotation2d.fromRadians(m_absoluteEncoder.getAbsolutePosition())); - } - - @Override - public SwerveModulePosition getPosition() { - return new SwerveModulePosition( - m_driveEncoder.getPosition(), - Rotation2d.fromRadians(m_absoluteEncoder.getAbsolutePosition())); - } - - @Override - public void setState(SwerveModuleState state) { - // Optimize the SwerveModuleState - state = - SwerveModuleState.optimize( - state, Rotation2d.fromRadians(m_absoluteEncoder.getAbsolutePosition())); - - m_driveController.setReference(state.speedMetersPerSecond, CANSparkMax.ControlType.kVelocity); - m_steerController.setReference(state.angle.getRadians(), CANSparkMax.ControlType.kPosition); - } - - @Override - public void resyncEncoders() { - m_driveEncoder.setPosition(0); - m_steerEncoder.setPosition(m_absoluteEncoder.getAbsolutePosition()); - } - - @Override - public void stop() { - m_driveMotor.stopMotor(); - } - - @Override - public void setNeutralMode( - Constants.NeutralMode driveNeutralMode, Constants.NeutralMode steerNeutralMode) { - m_driveMotor.setIdleMode(driveNeutralMode.toIdleMode()); - m_steerMotor.setIdleMode(steerNeutralMode.toIdleMode()); - } -} diff --git a/src/main/java/frc/robot/drive/SwerveModuleIOSim.java b/src/main/java/frc/robot/drive/SwerveModuleIOSim.java deleted file mode 100644 index a0016c5..0000000 --- a/src/main/java/frc/robot/drive/SwerveModuleIOSim.java +++ /dev/null @@ -1,46 +0,0 @@ -package frc.robot.drive; - -import edu.wpi.first.math.kinematics.SwerveModulePosition; -import edu.wpi.first.math.kinematics.SwerveModuleState; -import frc.robot.constants.Constants; - -public class SwerveModuleIOSim implements SwerveModuleIO { - private final SwerveModuleState m_currentState = new SwerveModuleState(); - private final SwerveModulePosition m_currentPosition = new SwerveModulePosition(); - - @Override - public SwerveModuleState getState() { - return m_currentState; - } - - @Override - public SwerveModulePosition getPosition() { - return m_currentPosition; - } - - @Override - public void setState(SwerveModuleState state) { - state = SwerveModuleState.optimize(state, m_currentState.angle); - - m_currentState.speedMetersPerSecond = state.speedMetersPerSecond; - m_currentState.angle = state.angle; - } - - @Override - public void stop() { - m_currentState.speedMetersPerSecond = 0; - } - - @Override - public void updateInputs(SwerveModuleInputs inputs) { - // Update Position - m_currentPosition.distanceMeters += - m_currentState.speedMetersPerSecond * Constants.loopPeriodSecs; - m_currentPosition.angle = m_currentState.angle; - - inputs.DistanceTraveledMeters = m_currentPosition.distanceMeters; - inputs.VelocityMetersPerSec = m_currentState.speedMetersPerSecond; - - inputs.AbsolutePositionRad = m_currentPosition.angle.getRadians(); - } -} diff --git a/src/main/java/frc/robot/drive/commands/DriveControl.java b/src/main/java/frc/robot/drive/commands/DriveControl.java deleted file mode 100644 index cbcfa48..0000000 --- a/src/main/java/frc/robot/drive/commands/DriveControl.java +++ /dev/null @@ -1,32 +0,0 @@ -package frc.robot.drive.commands; - -import edu.wpi.first.wpilibj2.command.CommandBase; -import frc.robot.constants.Constants; -import frc.robot.drive.DriveBase; -import frc.robot.oi.DriverInterface; - -public class DriveControl extends CommandBase { - private final DriveBase m_driveBase; - private final DriverInterface m_driverInterface; - - public DriveControl(DriveBase driveBase, DriverInterface driverInterface) { - m_driveBase = driveBase; - m_driverInterface = driverInterface; - - addRequirements(driveBase); - } - - @Override - public void execute() { - m_driveBase.setFromForces( - m_driverInterface.getTranslationalLateralPercent() - * Constants.Drivetrain.kMaxVelocityMetersPerSecond, - m_driverInterface.getTranslationalMedialPercent() - * Constants.Drivetrain.kMaxVelocityMetersPerSecond, - m_driverInterface.getRotationalPercent() - * Constants.Drivetrain.kMaxRotationVelocityRadPerSecond, - m_driverInterface.isRobotRelative().getAsBoolean() - ? DriveBase.DriveMode.kRobot - : DriveBase.DriveMode.kField); - } -} diff --git a/src/main/java/frc/robot/drive/gyro/GyroIO.java b/src/main/java/frc/robot/drive/gyro/GyroIO.java deleted file mode 100644 index 4691746..0000000 --- a/src/main/java/frc/robot/drive/gyro/GyroIO.java +++ /dev/null @@ -1,38 +0,0 @@ -package frc.robot.drive.gyro; - -import edu.wpi.first.math.geometry.Rotation2d; -import frc.lib.LoggedIO; -import org.littletonrobotics.junction.AutoLog; - -public interface GyroIO extends LoggedIO { - @AutoLog - public class GyroInputs { - public boolean Connected; - - public double RollPositionRad; - public double PitchPositionRad; - public double YawPositionRad; - - public double RollRateRadPerSecond; - public double PitchRateRadPerSecond; - public double YawRateRadPerSecond; - - public double AccelXGForce; - public double AccelYGForce; - public double AccelZGForce; - } - - /** - * Get the heading of the robot. - * - * @return robot heading. - */ - default Rotation2d getHeading() { - return new Rotation2d(); - } - - /** Reset the yaw of the gyro to 0. */ - default void resetHeading() {} - - default void incrementHeading(double incrementRad) {} -} diff --git a/src/main/java/frc/robot/drive/gyro/GyroIOPigeon2.java b/src/main/java/frc/robot/drive/gyro/GyroIOPigeon2.java deleted file mode 100644 index ba348e5..0000000 --- a/src/main/java/frc/robot/drive/gyro/GyroIOPigeon2.java +++ /dev/null @@ -1,46 +0,0 @@ -package frc.robot.drive.gyro; - -import com.ctre.phoenix.ErrorCode; -import com.ctre.phoenix.sensors.WPI_Pigeon2; -import edu.wpi.first.math.geometry.Rotation2d; - -public class GyroIOPigeon2 implements GyroIO { - private final WPI_Pigeon2 m_gyro; - - private final double[] xyzDegreesPerSecond = new double[3]; - private final short[] xyzAccelData = new short[3]; - - public GyroIOPigeon2(int gyroId) { - m_gyro = new WPI_Pigeon2(gyroId); - } - - @Override - public void updateInputs(GyroInputs inputs) { - m_gyro.getRawGyro(xyzDegreesPerSecond); - m_gyro.getBiasedAccelerometer(xyzAccelData); - - inputs.Connected = m_gyro.getLastError().equals(ErrorCode.OK); - - inputs.RollPositionRad = Math.toRadians(m_gyro.getRoll()); - inputs.PitchPositionRad = Math.toRadians(m_gyro.getPitch()); - inputs.YawPositionRad = Math.toRadians(m_gyro.getYaw()); - - inputs.RollRateRadPerSecond = Math.toRadians(xyzDegreesPerSecond[0]); - inputs.PitchRateRadPerSecond = Math.toRadians(xyzDegreesPerSecond[1]); - inputs.YawRateRadPerSecond = Math.toRadians(xyzDegreesPerSecond[2]); - - inputs.AccelXGForce = (double) xyzAccelData[0] / (1 << 14); - inputs.AccelYGForce = (double) xyzAccelData[1] / (1 << 14); - inputs.AccelZGForce = (double) xyzAccelData[2] / (1 << 14); - } - - @Override - public Rotation2d getHeading() { - return m_gyro.getRotation2d(); - } - - @Override - public void resetHeading() { - m_gyro.reset(); - } -} diff --git a/src/main/java/frc/robot/drive/gyro/GyroIOSim.java b/src/main/java/frc/robot/drive/gyro/GyroIOSim.java deleted file mode 100644 index 298184c..0000000 --- a/src/main/java/frc/robot/drive/gyro/GyroIOSim.java +++ /dev/null @@ -1,29 +0,0 @@ -package frc.robot.drive.gyro; - -import edu.wpi.first.math.geometry.Rotation2d; -import edu.wpi.first.wpilibj.simulation.AnalogGyroSim; - -public class GyroIOSim implements GyroIO { - private final AnalogGyroSim m_gyro = new AnalogGyroSim(1); - - @Override - public void updateInputs(GyroInputs inputs) { - inputs.YawPositionRad = m_gyro.getAngle(); - inputs.YawRateRadPerSecond = m_gyro.getRate(); - } - - @Override - public Rotation2d getHeading() { - return Rotation2d.fromRadians(m_gyro.getAngle()); - } - - @Override - public void resetHeading() { - m_gyro.setAngle(0); - } - - @Override - public void incrementHeading(double incrementRad) { - m_gyro.setAngle(m_gyro.getAngle() + incrementRad); - } -} diff --git a/src/main/java/frc/robot/oi/DriverInterface.java b/src/main/java/frc/robot/oi/DriverInterface.java deleted file mode 100644 index 6aff7f6..0000000 --- a/src/main/java/frc/robot/oi/DriverInterface.java +++ /dev/null @@ -1,13 +0,0 @@ -package frc.robot.oi; - -import edu.wpi.first.wpilibj2.command.button.Trigger; - -public interface DriverInterface { - public double getTranslationalMedialPercent(); - - public double getTranslationalLateralPercent(); - - public double getRotationalPercent(); - - public Trigger isRobotRelative(); -} diff --git a/src/main/java/frc/robot/oi/DualJoystickDriver.java b/src/main/java/frc/robot/oi/DualJoystickDriver.java deleted file mode 100644 index 468c5cc..0000000 --- a/src/main/java/frc/robot/oi/DualJoystickDriver.java +++ /dev/null @@ -1,35 +0,0 @@ -package frc.robot.oi; - -import edu.wpi.first.math.MathUtil; -import edu.wpi.first.wpilibj2.command.button.CommandJoystick; -import edu.wpi.first.wpilibj2.command.button.Trigger; - -public class DualJoystickDriver implements DriverInterface { - private final CommandJoystick m_leftJoystick; - private final CommandJoystick m_rightJoystick; - - public DualJoystickDriver(int leftPort, int rightPort) { - m_leftJoystick = new CommandJoystick(leftPort); - m_rightJoystick = new CommandJoystick(rightPort); - } - - @Override - public double getTranslationalMedialPercent() { - return MathUtil.applyDeadband(-m_leftJoystick.getY(), 0.15); - } - - @Override - public double getTranslationalLateralPercent() { - return MathUtil.applyDeadband(m_leftJoystick.getX(), 0.15); - } - - @Override - public double getRotationalPercent() { - return -MathUtil.applyDeadband(m_rightJoystick.getX(), 0.15); - } - - @Override - public Trigger isRobotRelative() { - return m_leftJoystick.button(1); - } -} diff --git a/src/main/java/frc/robot/oi/OIManager.java b/src/main/java/frc/robot/oi/OIManager.java deleted file mode 100644 index 21cbe8b..0000000 --- a/src/main/java/frc/robot/oi/OIManager.java +++ /dev/null @@ -1,20 +0,0 @@ -package frc.robot.oi; - -import edu.wpi.first.wpilibj.RobotBase; -import frc.robot.constants.HardwareDevices; - -public class OIManager { - private final DriverInterface m_driverInterface; - - public OIManager() { - if (RobotBase.isSimulation()) { - m_driverInterface = new PS4Driver(HardwareDevices.kDriverControllerPort); - } else { - m_driverInterface = new XboxDriver(HardwareDevices.kDriverControllerPort); - } - } - - public DriverInterface getDriverInterface() { - return m_driverInterface; - } -} diff --git a/src/main/java/frc/robot/oi/PS4Driver.java b/src/main/java/frc/robot/oi/PS4Driver.java deleted file mode 100644 index 81127a3..0000000 --- a/src/main/java/frc/robot/oi/PS4Driver.java +++ /dev/null @@ -1,33 +0,0 @@ -package frc.robot.oi; - -import edu.wpi.first.math.MathUtil; -import edu.wpi.first.wpilibj2.command.button.CommandPS4Controller; -import edu.wpi.first.wpilibj2.command.button.Trigger; - -public class PS4Driver implements DriverInterface { - private final CommandPS4Controller m_controller; - - public PS4Driver(int port) { - m_controller = new CommandPS4Controller(port); - } - - @Override - public double getTranslationalMedialPercent() { - return MathUtil.applyDeadband(-m_controller.getLeftY(), 0.15); - } - - @Override - public double getTranslationalLateralPercent() { - return MathUtil.applyDeadband(m_controller.getLeftX(), 0.15); - } - - @Override - public double getRotationalPercent() { - return -MathUtil.applyDeadband(m_controller.getRightX(), 0.15); - } - - @Override - public Trigger isRobotRelative() { - return m_controller.R1(); - } -} diff --git a/src/main/java/frc/robot/oi/XboxDriver.java b/src/main/java/frc/robot/oi/XboxDriver.java deleted file mode 100644 index 04e18e1..0000000 --- a/src/main/java/frc/robot/oi/XboxDriver.java +++ /dev/null @@ -1,33 +0,0 @@ -package frc.robot.oi; - -import edu.wpi.first.math.MathUtil; -import edu.wpi.first.wpilibj2.command.button.CommandXboxController; -import edu.wpi.first.wpilibj2.command.button.Trigger; - -public class XboxDriver implements DriverInterface { - private final CommandXboxController m_controller; - - public XboxDriver(int port) { - m_controller = new CommandXboxController(port); - } - - @Override - public double getTranslationalMedialPercent() { - return MathUtil.applyDeadband(-m_controller.getLeftY(), 0.15); - } - - @Override - public double getTranslationalLateralPercent() { - return MathUtil.applyDeadband(m_controller.getLeftX(), 0.15); - } - - @Override - public double getRotationalPercent() { - return -MathUtil.applyDeadband(m_controller.getRightX(), 0.15); - } - - @Override - public Trigger isRobotRelative() { - return m_controller.rightBumper(); - } -} diff --git a/src/main/java/frc/robot/subsystems/drive/DriveBase.java b/src/main/java/frc/robot/subsystems/drive/DriveBase.java new file mode 100644 index 0000000..250a444 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/drive/DriveBase.java @@ -0,0 +1,148 @@ +package frc.robot.subsystems.drive; + +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Twist2d; +import edu.wpi.first.math.kinematics.ChassisSpeeds; +import edu.wpi.first.math.kinematics.SwerveDriveKinematics; +import edu.wpi.first.math.kinematics.SwerveModulePosition; +import edu.wpi.first.math.kinematics.SwerveModuleState; +import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.first.wpilibj2.command.SubsystemBase; +import frc.robot.constants.Constants; +import frc.robot.util.PoseEstimator; +import org.littletonrobotics.junction.AutoLogOutput; +import org.littletonrobotics.junction.Logger; + +public class DriveBase extends SubsystemBase { + public static double ODOMETRY_FREQUENCY = 250.0; + + private final GyroIO m_gyroIO; + private final GyroIOInputsAutoLogged m_gyroInputs = new GyroIOInputsAutoLogged(); + private final Module[] m_modules = new Module[4]; // FL, FR, BL, BR + + private Rotation2d m_lastGyroRotation = new Rotation2d(); + + public DriveBase( + GyroIO gyroIO, + ModuleIO frontLeftIO, + ModuleIO frontRightIO, + ModuleIO backLeftIO, + ModuleIO backRightIO) { + this.m_gyroIO = gyroIO; + + m_modules[0] = new Module(0, frontLeftIO); + m_modules[1] = new Module(1, frontRightIO); + m_modules[2] = new Module(2, backLeftIO); + m_modules[3] = new Module(3, backRightIO); + } + + public void periodic() { + PoseEstimator.odometryLock.lock(); + m_gyroIO.updateInputs(m_gyroInputs); + for (var module : m_modules) { + module.updateInputs(); + } + PoseEstimator.odometryLock.unlock(); + + Logger.processInputs("Drive/Gyro", m_gyroInputs); + for (var module : m_modules) { + module.periodic(); + } + + if (DriverStation.isDisabled()) { + // Stop moving when disabled + for (var module : m_modules) { + module.disable(); + } + + // Log empty setpoint states when disabled + Logger.recordOutput("SwerveStates/Setpoints", new SwerveModuleState[] {}); + Logger.recordOutput("SwerveStates/SetpointsOptimized", new SwerveModuleState[] {}); + } + + // Update odometry + int deltaCount = + m_gyroInputs.connected ? m_gyroInputs.odometryYawPositions.length : Integer.MAX_VALUE; + for (var module : m_modules) { + deltaCount = Math.min(deltaCount, module.getPositionDeltas().length); + } + for (int i = 0; i < deltaCount; i++) { + SwerveModulePosition[] wheelDeltas = new SwerveModulePosition[4]; + for (int j = 0; j < 4; j++) { + wheelDeltas[j] = m_modules[j].getPositionDeltas()[i]; + } + + // The twist represents the motion of the robot since the last + // sample in x, y, and theta based only on the modules, without + // the gyro. The gyro is always disconnected in simulation. + var twist = Constants.Drivetrain.m_kinematics.toTwist2d(wheelDeltas); + if (m_gyroInputs.connected) { + // If the gyro is connected, replace the theta component of the twist + // with the change in angle since the last sample. + Rotation2d gyroRotation = m_gyroInputs.odometryYawPositions[i]; + twist = + new Twist2d(twist.dx, twist.dy, gyroRotation.minus(m_lastGyroRotation).getRadians()); + m_lastGyroRotation = gyroRotation; + } + + // Apply the twist (change since last sample) to the current pose + PoseEstimator.getInstance().addDriveData(twist); + } + + Logger.recordOutput("Odometry/EstimatedPose", PoseEstimator.getInstance().getPose()); + } + + /** + * Runs the drive at the desired velocity. + * + * @param speeds Speeds in meters/sec + */ + public void runVelocity(ChassisSpeeds speeds) { + // Calculate module setpoints + ChassisSpeeds discreteSpeeds = ChassisSpeeds.discretize(speeds, 0.02); + SwerveModuleState[] setpointStates = + Constants.Drivetrain.m_kinematics.toSwerveModuleStates(discreteSpeeds); + SwerveDriveKinematics.desaturateWheelSpeeds( + setpointStates, Constants.Drivetrain.kMaxLinearVelocityMetersPerSecond); + + // Send setpoints to modules + SwerveModuleState[] optimizedSetpointStates = new SwerveModuleState[4]; + for (int i = 0; i < 4; i++) { + // The module returns the optimized state, useful for logging + optimizedSetpointStates[i] = m_modules[i].runSetpoint(setpointStates[i]); + } + + // Log setpoint states + Logger.recordOutput("SwerveStates/Setpoints", setpointStates); + Logger.recordOutput("SwerveStates/SetpointsOptimized", optimizedSetpointStates); + } + + /** Stops the drive. */ + public void stop() { + runVelocity(new ChassisSpeeds()); + } + + /** + * Stops the drive and turns the modules to an X arrangement to resist movement. The modules will + * return to their normal orientations the next time a nonzero velocity is requested. + */ + public void stopWithX() { + var translations = Constants.Drivetrain.getModuleTranslations(); + Rotation2d[] headings = new Rotation2d[4]; + for (int i = 0; i < 4; i++) { + headings[i] = translations[i].getAngle(); + } + Constants.Drivetrain.m_kinematics.resetHeadings(headings); + stop(); + } + + /** Returns the module states (turn angles and drive velocities) for all the modules. */ + @AutoLogOutput(key = "SwerveStates/Measured") + private SwerveModuleState[] getModuleStates() { + SwerveModuleState[] states = new SwerveModuleState[4]; + for (int i = 0; i < 4; i++) { + states[i] = m_modules[i].getState(); + } + return states; + } +} diff --git a/src/main/java/frc/robot/subsystems/drive/GyroIO.java b/src/main/java/frc/robot/subsystems/drive/GyroIO.java new file mode 100644 index 0000000..b47de3c --- /dev/null +++ b/src/main/java/frc/robot/subsystems/drive/GyroIO.java @@ -0,0 +1,27 @@ +package frc.robot.subsystems.drive; + +import edu.wpi.first.math.geometry.Rotation2d; +import org.littletonrobotics.junction.AutoLog; + +public interface GyroIO { + @AutoLog + public static class GyroIOInputs { + public boolean connected = false; + + public Rotation2d rollPosition = new Rotation2d(); + public Rotation2d pitchPosition = new Rotation2d(); + public Rotation2d yawPosition = new Rotation2d(); + + public Rotation2d[] odometryYawPositions = new Rotation2d[] {}; + + public double rollVelocityRadPerSec = 0.0; + public double pitchVelocityRadPerSec = 0.0; + public double yawVelocityRadPerSec = 0.0; + + public double accelX = 0.0; + public double accelY = 0.0; + public double accelZ = 0.0; + } + + public default void updateInputs(GyroIOInputs inputs) {} +} diff --git a/src/main/java/frc/robot/subsystems/drive/GyroIOPigeon2.java b/src/main/java/frc/robot/subsystems/drive/GyroIOPigeon2.java new file mode 100644 index 0000000..82e8205 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/drive/GyroIOPigeon2.java @@ -0,0 +1,83 @@ +package frc.robot.subsystems.drive; + +import com.ctre.phoenix6.BaseStatusSignal; +import com.ctre.phoenix6.StatusCode; +import com.ctre.phoenix6.StatusSignal; +import com.ctre.phoenix6.configs.Pigeon2Configuration; +import com.ctre.phoenix6.hardware.Pigeon2; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.util.Units; +import java.util.Queue; + +/** IO implementation for Pigeon2 */ +public class GyroIOPigeon2 implements GyroIO { + private final Pigeon2 m_gyro; + + private final StatusSignal m_roll; + private final StatusSignal m_pitch; + private final StatusSignal m_yaw; + + private final StatusSignal m_rollVelocity; + private final StatusSignal m_pitchVelocity; + private final StatusSignal m_yawVelocity; + + private final StatusSignal m_accelX; + private final StatusSignal m_accelY; + private final StatusSignal m_accelZ; + + private final Queue yawPositionQueue; + + public GyroIOPigeon2(int id) { + this.m_gyro = new Pigeon2(id); + + this.m_gyro.getConfigurator().apply(new Pigeon2Configuration()); + this.m_gyro.getConfigurator().setYaw(0.0); + + this.m_roll = this.m_gyro.getRoll(); + this.m_pitch = this.m_gyro.getPitch(); + this.m_yaw = this.m_gyro.getYaw(); + + this.m_rollVelocity = this.m_gyro.getAngularVelocityX(); + this.m_pitchVelocity = this.m_gyro.getAngularVelocityY(); + this.m_yawVelocity = this.m_gyro.getAngularVelocityZ(); + + this.m_accelX = this.m_gyro.getAccelerationX(); + this.m_accelY = this.m_gyro.getAccelerationY(); + this.m_accelZ = this.m_gyro.getAccelerationZ(); + + // Faster rate for Yaw for Odometry + this.m_yaw.setUpdateFrequency(DriveBase.ODOMETRY_FREQUENCY); + this.m_yawVelocity.setUpdateFrequency(100); + BaseStatusSignal.setUpdateFrequencyForAll( + 50.0, m_roll, m_pitch, m_rollVelocity, m_pitchVelocity, m_accelX, m_accelY, m_accelZ); + m_gyro.optimizeBusUtilization(); + + this.yawPositionQueue = + SparkMaxOdometryThread.getInstance() + .registerSignal(() -> m_gyro.getYaw().getValueAsDouble()); + } + + @Override + public void updateInputs(GyroIOInputs inputs) { + // Only check yaw and yaw velocity as they are needed for odometry + inputs.connected = BaseStatusSignal.refreshAll(m_yaw, m_yawVelocity).equals(StatusCode.OK); + + inputs.rollPosition = Rotation2d.fromDegrees(m_roll.getValueAsDouble()); + inputs.pitchPosition = Rotation2d.fromDegrees(m_pitch.getValueAsDouble()); + inputs.yawPosition = Rotation2d.fromDegrees(m_yaw.getValueAsDouble()); + + inputs.rollVelocityRadPerSec = Units.degreesToRadians(m_rollVelocity.getValueAsDouble()); + inputs.pitchVelocityRadPerSec = Units.degreesToRadians(m_pitchVelocity.getValueAsDouble()); + inputs.yawVelocityRadPerSec = Units.degreesToRadians(m_yawVelocity.getValueAsDouble()); + + inputs.accelX = m_accelX.getValueAsDouble(); + inputs.accelY = m_accelY.getValueAsDouble(); + inputs.accelZ = m_accelZ.getValueAsDouble(); + + inputs.odometryYawPositions = + yawPositionQueue.stream() + .map((Double value) -> Rotation2d.fromDegrees(value)) + .toArray(Rotation2d[]::new); + this.yawPositionQueue.clear(); + } +} diff --git a/src/main/java/frc/robot/subsystems/drive/Module.java b/src/main/java/frc/robot/subsystems/drive/Module.java new file mode 100644 index 0000000..1b94000 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/drive/Module.java @@ -0,0 +1,188 @@ +package frc.robot.subsystems.drive; + +import edu.wpi.first.math.MathUtil; +import edu.wpi.first.math.controller.PIDController; +import edu.wpi.first.math.controller.SimpleMotorFeedforward; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.kinematics.SwerveModulePosition; +import edu.wpi.first.math.kinematics.SwerveModuleState; +import frc.robot.constants.Constants; +import frc.robot.constants.Constants.Drivetrain.DriveCoefficients; +import frc.robot.constants.Constants.Drivetrain.TurnCoefficients; +import org.littletonrobotics.junction.Logger; + +public class Module { + private final ModuleIO m_io; + private final ModuleIOInputsAutoLogged m_inputs = new ModuleIOInputsAutoLogged(); + private final int kModuleIndex; + + private final SimpleMotorFeedforward m_driveFeedforward; + private final PIDController m_driveController; + private final PIDController m_turnController; + + private Rotation2d m_turnAngleSetpoint = null; + private Double m_driveVelocitySetpoint = null; + + private Rotation2d m_turnRelativeOffset = null; + private double m_lastPositionMeters; + private SwerveModulePosition[] positionDeltas = new SwerveModulePosition[] {}; + + public Module(int index, ModuleIO io) { + this.kModuleIndex = index; + this.m_io = io; + + switch (Constants.getRobotMode()) { + case REPLAY, REAL: + m_driveFeedforward = + new SimpleMotorFeedforward( + DriveCoefficients.kS, DriveCoefficients.kV, DriveCoefficients.kA); + m_driveController = + new PIDController(DriveCoefficients.kP, DriveCoefficients.kI, DriveCoefficients.kD); + m_turnController = + new PIDController(TurnCoefficients.kP, TurnCoefficients.kI, TurnCoefficients.kD); + break; + case SIM: + m_driveFeedforward = new SimpleMotorFeedforward(0.0, 0.13); + m_driveController = new PIDController(0.1, 0, 0); + m_turnController = new PIDController(10.0, 0, 0); + break; + default: + m_driveFeedforward = new SimpleMotorFeedforward(0, 0); + m_driveController = new PIDController(0, 0, 0); + m_turnController = new PIDController(0, 0, 0); + break; + } + + m_turnController.enableContinuousInput(-Math.PI, Math.PI); + setBrakeMode(true); + } + + /** Updates the inputs of the module. Should be called before {@link #periodic} */ + public void updateInputs() { + m_io.updateInputs(m_inputs); + } + + public void periodic() { + Logger.processInputs("Drive/Module" + kModuleIndex, m_inputs); + + // On first cycle, reset relative turn encoder + // Wait until absolute angle is nonzero in case it wasn't initialized yet + if (m_turnRelativeOffset == null && m_inputs.turnAbsolutePosition.getRadians() != 0.0) { + m_turnRelativeOffset = m_inputs.turnAbsolutePosition.minus(m_inputs.turnPosition); + } + + // Run closed loop turn control + if (m_turnAngleSetpoint != null) { + m_io.setTurnVoltage( + m_turnController.calculate(getAngle().getRadians(), m_turnAngleSetpoint.getRadians())); + + // Run closed loop drive control. Only if turn closed loop is enabled. + if (m_driveVelocitySetpoint != null) { + // When the error is 90°, the velocity setpoint should be 0. As the wheel turns + // towards the setpoint, its velocity should increase. This is achieved by + // taking the component of the velocity in the direction of the setpoint. + double adjustSpeedSetpoint = + m_driveVelocitySetpoint * Math.cos(m_turnController.getPositionError()); + + // Run drive controller + double velocityRadPerSec = adjustSpeedSetpoint / Constants.Drivetrain.kWheelRadiusMeters; + m_io.setDriveVoltage( + m_driveFeedforward.calculate(velocityRadPerSec) + + m_driveController.calculate(m_inputs.driveVelocityRadPerSec, velocityRadPerSec)); + } + } + + // Calculate position deltas for odometry + int deltaCount = + Math.min(m_inputs.odometryDrivePositionsRad.length, m_inputs.odometryTurnPositions.length); + positionDeltas = new SwerveModulePosition[deltaCount]; + for (int i = 0; i < deltaCount; i++) { + double positionMeters = + m_inputs.odometryDrivePositionsRad[i] * Constants.Drivetrain.kWheelRadiusMeters; + Rotation2d angle = + m_inputs.odometryTurnPositions[i].plus( + m_turnRelativeOffset != null ? m_turnRelativeOffset : new Rotation2d()); + positionDeltas[i] = new SwerveModulePosition(positionMeters - m_lastPositionMeters, angle); + m_lastPositionMeters = positionMeters; + } + } + + /** + * Runs the module with the specified setpoint state. Must be called periodically. Returns the + * optimized state. + */ + public SwerveModuleState runSetpoint(SwerveModuleState state) { + // Optimize state based on current angle + // Controllers run in "periodic" when the setpoint is not null + var optimizedState = SwerveModuleState.optimize(state, getAngle()); + + // Update setpoints, controllers run in "periodic" + m_turnAngleSetpoint = optimizedState.angle; + m_driveVelocitySetpoint = optimizedState.speedMetersPerSecond; + + return optimizedState; + } + + /** + * Set the module output to the specified voltages + * + * @param driveVoltage drive voltage [-12, 12] + * @param turnVoltage turn voltage [-12, 12] + */ + public void runVoltage(double driveVoltage, double turnVoltage) { + m_io.setDriveVoltage(MathUtil.clamp(driveVoltage, -12, 12)); + m_io.setTurnVoltage(MathUtil.clamp(turnVoltage, -12, 12)); + + m_turnAngleSetpoint = null; + m_driveVelocitySetpoint = null; + } + + /** Disables all outputs to motors. */ + public void disable() { + m_io.setTurnVoltage(0.0); + m_io.setDriveVoltage(0.0); + + m_turnAngleSetpoint = null; + m_driveVelocitySetpoint = null; + } + + /** Sets whether brake mode is enabled. */ + public void setBrakeMode(boolean enabled) { + m_io.setDriveBrakeMode(enabled); + m_io.setTurnBrakeMode(enabled); + } + + /** Returns the current turn angle of the module. */ + public Rotation2d getAngle() { + if (m_turnRelativeOffset == null) { + return new Rotation2d(); + } else { + return m_inputs.turnPosition.plus(m_turnRelativeOffset); + } + } + + /** Returns the current drive position of the module in meters. */ + public double getPositionMeters() { + return m_inputs.drivePositionRad * Constants.Drivetrain.kWheelRadiusMeters; + } + + /** Returns the current drive velocity of the module in meters per second. */ + public double getVelocityMetersPerSec() { + return m_inputs.driveVelocityRadPerSec * Constants.Drivetrain.kWheelRadiusMeters; + } + + /** Returns the module position (turn angle and drive position). */ + public SwerveModulePosition getPosition() { + return new SwerveModulePosition(getPositionMeters(), getAngle()); + } + + /** Returns the module state (turn angle and drive velocity). */ + public SwerveModuleState getState() { + return new SwerveModuleState(getVelocityMetersPerSec(), getAngle()); + } + + /** Returns the module position deltas received this cycle. */ + public SwerveModulePosition[] getPositionDeltas() { + return positionDeltas; + } +} diff --git a/src/main/java/frc/robot/subsystems/drive/ModuleIO.java b/src/main/java/frc/robot/subsystems/drive/ModuleIO.java new file mode 100644 index 0000000..684f3cd --- /dev/null +++ b/src/main/java/frc/robot/subsystems/drive/ModuleIO.java @@ -0,0 +1,38 @@ +package frc.robot.subsystems.drive; + +import edu.wpi.first.math.geometry.Rotation2d; +import org.littletonrobotics.junction.AutoLog; + +public interface ModuleIO { + @AutoLog + public static class ModuleIOInputs { + public double drivePositionRad = 0.0; + public double driveVelocityRadPerSec = 0.0; + public double driveAppliedVolts = 0.0; + public double[] driveCurrentAmps = new double[] {}; + + public Rotation2d turnAbsolutePosition = new Rotation2d(); + public Rotation2d turnPosition = new Rotation2d(); + public double turnVelocityRadPerSec = 0.0; + public double turnAppliedVolts = 0.0; + public double[] turnCurrentAmps = new double[] {}; + + public double[] odometryDrivePositionsRad = new double[] {}; + public Rotation2d[] odometryTurnPositions = new Rotation2d[] {}; + } + + /** Updates the set of loggable inputs. */ + public default void updateInputs(ModuleIOInputs inputs) {} + + /** Run the drive motor at the specified voltage. */ + public default void setDriveVoltage(double volts) {} + + /** Run the turn motor at the specified voltage. */ + public default void setTurnVoltage(double volts) {} + + /** Enable or disable brake mode on the drive motor. */ + public default void setDriveBrakeMode(boolean enable) {} + + /** Enable or disable brake mode on the turn motor. */ + public default void setTurnBrakeMode(boolean enable) {} +} diff --git a/src/main/java/frc/robot/subsystems/drive/ModuleIOSim.java b/src/main/java/frc/robot/subsystems/drive/ModuleIOSim.java new file mode 100644 index 0000000..8c17206 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/drive/ModuleIOSim.java @@ -0,0 +1,61 @@ +package frc.robot.subsystems.drive; + +import edu.wpi.first.math.MathUtil; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.system.plant.DCMotor; +import edu.wpi.first.wpilibj.simulation.DCMotorSim; +import frc.robot.constants.Constants; + +/** + * Physics sim implementation of module IO. + * + *

Uses two DCMotor sims for the drive and turn motors, with the absolute position initialized to + * a random value. The DCMotor sims are not physically accurate, but provide a decent approximation + * for the behavior of the module. + */ +public class ModuleIOSim implements ModuleIO { + private final DCMotorSim m_driveSim; + private final DCMotorSim m_turnSim; + + private final Rotation2d turnAbsoluteInitPosition = new Rotation2d(Math.random() * 2.0 * Math.PI); + private double driveAppliedVolts = 0.0; + private double turnAppliedVolts = 0.0; + + public ModuleIOSim() { + this.m_driveSim = new DCMotorSim(DCMotor.getNEO(1), Constants.Drivetrain.kDriveGearing, 0.025); + this.m_turnSim = new DCMotorSim(DCMotor.getNEO(1), Constants.Drivetrain.kTurnGearing, 0.004); + } + + @Override + public void updateInputs(ModuleIOInputs inputs) { + m_driveSim.update(Constants.kLoopPeriodSecs); + m_turnSim.update(Constants.kLoopPeriodSecs); + + inputs.drivePositionRad = m_driveSim.getAngularPositionRad(); + inputs.driveVelocityRadPerSec = m_driveSim.getAngularVelocityRadPerSec(); + inputs.driveAppliedVolts = driveAppliedVolts; + inputs.driveCurrentAmps = new double[] {Math.abs(m_driveSim.getCurrentDrawAmps())}; + + inputs.turnAbsolutePosition = + new Rotation2d(m_turnSim.getAngularPositionRad()).plus(turnAbsoluteInitPosition); + inputs.turnPosition = new Rotation2d(m_turnSim.getAngularPositionRad()); + inputs.turnVelocityRadPerSec = m_turnSim.getAngularVelocityRadPerSec(); + inputs.turnAppliedVolts = turnAppliedVolts; + inputs.turnCurrentAmps = new double[] {Math.abs(m_turnSim.getCurrentDrawAmps())}; + + inputs.odometryDrivePositionsRad = new double[] {inputs.drivePositionRad}; + inputs.odometryTurnPositions = new Rotation2d[] {inputs.turnPosition}; + } + + @Override + public void setDriveVoltage(double volts) { + driveAppliedVolts = MathUtil.clamp(volts, -12.0, 12.0); + m_driveSim.setInputVoltage(driveAppliedVolts); + } + + @Override + public void setTurnVoltage(double volts) { + turnAppliedVolts = MathUtil.clamp(volts, -12.0, 12.0); + m_turnSim.setInputVoltage(turnAppliedVolts); + } +} diff --git a/src/main/java/frc/robot/subsystems/drive/ModuleIOSparkMax.java b/src/main/java/frc/robot/subsystems/drive/ModuleIOSparkMax.java new file mode 100644 index 0000000..ec755fe --- /dev/null +++ b/src/main/java/frc/robot/subsystems/drive/ModuleIOSparkMax.java @@ -0,0 +1,135 @@ +package frc.robot.subsystems.drive; + +import com.ctre.phoenix6.StatusSignal; +import com.ctre.phoenix6.configs.CANcoderConfiguration; +import com.ctre.phoenix6.hardware.CANcoder; +import com.revrobotics.CANSparkMax; +import com.revrobotics.CANSparkMax.IdleMode; +import com.revrobotics.CANSparkMaxLowLevel.MotorType; +import com.revrobotics.CANSparkMaxLowLevel.PeriodicFrame; +import com.revrobotics.RelativeEncoder; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.util.Units; +import frc.robot.constants.Constants; +import java.util.Queue; + +public class ModuleIOSparkMax implements ModuleIO { + private final CANSparkMax m_driveMotor; + private final CANSparkMax m_turnMotor; + private final CANcoder m_absoluteEncoder; + + private final RelativeEncoder m_driveEncoder; + private final RelativeEncoder m_turnRelativeEncoder; + private final StatusSignal m_turnAbsoluteEncoder; + + private final Queue drivePositionQueue; + private final Queue turnPositionQueue; + + public ModuleIOSparkMax(int driveID, int turnID, int encoderID) { + this.m_driveMotor = new CANSparkMax(driveID, MotorType.kBrushless); + this.m_turnMotor = new CANSparkMax(turnID, MotorType.kBrushless); + this.m_absoluteEncoder = new CANcoder(encoderID); + + this.m_driveMotor.restoreFactoryDefaults(); + this.m_turnMotor.restoreFactoryDefaults(); + this.m_absoluteEncoder.getConfigurator().apply(new CANcoderConfiguration()); + + this.m_driveMotor.setCANTimeout(250); + this.m_turnMotor.setCANTimeout(250); + + this.m_driveEncoder = this.m_driveMotor.getEncoder(); + this.m_turnRelativeEncoder = this.m_turnMotor.getEncoder(); + this.m_turnAbsoluteEncoder = this.m_absoluteEncoder.getAbsolutePosition(); + + this.m_turnMotor.setInverted(Constants.Drivetrain.kTurnMotorInverted); + this.m_driveMotor.setSmartCurrentLimit(40); + this.m_turnMotor.setSmartCurrentLimit(30); + this.m_driveMotor.enableVoltageCompensation(12.0); + this.m_turnMotor.enableVoltageCompensation(12.0); + + this.m_driveEncoder.setPosition(0.0); + this.m_driveEncoder.setMeasurementPeriod(10); + this.m_driveEncoder.setAverageDepth(2); + + this.m_turnRelativeEncoder.setPosition(0.0); + this.m_turnRelativeEncoder.setMeasurementPeriod(10); + this.m_turnRelativeEncoder.setAverageDepth(2); + + this.m_turnAbsoluteEncoder.setUpdateFrequency(50); + + this.m_driveMotor.setCANTimeout(0); + this.m_turnMotor.setCANTimeout(0); + + this.m_driveMotor.setPeriodicFramePeriod( + PeriodicFrame.kStatus2, (int) (1000.0 / DriveBase.ODOMETRY_FREQUENCY)); + this.m_turnMotor.setPeriodicFramePeriod( + PeriodicFrame.kStatus2, (int) (1000.0 / DriveBase.ODOMETRY_FREQUENCY)); + this.drivePositionQueue = + SparkMaxOdometryThread.getInstance().registerSignal(m_driveEncoder::getPosition); + this.turnPositionQueue = + SparkMaxOdometryThread.getInstance().registerSignal(m_turnRelativeEncoder::getPosition); + + this.m_driveMotor.burnFlash(); + this.m_turnMotor.burnFlash(); + } + + @Override + public void updateInputs(ModuleIOInputs inputs) { + inputs.drivePositionRad = + Units.rotationsToRadians(m_driveEncoder.getPosition()) / Constants.Drivetrain.kDriveGearing; + inputs.driveVelocityRadPerSec = + Units.rotationsPerMinuteToRadiansPerSecond(m_driveEncoder.getVelocity()) + / Constants.Drivetrain.kDriveGearing; + inputs.driveAppliedVolts = m_driveMotor.getAppliedOutput() * m_driveMotor.getBusVoltage(); + inputs.driveCurrentAmps = new double[] {m_driveMotor.getOutputCurrent()}; + + // Refresh the Encoder data becasue it is cached. This is non-blocking. + m_turnAbsoluteEncoder.refresh(); + inputs.turnAbsolutePosition = + Rotation2d.fromRotations(m_turnAbsoluteEncoder.getValueAsDouble()); + + inputs.turnPosition = + Rotation2d.fromRotations( + m_turnRelativeEncoder.getPosition() / Constants.Drivetrain.kTurnGearing); + inputs.turnVelocityRadPerSec = + Units.rotationsPerMinuteToRadiansPerSecond(m_turnRelativeEncoder.getVelocity()) + / Constants.Drivetrain.kTurnGearing; + inputs.turnAppliedVolts = m_turnMotor.getAppliedOutput() * m_turnMotor.getBusVoltage(); + inputs.turnCurrentAmps = new double[] {m_turnMotor.getOutputCurrent()}; + + inputs.odometryDrivePositionsRad = + this.drivePositionQueue.stream() + .mapToDouble( + (Double value) -> + Units.rotationsToRadians(value) / Constants.Drivetrain.kDriveGearing) + .toArray(); + inputs.odometryTurnPositions = + this.turnPositionQueue.stream() + .map( + (Double value) -> + Rotation2d.fromRotations(value / Constants.Drivetrain.kTurnGearing)) + .toArray(Rotation2d[]::new); + this.drivePositionQueue.clear(); + this.turnPositionQueue.clear(); + } + + @Override + public void setDriveVoltage(double volts) { + m_driveMotor.setVoltage(volts); + } + + @Override + public void setTurnVoltage(double volts) { + m_turnMotor.setVoltage(volts); + } + + @Override + public void setDriveBrakeMode(boolean enable) { + m_driveMotor.setIdleMode(enable ? IdleMode.kBrake : IdleMode.kCoast); + } + + @Override + public void setTurnBrakeMode(boolean enable) { + m_turnMotor.setIdleMode(enable ? IdleMode.kBrake : IdleMode.kCoast); + } +} diff --git a/src/main/java/frc/robot/subsystems/drive/SparkMaxOdometryThread.java b/src/main/java/frc/robot/subsystems/drive/SparkMaxOdometryThread.java new file mode 100644 index 0000000..38fbb2e --- /dev/null +++ b/src/main/java/frc/robot/subsystems/drive/SparkMaxOdometryThread.java @@ -0,0 +1,59 @@ +package frc.robot.subsystems.drive; + +import edu.wpi.first.wpilibj.Notifier; +import frc.robot.util.PoseEstimator; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.function.DoubleSupplier; + +/** + * Provides an interface for asynchronously reading high-frequency measurements to a set of queues. + * + *

This version is intended for devices like the SparkMax that require polling rather than a + * blocking thread. A Notifier thread is used to gather samples with consistent timing. + */ +public class SparkMaxOdometryThread { + private List signals = new ArrayList<>(); + private List> queues = new ArrayList<>(); + + private final Notifier notifier; + private static SparkMaxOdometryThread instance = null; + + public static SparkMaxOdometryThread getInstance() { + if (instance == null) { + instance = new SparkMaxOdometryThread(); + } + return instance; + } + + private SparkMaxOdometryThread() { + notifier = new Notifier(this::periodic); + notifier.setName("SparkMaxOdometryThread"); + notifier.startPeriodic(1.0 / DriveBase.ODOMETRY_FREQUENCY); + } + + public Queue registerSignal(DoubleSupplier signal) { + Queue queue = new ArrayBlockingQueue<>(100); + PoseEstimator.odometryLock.lock(); + try { + signals.add(signal); + queues.add(queue); + } finally { + PoseEstimator.odometryLock.unlock(); + } + return queue; + } + + private void periodic() { + PoseEstimator.odometryLock.lock(); + try { + for (int i = 0; i < signals.size(); i++) { + queues.get(i).offer(signals.get(i).getAsDouble()); + } + } finally { + PoseEstimator.odometryLock.unlock(); + } + } +} diff --git a/src/main/java/frc/robot/util/LoggerUtil.java b/src/main/java/frc/robot/util/LoggerUtil.java new file mode 100644 index 0000000..fbdae2d --- /dev/null +++ b/src/main/java/frc/robot/util/LoggerUtil.java @@ -0,0 +1,27 @@ +package frc.robot.util; + +import edu.wpi.first.wpilibj.RobotBase; +import frc.generated.BuildConstants; +import frc.robot.constants.Constants; +import org.littletonrobotics.junction.Logger; + +public class LoggerUtil { + /** Initialize the Logger with the auto-generated data from the build. */ + public static void initializeLoggerMetadata() { + // Record metadata from generated state file. + Logger.recordMetadata("ROBOT_NAME", Constants.getRobotType().toString()); + Logger.recordMetadata("RUNTIME_ENVIRONMENT", RobotBase.getRuntimeType().toString()); + Logger.recordMetadata("PROJECT_NAME", BuildConstants.MAVEN_NAME); + Logger.recordMetadata("BUILD_DATE", BuildConstants.BUILD_DATE); + Logger.recordMetadata("GIT_SHA", BuildConstants.GIT_SHA); + Logger.recordMetadata("GIT_DATE", BuildConstants.GIT_DATE); + Logger.recordMetadata("GIT_BRANCH", BuildConstants.GIT_BRANCH); + + // Set the current GIT state of the robot (helps manage the logs that are saved). + switch (BuildConstants.DIRTY) { + case 0 -> Logger.recordMetadata("GIT_STATUS", "All changes committed"); + case 1 -> Logger.recordMetadata("GIT_STATUS", "Uncommitted changes"); + default -> Logger.recordMetadata("GIT_STATUS", "Unknown"); + } + } +} diff --git a/src/main/java/frc/robot/util/PolynomialRegression.java b/src/main/java/frc/robot/util/PolynomialRegression.java new file mode 100644 index 0000000..28c5796 --- /dev/null +++ b/src/main/java/frc/robot/util/PolynomialRegression.java @@ -0,0 +1,205 @@ +package frc.robot.util; + +import Jama.Matrix; +import Jama.QRDecomposition; + +// NOTE: This file is available at +// http://algs4.cs.princeton.edu/14analysis/PolynomialRegression.java.html + +/** + * The {@code PolynomialRegression} class performs a polynomial regression on an set of N + * data points (yi, xi). That is, it fits a polynomial + * y = β0 + β1 x + β2 + * x2 + ... + βd xd (where + * y is the response variable, x is the predictor variable, and the + * βi are the regression coefficients) that minimizes the sum of squared + * residuals of the multiple regression model. It also computes associated the coefficient of + * determination R2. + * + *

This implementation performs a QR-decomposition of the underlying Vandermonde matrix, so it is + * neither the fastest nor the most numerically stable way to perform the polynomial regression. + * + * @author Robert Sedgewick + * @author Kevin Wayne + */ +public class PolynomialRegression implements Comparable { + private final String variableName; // name of the predictor variable + private int degree; // degree of the polynomial regression + private Matrix beta; // the polynomial regression coefficients + private double sse; // sum of squares due to error + private double sst; // total sum of squares + + /** + * Performs a polynomial reggression on the data points {@code (y[i], x[i])}. Uses n as the name + * of the predictor variable. + * + * @param x the values of the predictor variable + * @param y the corresponding values of the response variable + * @param degree the degree of the polynomial to fit + * @throws IllegalArgumentException if the lengths of the two arrays are not equal + */ + public PolynomialRegression(double[] x, double[] y, int degree) { + this(x, y, degree, "n"); + } + + /** + * Performs a polynomial reggression on the data points {@code (y[i], x[i])}. + * + * @param x the values of the predictor variable + * @param y the corresponding values of the response variable + * @param degree the degree of the polynomial to fit + * @param variableName the name of the predictor variable + * @throws IllegalArgumentException if the lengths of the two arrays are not equal + */ + public PolynomialRegression(double[] x, double[] y, int degree, String variableName) { + this.degree = degree; + this.variableName = variableName; + + int n = x.length; + QRDecomposition qr = null; + Matrix matrixX = null; + + // in case Vandermonde matrix does not have full rank, reduce degree until it + // does + while (true) { + + // build Vandermonde matrix + double[][] vandermonde = new double[n][this.degree + 1]; + for (int i = 0; i < n; i++) { + for (int j = 0; j <= this.degree; j++) { + vandermonde[i][j] = Math.pow(x[i], j); + } + } + matrixX = new Matrix(vandermonde); + + // find least squares solution + qr = new QRDecomposition(matrixX); + if (qr.isFullRank()) break; + + // decrease degree and try again + this.degree--; + } + + // create matrix from vector + Matrix matrixY = new Matrix(y, n); + + // linear regression coefficients + beta = qr.solve(matrixY); + + // mean of y[] values + double sum = 0.0; + for (int i = 0; i < n; i++) sum += y[i]; + double mean = sum / n; + + // total variation to be accounted for + for (int i = 0; i < n; i++) { + double dev = y[i] - mean; + sst += dev * dev; + } + + // variation not accounted for + Matrix residuals = matrixX.times(beta).minus(matrixY); + sse = residuals.norm2() * residuals.norm2(); + } + + /** + * Returns the {@code j}th regression coefficient. + * + * @param j the index + * @return the {@code j}th regression coefficient + */ + public double beta(int j) { + // to make -0.0 print as 0.0 + if (Math.abs(beta.get(j, 0)) < 1E-4) return 0.0; + return beta.get(j, 0); + } + + /** + * Returns the degree of the polynomial to fit. + * + * @return the degree of the polynomial to fit + */ + public int degree() { + return degree; + } + + /** + * Returns the coefficient of determination R2. + * + * @return the coefficient of determination R2, which is a real number between + * 0 and 1 + */ + public double R2() { + if (sst == 0.0) return 1.0; // constant function + return 1.0 - sse / sst; + } + + /** + * Returns the expected response {@code y} given the value of the predictor variable {@code x}. + * + * @param x the value of the predictor variable + * @return the expected response {@code y} given the value of the predictor variable {@code x} + */ + public double predict(double x) { + // horner's method + double y = 0.0; + for (int j = degree; j >= 0; j--) y = beta(j) + (x * y); + return y; + } + + /** + * Returns a string representation of the polynomial regression model. + * + * @return a string representation of the polynomial regression model, including the best-fit + * polynomial and the coefficient of determination R2 + */ + public String toString() { + StringBuilder s = new StringBuilder(); + int j = degree; + + // ignoring leading zero coefficients + while (j >= 0 && Math.abs(beta(j)) < 1E-5) j--; + + // create remaining terms + while (j >= 0) { + if (j == 0) s.append(String.format("%.10f ", beta(j))); + else if (j == 1) s.append(String.format("%.10f %s + ", beta(j), variableName)); + else s.append(String.format("%.10f %s^%d + ", beta(j), variableName, j)); + j--; + } + s = s.append(" (R^2 = " + String.format("%.3f", R2()) + ")"); + + // replace "+ -2n" with "- 2n" + return s.toString().replace("+ -", "- "); + } + + /** Compare lexicographically. */ + public int compareTo(PolynomialRegression that) { + double EPSILON = 1E-5; + int maxDegree = Math.max(this.degree(), that.degree()); + for (int j = maxDegree; j >= 0; j--) { + double term1 = 0.0; + double term2 = 0.0; + if (this.degree() >= j) term1 = this.beta(j); + if (that.degree() >= j) term2 = that.beta(j); + if (Math.abs(term1) < EPSILON) term1 = 0.0; + if (Math.abs(term2) < EPSILON) term2 = 0.0; + if (term1 < term2) return -1; + else if (term1 > term2) return +1; + } + return 0; + } + + /** + * Unit tests the {@code PolynomialRegression} data type. + * + * @param args the command-line arguments + */ + public static void main(String[] args) { + double[] x = {10, 20, 40, 80, 160, 200}; + double[] y = {100, 350, 1500, 6700, 20160, 40000}; + PolynomialRegression regression = new PolynomialRegression(x, y, 3); + + System.out.println(regression); + } +} diff --git a/src/main/java/frc/robot/util/PoseEstimator.java b/src/main/java/frc/robot/util/PoseEstimator.java new file mode 100644 index 0000000..08438c8 --- /dev/null +++ b/src/main/java/frc/robot/util/PoseEstimator.java @@ -0,0 +1,33 @@ +package frc.robot.util; + +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Twist2d; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +public class PoseEstimator { + private static PoseEstimator instance; + + public static PoseEstimator getInstance() { + if (instance == null) { + instance = new PoseEstimator(); + } + + return instance; + } + + public static final Lock odometryLock = new ReentrantLock(); + private Pose2d pose = new Pose2d(); + + public Pose2d getPose() { + return pose; + } + + public void resetPose(Pose2d pose) { + this.pose = pose; + } + + public void addDriveData(Twist2d twist) { + this.pose = pose.exp(twist); + } +} diff --git a/vendordeps/AdvantageKit.json b/vendordeps/AdvantageKit.json index fa08705..261dc8f 100644 --- a/vendordeps/AdvantageKit.json +++ b/vendordeps/AdvantageKit.json @@ -1,41 +1,42 @@ { - "fileName": "AdvantageKit.json", - "name": "AdvantageKit", - "version": "2.2.4", - "uuid": "d820cc26-74e3-11ec-90d6-0242ac120003", - "mavenUrls": [], - "jsonUrl": "https://github.com/Mechanical-Advantage/AdvantageKit/releases/latest/download/AdvantageKit.json", - "javaDependencies": [ - { - "groupId": "org.littletonrobotics.akit.junction", - "artifactId": "wpilib-shim", - "version": "2.2.4" - }, - { - "groupId": "org.littletonrobotics.akit.junction", - "artifactId": "junction-core", - "version": "2.2.4" - }, - { - "groupId": "org.littletonrobotics.akit.conduit", - "artifactId": "conduit-api", - "version": "2.2.4" - } - ], - "jniDependencies": [ - { - "groupId": "org.littletonrobotics.akit.conduit", - "artifactId": "conduit-wpilibio", - "version": "2.2.4", - "skipInvalidPlatforms": false, - "isJar": false, - "validPlatforms": [ - "linuxathena", - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ] - } - ], - "cppDependencies": [] -} \ No newline at end of file + "fileName": "AdvantageKit.json", + "name": "AdvantageKit", + "version": "3.0.0-beta-5", + "uuid": "d820cc26-74e3-11ec-90d6-0242ac120003", + "frcYear": "2024", + "mavenUrls": [], + "jsonUrl": "https://github.com/Mechanical-Advantage/AdvantageKit/releases/latest/download/AdvantageKit.json", + "javaDependencies": [ + { + "groupId": "org.littletonrobotics.akit.junction", + "artifactId": "wpilib-shim", + "version": "3.0.0-beta-5" + }, + { + "groupId": "org.littletonrobotics.akit.junction", + "artifactId": "junction-core", + "version": "3.0.0-beta-5" + }, + { + "groupId": "org.littletonrobotics.akit.conduit", + "artifactId": "conduit-api", + "version": "3.0.0-beta-5" + } + ], + "jniDependencies": [ + { + "groupId": "org.littletonrobotics.akit.conduit", + "artifactId": "conduit-wpilibio", + "version": "3.0.0-beta-5", + "skipInvalidPlatforms": false, + "isJar": false, + "validPlatforms": [ + "linuxathena", + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ] + } + ], + "cppDependencies": [] +} diff --git a/vendordeps/Phoenix.json b/vendordeps/Phoenix.json deleted file mode 100644 index 04bb659..0000000 --- a/vendordeps/Phoenix.json +++ /dev/null @@ -1,423 +0,0 @@ -{ - "fileName": "Phoenix.json", - "name": "CTRE-Phoenix (v5)", - "version": "5.30.4+23.0.11", - "frcYear": 2023, - "uuid": "ab676553-b602-441f-a38d-f1296eff6537", - "mavenUrls": [ - "https://maven.ctr-electronics.com/release/" - ], - "jsonUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix/Phoenix5-frc2023-latest.json", - "javaDependencies": [ - { - "groupId": "com.ctre.phoenix", - "artifactId": "api-java", - "version": "5.30.4" - }, - { - "groupId": "com.ctre.phoenix", - "artifactId": "wpiapi-java", - "version": "5.30.4" - } - ], - "jniDependencies": [ - { - "groupId": "com.ctre.phoenix", - "artifactId": "cci", - "version": "5.30.4", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "linuxathena" - ], - "simMode": "hwsim" - }, - { - "groupId": "com.ctre.phoenix.sim", - "artifactId": "cci-sim", - "version": "5.30.4", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenixpro", - "artifactId": "tools", - "version": "23.0.11", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "linuxathena" - ], - "simMode": "hwsim" - }, - { - "groupId": "com.ctre.phoenixpro.sim", - "artifactId": "tools-sim", - "version": "23.0.11", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenixpro.sim", - "artifactId": "simTalonSRX", - "version": "23.0.11", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenixpro.sim", - "artifactId": "simTalonFX", - "version": "23.0.11", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenixpro.sim", - "artifactId": "simVictorSPX", - "version": "23.0.11", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenixpro.sim", - "artifactId": "simPigeonIMU", - "version": "23.0.11", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenixpro.sim", - "artifactId": "simCANCoder", - "version": "23.0.11", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenixpro.sim", - "artifactId": "simProTalonFX", - "version": "23.0.11", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenixpro.sim", - "artifactId": "simProCANcoder", - "version": "23.0.11", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenixpro.sim", - "artifactId": "simProPigeon2", - "version": "23.0.11", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - } - ], - "cppDependencies": [ - { - "groupId": "com.ctre.phoenix", - "artifactId": "wpiapi-cpp", - "version": "5.30.4", - "libName": "CTRE_Phoenix_WPI", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "linuxathena" - ], - "simMode": "hwsim" - }, - { - "groupId": "com.ctre.phoenix", - "artifactId": "api-cpp", - "version": "5.30.4", - "libName": "CTRE_Phoenix", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "linuxathena" - ], - "simMode": "hwsim" - }, - { - "groupId": "com.ctre.phoenix", - "artifactId": "cci", - "version": "5.30.4", - "libName": "CTRE_PhoenixCCI", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "linuxathena" - ], - "simMode": "hwsim" - }, - { - "groupId": "com.ctre.phoenixpro", - "artifactId": "tools", - "version": "23.0.11", - "libName": "CTRE_PhoenixTools", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "linuxathena" - ], - "simMode": "hwsim" - }, - { - "groupId": "com.ctre.phoenix.sim", - "artifactId": "wpiapi-cpp-sim", - "version": "5.30.4", - "libName": "CTRE_Phoenix_WPISim", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix.sim", - "artifactId": "api-cpp-sim", - "version": "5.30.4", - "libName": "CTRE_PhoenixSim", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix.sim", - "artifactId": "cci-sim", - "version": "5.30.4", - "libName": "CTRE_PhoenixCCISim", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenixpro.sim", - "artifactId": "tools-sim", - "version": "23.0.11", - "libName": "CTRE_PhoenixTools_Sim", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenixpro.sim", - "artifactId": "simTalonSRX", - "version": "23.0.11", - "libName": "CTRE_SimTalonSRX", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenixpro.sim", - "artifactId": "simTalonFX", - "version": "23.0.11", - "libName": "CTRE_SimTalonFX", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenixpro.sim", - "artifactId": "simVictorSPX", - "version": "23.0.11", - "libName": "CTRE_SimVictorSPX", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenixpro.sim", - "artifactId": "simPigeonIMU", - "version": "23.0.11", - "libName": "CTRE_SimPigeonIMU", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenixpro.sim", - "artifactId": "simCANCoder", - "version": "23.0.11", - "libName": "CTRE_SimCANCoder", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenixpro.sim", - "artifactId": "simProTalonFX", - "version": "23.0.11", - "libName": "CTRE_SimProTalonFX", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenixpro.sim", - "artifactId": "simProCANcoder", - "version": "23.0.11", - "libName": "CTRE_SimProCANcoder", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenixpro.sim", - "artifactId": "simProPigeon2", - "version": "23.0.11", - "libName": "CTRE_SimProPigeon2", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - } - ] -} \ No newline at end of file diff --git a/vendordeps/Phoenix6.json b/vendordeps/Phoenix6.json new file mode 100644 index 0000000..3f79af2 --- /dev/null +++ b/vendordeps/Phoenix6.json @@ -0,0 +1,339 @@ +{ + "fileName": "Phoenix6.json", + "name": "CTRE-Phoenix (v6)", + "version": "24.0.0-beta-4", + "frcYear": 2024, + "uuid": "e995de00-2c64-4df5-8831-c1441420ff19", + "mavenUrls": [ + "https://maven.ctr-electronics.com/release/" + ], + "jsonUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix6/latest/Phoenix6-frc2024-beta-latest.json", + "conflictsWith": [ + { + "uuid": "3fcf3402-e646-4fa6-971e-18afe8173b1a", + "errorMessage": "The combined Phoenix-6-And-5 vendordep is no longer supported. Please remove the vendordep and instead add both the latest Phoenix 6 vendordep and Phoenix 5 vendordep.", + "offlineFileName": "Phoenix6And5.json" + } + ], + "javaDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "wpiapi-java", + "version": "24.0.0-beta-4" + } + ], + "jniDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "tools", + "version": "24.0.0-beta-4", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "tools-sim", + "version": "24.0.0-beta-4", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonSRX", + "version": "24.0.0-beta-4", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonFX", + "version": "24.0.0-beta-4", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simVictorSPX", + "version": "24.0.0-beta-4", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simPigeonIMU", + "version": "24.0.0-beta-4", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simCANCoder", + "version": "24.0.0-beta-4", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProTalonFX", + "version": "24.0.0-beta-4", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANcoder", + "version": "24.0.0-beta-4", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProPigeon2", + "version": "24.0.0-beta-4", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + } + ], + "cppDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "wpiapi-cpp", + "version": "24.0.0-beta-4", + "libName": "CTRE_Phoenix6_WPI", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6", + "artifactId": "tools", + "version": "24.0.0-beta-4", + "libName": "CTRE_PhoenixTools", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "wpiapi-cpp-sim", + "version": "24.0.0-beta-4", + "libName": "CTRE_Phoenix6_WPISim", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "tools-sim", + "version": "24.0.0-beta-4", + "libName": "CTRE_PhoenixTools_Sim", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonSRX", + "version": "24.0.0-beta-4", + "libName": "CTRE_SimTalonSRX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonFX", + "version": "24.0.0-beta-4", + "libName": "CTRE_SimTalonFX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simVictorSPX", + "version": "24.0.0-beta-4", + "libName": "CTRE_SimVictorSPX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simPigeonIMU", + "version": "24.0.0-beta-4", + "libName": "CTRE_SimPigeonIMU", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simCANCoder", + "version": "24.0.0-beta-4", + "libName": "CTRE_SimCANCoder", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProTalonFX", + "version": "24.0.0-beta-4", + "libName": "CTRE_SimProTalonFX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANcoder", + "version": "24.0.0-beta-4", + "libName": "CTRE_SimProCANcoder", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProPigeon2", + "version": "24.0.0-beta-4", + "libName": "CTRE_SimProPigeon2", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + } + ] +} diff --git a/vendordeps/REVLib.json b/vendordeps/REVLib.json index f2d0b7d..cfafaba 100644 --- a/vendordeps/REVLib.json +++ b/vendordeps/REVLib.json @@ -1,73 +1,74 @@ { - "fileName": "REVLib.json", - "name": "REVLib", - "version": "2023.1.3", - "uuid": "3f48eb8c-50fe-43a6-9cb7-44c86353c4cb", - "mavenUrls": [ - "https://maven.revrobotics.com/" - ], - "jsonUrl": "https://software-metadata.revrobotics.com/REVLib-2023.json", - "javaDependencies": [ - { - "groupId": "com.revrobotics.frc", - "artifactId": "REVLib-java", - "version": "2023.1.3" - } - ], - "jniDependencies": [ - { - "groupId": "com.revrobotics.frc", - "artifactId": "REVLib-driver", - "version": "2023.1.3", - "skipInvalidPlatforms": true, - "isJar": false, - "validPlatforms": [ - "windowsx86-64", - "windowsx86", - "linuxarm64", - "linuxx86-64", - "linuxathena", - "linuxarm32", - "osxuniversal" - ] - } - ], - "cppDependencies": [ - { - "groupId": "com.revrobotics.frc", - "artifactId": "REVLib-cpp", - "version": "2023.1.3", - "libName": "REVLib", - "headerClassifier": "headers", - "sharedLibrary": false, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "windowsx86", - "linuxarm64", - "linuxx86-64", - "linuxathena", - "linuxarm32", - "osxuniversal" - ] - }, - { - "groupId": "com.revrobotics.frc", - "artifactId": "REVLib-driver", - "version": "2023.1.3", - "libName": "REVLibDriver", - "headerClassifier": "headers", - "sharedLibrary": false, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "windowsx86", - "linuxarm64", - "linuxx86-64", - "linuxathena", - "linuxarm32", - "osxuniversal" - ] - } - ] -} \ No newline at end of file + "fileName": "REVLib.json", + "name": "REVLib", + "version": "2024.0.0", + "frcYear": "2024", + "uuid": "3f48eb8c-50fe-43a6-9cb7-44c86353c4cb", + "mavenUrls": [ + "https://maven.revrobotics.com/" + ], + "jsonUrl": "https://software-metadata.revrobotics.com/REVLib-2024.json", + "javaDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-java", + "version": "2024.0.0" + } + ], + "jniDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-driver", + "version": "2024.0.0", + "skipInvalidPlatforms": true, + "isJar": false, + "validPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + } + ], + "cppDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-cpp", + "version": "2024.0.0", + "libName": "REVLib", + "headerClassifier": "headers", + "sharedLibrary": false, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + }, + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-driver", + "version": "2024.0.0", + "libName": "REVLibDriver", + "headerClassifier": "headers", + "sharedLibrary": false, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + } + ] +} diff --git a/vendordeps/WPILibNewCommands.json b/vendordeps/WPILibNewCommands.json index 0f59e5b..67bf389 100644 --- a/vendordeps/WPILibNewCommands.json +++ b/vendordeps/WPILibNewCommands.json @@ -1,37 +1,38 @@ -{ - "fileName": "WPILibNewCommands.json", - "name": "WPILib-New-Commands", - "version": "1.0.0", - "uuid": "111e20f7-815e-48f8-9dd6-e675ce75b266", - "mavenUrls": [], - "jsonUrl": "", - "javaDependencies": [ - { - "groupId": "edu.wpi.first.wpilibNewCommands", - "artifactId": "wpilibNewCommands-java", - "version": "wpilib" - } - ], - "jniDependencies": [], - "cppDependencies": [ - { - "groupId": "edu.wpi.first.wpilibNewCommands", - "artifactId": "wpilibNewCommands-cpp", - "version": "wpilib", - "libName": "wpilibNewCommands", - "headerClassifier": "headers", - "sourcesClassifier": "sources", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "linuxathena", - "linuxarm32", - "linuxarm64", - "windowsx86-64", - "windowsx86", - "linuxx86-64", - "osxuniversal" - ] - } - ] -} \ No newline at end of file +{ + "fileName": "WPILibNewCommands.json", + "name": "WPILib-New-Commands", + "version": "1.0.0", + "uuid": "111e20f7-815e-48f8-9dd6-e675ce75b266", + "frcYear": "2024", + "mavenUrls": [], + "jsonUrl": "", + "javaDependencies": [ + { + "groupId": "edu.wpi.first.wpilibNewCommands", + "artifactId": "wpilibNewCommands-java", + "version": "wpilib" + } + ], + "jniDependencies": [], + "cppDependencies": [ + { + "groupId": "edu.wpi.first.wpilibNewCommands", + "artifactId": "wpilibNewCommands-cpp", + "version": "wpilib", + "libName": "wpilibNewCommands", + "headerClassifier": "headers", + "sourcesClassifier": "sources", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "linuxathena", + "linuxarm32", + "linuxarm64", + "windowsx86-64", + "windowsx86", + "linuxx86-64", + "osxuniversal" + ] + } + ] +} From 0dfeb6f309981b0263d51637a0b3e02db37284f1 Mon Sep 17 00:00:00 2001 From: Sriman Achanta <68172138+srimanachanta@users.noreply.github.com> Date: Tue, 5 Dec 2023 23:42:42 -0500 Subject: [PATCH 2/2] reimplement FF characterization command --- src/main/java/frc/robot/RobotContainer.java | 9 +++++---- .../drive/FeedForwardCharacterization.java | 6 ------ .../frc/robot/subsystems/drive/DriveBase.java | 14 +++++++++++++ .../frc/robot/subsystems/drive/Module.java | 20 +++++++++---------- 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java index b87aeec..6418f67 100644 --- a/src/main/java/frc/robot/RobotContainer.java +++ b/src/main/java/frc/robot/RobotContainer.java @@ -4,6 +4,7 @@ import edu.wpi.first.wpilibj2.command.Commands; import edu.wpi.first.wpilibj2.command.button.CommandPS4Controller; import frc.robot.commands.drive.DriveCommandFactory; +import frc.robot.commands.drive.FeedForwardCharacterization; import frc.robot.constants.Constants; import frc.robot.constants.HardwareIds; import frc.robot.subsystems.drive.*; @@ -65,10 +66,10 @@ public RobotContainer() { } // // Set up FF characterization routines - // autoChooser.addOption( - // "DriveBase FF Characterization", - // new FeedForwardCharacterization( - // m_drive, m_drive::runCharacterizationVolts, m_drive::getCharacterizationVelocity)); + autoChooser.addOption( + "DriveBase FF Characterization", + new FeedForwardCharacterization( + m_drive, m_drive::runCharacterizationVolts, m_drive::getCharacterizationVelocity)); // Configure the button bindings configureButtonBindings(); diff --git a/src/main/java/frc/robot/commands/drive/FeedForwardCharacterization.java b/src/main/java/frc/robot/commands/drive/FeedForwardCharacterization.java index 0fd234f..8f5e314 100644 --- a/src/main/java/frc/robot/commands/drive/FeedForwardCharacterization.java +++ b/src/main/java/frc/robot/commands/drive/FeedForwardCharacterization.java @@ -68,12 +68,6 @@ public void end(boolean interrupted) { data.print(); } - // Returns true when the command should end. - @Override - public boolean isFinished() { - return false; - } - public static class FeedForwardCharacterizationData { private final List velocityData = new LinkedList<>(); private final List voltageData = new LinkedList<>(); diff --git a/src/main/java/frc/robot/subsystems/drive/DriveBase.java b/src/main/java/frc/robot/subsystems/drive/DriveBase.java index 250a444..cbb8461 100644 --- a/src/main/java/frc/robot/subsystems/drive/DriveBase.java +++ b/src/main/java/frc/robot/subsystems/drive/DriveBase.java @@ -145,4 +145,18 @@ private SwerveModuleState[] getModuleStates() { } return states; } + + public void runCharacterizationVolts(double volts) { + for (var module : m_modules) { + module.runCharacterization(volts); + } + } + + public double getCharacterizationVelocity() { + double driveVelocityAverage = 0.0; + for (var module : m_modules) { + driveVelocityAverage += module.getCharacterizationVelocity(); + } + return driveVelocityAverage / 4.0; + } } diff --git a/src/main/java/frc/robot/subsystems/drive/Module.java b/src/main/java/frc/robot/subsystems/drive/Module.java index 1b94000..61a3c0f 100644 --- a/src/main/java/frc/robot/subsystems/drive/Module.java +++ b/src/main/java/frc/robot/subsystems/drive/Module.java @@ -1,6 +1,5 @@ package frc.robot.subsystems.drive; -import edu.wpi.first.math.MathUtil; import edu.wpi.first.math.controller.PIDController; import edu.wpi.first.math.controller.SimpleMotorFeedforward; import edu.wpi.first.math.geometry.Rotation2d; @@ -123,20 +122,19 @@ public SwerveModuleState runSetpoint(SwerveModuleState state) { return optimizedState; } - /** - * Set the module output to the specified voltages - * - * @param driveVoltage drive voltage [-12, 12] - * @param turnVoltage turn voltage [-12, 12] - */ - public void runVoltage(double driveVoltage, double turnVoltage) { - m_io.setDriveVoltage(MathUtil.clamp(driveVoltage, -12, 12)); - m_io.setTurnVoltage(MathUtil.clamp(turnVoltage, -12, 12)); + public void runCharacterization(double volts) { + // Closed loop turn control + m_turnAngleSetpoint = new Rotation2d(); - m_turnAngleSetpoint = null; + // Open loop drive control + m_io.setDriveVoltage(volts); m_driveVelocitySetpoint = null; } + public double getCharacterizationVelocity() { + return m_inputs.driveVelocityRadPerSec; + } + /** Disables all outputs to motors. */ public void disable() { m_io.setTurnVoltage(0.0);