diff --git a/.github/workflows/pullgompeilib.yml b/.github/workflows/pullgompeilib.yml index a83362f9..e8790377 100644 --- a/.github/workflows/pullgompeilib.yml +++ b/.github/workflows/pullgompeilib.yml @@ -5,7 +5,7 @@ on: jobs: sync: - uses: Team-190/CI-Workflows/.github/workflows/syncgompeilib.yml@main + uses: Team-190/CI-Workflows/.github/workflows/syncgompeilib.yaml@main with: direction: pull secrets: diff --git a/.github/workflows/pushgompeilib.yml b/.github/workflows/pushgompeilib.yml index f3759bb2..84ba499f 100644 --- a/.github/workflows/pushgompeilib.yml +++ b/.github/workflows/pushgompeilib.yml @@ -6,7 +6,7 @@ on: jobs: sync: - uses: Team-190/CI-Workflows/.github/workflows/syncgompeilib.yml@main + uses: Team-190/CI-Workflows/.github/workflows/syncgompeilib.yaml@main with: direction: push secrets: diff --git a/advantagescope_assets/Robot_Delta/config.json b/advantagescope_assets/Robot_Delta/config.json deleted file mode 100644 index 6e4dcff9..00000000 --- a/advantagescope_assets/Robot_Delta/config.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "Delta", - "sourceUrl": "https://frc190.onshape.com/documents/ba8a365713d40fc24cf0b54f/w/90379dd84e9a15f2f3a15787/e/f7562994bcc6d5175e8e4e30", - "disableSimplification": true, - "rotations": [ - { - "axis": "x", - "degrees": 90 - }, - { - "axis": "z", - "degrees": 90 - } - ], - "position": [ - 0, - 0, - 0 - ], - "cameras": [], - "components": [] -} diff --git a/advantagescope_assets/Robot_Delta/model_6.glb b/advantagescope_assets/Robot_Delta/model_6.glb deleted file mode 100644 index 2bf95158..00000000 Binary files a/advantagescope_assets/Robot_Delta/model_6.glb and /dev/null differ diff --git a/advantagescope_assets/Robot_Turnover/config.json b/advantagescope_assets/Robot_Turnover/config.json new file mode 100644 index 00000000..ed744fb3 --- /dev/null +++ b/advantagescope_assets/Robot_Turnover/config.json @@ -0,0 +1,142 @@ +{ + "name": "Turnover", + "sourceUrl": "https://frc190.onshape.com/documents/ba8a365713d40fc24cf0b54f/w/90379dd84e9a15f2f3a15787/e/f7562994bcc6d5175e8e4e30", + "disableSimplification": true, + "rotations": [ + { + "axis": "x", + "degrees": 90 + }, + { + "axis": "z", + "degrees": 90 + } + ], + "position": [ + 0, + 0, + 0 + ], + "cameras": [], + "components": [ + { + "zeroedRotations": [ + { + "axis": "x", + "degrees": 90 + }, + { + "axis": "z", + "degrees": 90 + } + ], + "zeroedPosition": [ + 0.017463, + 0.163513, + -0.371475 + ] + }, + { + "zeroedRotations": [ + { + "axis": "x", + "degrees": 90 + }, + { + "axis": "z", + "degrees": 90 + } + ], + "zeroedPosition": [ + -0.043168, + 0.1635125, + -0.474975 + ] + }, + { + "zeroedRotations": [ + { + "axis": "x", + "degrees": 90 + }, + { + "axis": "z", + "degrees": 90 + } + ], + "zeroedPosition": [ + 0.317482, + -0.090043, + -0.477114 + ] + }, + { + "zeroedRotations": [ + { + "axis": "x", + "degrees": 90 + }, + { + "axis": "z", + "degrees": 90 + } + ], + "zeroedPosition": [ + 0, + 0, + 0 + ] + }, + { + "zeroedRotations": [ + { + "axis": "x", + "degrees": 90 + }, + { + "axis": "z", + "degrees": 90 + } + ], + "zeroedPosition": [ + -0.139700, + 0, + -0.254000 + ] + }, + { + "zeroedRotations": [ + { + "axis": "x", + "degrees": 90 + }, + { + "axis": "z", + "degrees": 90 + } + ], + "zeroedPosition": [ + 0.003638, + 0, + -0.335929 + ] + }, + { + "zeroedRotations": [ + { + "axis": "x", + "degrees": 90 + }, + { + "axis": "z", + "degrees": 90 + } + ], + "zeroedPosition": [ + -0.292100, + 0, + -0.171450 + ] + } + ] +} diff --git a/advantagescope_assets/Robot_Delta/model.glb b/advantagescope_assets/Robot_Turnover/model.glb similarity index 100% rename from advantagescope_assets/Robot_Delta/model.glb rename to advantagescope_assets/Robot_Turnover/model.glb diff --git a/advantagescope_assets/Robot_Delta/model_0.glb b/advantagescope_assets/Robot_Turnover/model_0.glb similarity index 100% rename from advantagescope_assets/Robot_Delta/model_0.glb rename to advantagescope_assets/Robot_Turnover/model_0.glb diff --git a/advantagescope_assets/Robot_Delta/model_1.glb b/advantagescope_assets/Robot_Turnover/model_1.glb similarity index 100% rename from advantagescope_assets/Robot_Delta/model_1.glb rename to advantagescope_assets/Robot_Turnover/model_1.glb diff --git a/advantagescope_assets/Robot_Delta/model_2.glb b/advantagescope_assets/Robot_Turnover/model_2.glb similarity index 100% rename from advantagescope_assets/Robot_Delta/model_2.glb rename to advantagescope_assets/Robot_Turnover/model_2.glb diff --git a/advantagescope_assets/Robot_Delta/model_3.glb b/advantagescope_assets/Robot_Turnover/model_3.glb similarity index 100% rename from advantagescope_assets/Robot_Delta/model_3.glb rename to advantagescope_assets/Robot_Turnover/model_3.glb diff --git a/advantagescope_assets/Robot_Delta/model_4.glb b/advantagescope_assets/Robot_Turnover/model_4.glb similarity index 100% rename from advantagescope_assets/Robot_Delta/model_4.glb rename to advantagescope_assets/Robot_Turnover/model_4.glb diff --git a/advantagescope_assets/Robot_Delta/model_5.glb b/advantagescope_assets/Robot_Turnover/model_5.glb similarity index 100% rename from advantagescope_assets/Robot_Delta/model_5.glb rename to advantagescope_assets/Robot_Turnover/model_5.glb diff --git a/advantagescope_assets/Robot_Turnover/model_6.glb b/advantagescope_assets/Robot_Turnover/model_6.glb new file mode 100644 index 00000000..6b6d685b Binary files /dev/null and b/advantagescope_assets/Robot_Turnover/model_6.glb differ diff --git a/build.gradle b/build.gradle index 175ebe8a..bee0445e 100755 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { id "com.peterabeles.gversion" version "1.10" id "io.freefair.lombok" version "8.4" id "com.diffplug.spotless" version "6.25.0" - id "io.freefair.aspectj.post-compile-weaving" version "8.4" + id "io.freefair.aspectj.post-compile-weaving" version "8.6" } ext { @@ -111,65 +111,11 @@ deploy { } } -def getGompeiVersionFromSource() { - def configFile = file("src/main/java/frc/robot/RobotConfig.java") - if (!configFile.exists()) { - logger.warn("WARNING: No RobotConfig.java class found, falling back to latest GompeiLib package") - return "2026+" - } else { - logger.info("INFO: RobotConfig.java class found") - } - - def content = configFile.text - - def robotMatcher = content =~ /ROBOT\s*=\s*RobotType\.(\w+);/ - if (robotMatcher.find()) { - def activeRobot = robotMatcher.group(1) - - def versionMatcher = content =~ /${activeRobot}\("([^"]+)"\)/ - if (versionMatcher.find()) { - return versionMatcher.group(1) - } - } - return "0.0.0-SNAPSHOT" -} - -def GLibRepoPath = "" - repositories { - - def gprUser = project.findProperty("gpr.user") - def gprKey = project.findProperty("gpr.key") - - if (!useLocalGompeiLib && gprUser != null && gprKey != null) { - // Use GitHub Packages if credentials exist - exclusiveContent { - forRepository { - maven { - name = "GompeiLibGitHubPackages" - url = uri("https://maven.pkg.github.com/Team-190/GompeiLib") - credentials { - username = gprUser - password = gprKey - } - } - } - filter { - includeGroup "edu.wpi.team190" - } - } - def gompeiLibVersion = getGompeiVersionFromSource() - GLibRepoPath = "edu.wpi.team190:gompeilib:${gompeiLibVersion}" - } else { - logger.warn("WARNING: Using local GompeiLib build.") - - mavenLocal() - GLibRepoPath = "edu.wpi.team190:gompeilib:0.0.0-SNAPSHOT" - } + mavenCentral() } // Grab the roboRIO comment via SSH - import com.jcraft.jsch.JSch import edu.wpi.first.gradlerio.GradleRIOPlugin import groovy.json.JsonSlurper @@ -328,12 +274,12 @@ dependencies { implementation 'org.jgrapht:jgrapht-core:1.5.1' implementation 'org.jgrapht:jgrapht-io:1.5.1' - - implementation GLibRepoPath // This is required for the @Tracer annotations to work at runtime implementation 'org.aspectj:aspectjrt:1.9.21' implementation 'org.aspectj:aspectjtools:1.9.21' - aspect GLibRepoPath + + implementation project(':gompeilib') + aspect project(':gompeilib') roborioRelease wpi.java.deps.wpilibJniRelease(wpi.platforms.roborio) roborioRelease wpi.java.vendor.jniRelease(wpi.platforms.roborio) @@ -476,39 +422,3 @@ spotless { endWithNewline() } } - -tasks.register('appendBuildVersion') { - doLast { - def src = useLocalGompeiLib - ? "GompeiLib:local" - : ((project.hasProperty("gpr.user") && project.hasProperty("gpr.key")) - ? "GompeiLib:github-packages" - : "GompeiLib:jitpack") - - def dep = configurations.detachedConfiguration( - dependencies.create(GLibRepoPath) - ).resolvedConfiguration.firstLevelModuleDependencies.find { - it.moduleName == "gompeilib" - } - - def ver = dep?.moduleVersion ?: "unknown" - "${src}:${ver}" - def outFile = file("src/main/java/frc/robot/BuildConstants.java") - if (!outFile.exists()) throw new GradleException("BuildConstants.java not found. Run gversion first.") - - def contents = outFile.text - - // Replace existing VERSION field if present, otherwise append before last brace - if (contents =~ /public static final String VERSION\s*=\s*".*";/) { - contents = contents.replaceFirst(/public static final String VERSION\s*=\s*".*";/, - "public static final String VERSION = \"${src}:${ver}\";") - } else { - contents = contents.replaceFirst(/\}\s*$/, " public static final String VERSION = \"${src}:${ver}\";\n}") - } - - outFile.text = contents - println "VERSION field set to ${src}:${ver}" - } -} - -createVersionFile.finalizedBy appendBuildVersion diff --git a/lib/.github/workflows/approvalautomation.yaml b/lib/.github/workflows/approvalautomation.yaml new file mode 100644 index 00000000..2f1582f3 --- /dev/null +++ b/lib/.github/workflows/approvalautomation.yaml @@ -0,0 +1,92 @@ +name: LCM Review Automation + +on: + pull_request: + types: [opened, ready_for_review, synchronize] + +jobs: + lcm-review-check: + name: LCM Review Check + runs-on: ubuntu-latest + steps: + - name: Determine action for this PR + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.APPROVAL_PAT }} + script: | + const pr = context.payload.pull_request; + const prAuthor = pr.user.login; + const prNumber = pr.number; + const prUpdatedAt = new Date(pr.updated_at); + + // Get all reviews + const { data: reviews } = await github.rest.pulls.listReviews({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + + // Separate bot and human reviews + const botReviews = reviews + .filter(r => r.user.login === '190automationbot') + .sort((a, b) => new Date(b.submitted_at) - new Date(a.submitted_at)); + + const humanReviews = reviews + .filter(r => r.user.login !== '190automationbot') + .sort((a, b) => new Date(b.submitted_at) - new Date(a.submitted_at)); + + const latestBotReview = botReviews[0]; + const latestHumanReview = humanReviews[0]; + + if (prAuthor === 'ElliotScher') { + // Auto-approve logic + let needsApproval = false; + + if (!latestBotReview) { + needsApproval = true; + } else { + const lastApprovalTime = new Date(latestBotReview.submitted_at); + if (prUpdatedAt > lastApprovalTime) { + needsApproval = true; + } + } + + // Don't approve if human requested changes + if (latestHumanReview && latestHumanReview.state === 'CHANGES_REQUESTED') { + needsApproval = false; + } + + if (needsApproval && pr.draft === false) { + console.log("Auto-approving PR by ElliotScher"); + await github.rest.pulls.createReview({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + event: 'APPROVE' + }); + } else { + console.log("No approval needed at this time"); + } + + } else { + // Request review from ElliotScher for other authors + if (prAuthor !== 'ElliotScher') { + const alreadyRequested = pr.requested_reviewers + .map(r => r.login) + .includes('ElliotScher'); + + if (!alreadyRequested) { + console.log("Requesting review from ElliotScher"); + await github.rest.pulls.requestReviewers({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + reviewers: ['ElliotScher'] + }); + } else { + console.log("ElliotScher already requested as reviewer"); + } + } else { + console.log("Skipping review request because author is ElliotScher"); + } + } diff --git a/lib/.github/workflows/build.yaml b/lib/.github/workflows/build.yaml new file mode 100644 index 00000000..233cfe66 --- /dev/null +++ b/lib/.github/workflows/build.yaml @@ -0,0 +1,30 @@ +name: Build + +on: + push: + branches: + - main + pull_request: + +jobs: + Build: + runs-on: ubuntu-latest + container: wpilib/roborio-cross-ubuntu:2024-22.04 + + env: + GPR_USER: ${{ github.actor }} + GPR_KEY: ${{ secrets.GPR_KEY }} + + steps: + - uses: actions/checkout@v4 + + - name: Grant execute permission + run: chmod +x gradlew + + - name: Build robot code + run: | + ./gradlew build \ + -Pgpr.user=$GPR_USER \ + -Pgpr.key=$GPR_KEY \ + --refresh-dependencies \ + -x test \ No newline at end of file diff --git a/lib/.github/workflows/lint.yaml b/lib/.github/workflows/lint.yaml new file mode 100644 index 00000000..fd3bf548 --- /dev/null +++ b/lib/.github/workflows/lint.yaml @@ -0,0 +1,28 @@ +name: Lint + +on: + push: + branches: + - main + pull_request: + +jobs: + Lint: + runs-on: ubuntu-latest + container: wpilib/roborio-cross-ubuntu:2024-22.04 + + env: + GPR_USER: ${{ github.actor }} + GPR_KEY: ${{ secrets.GPR_KEY }} + + steps: + - uses: actions/checkout@v4 + + - name: Grant execute permission + run: chmod +x gradlew + + - name: Spotless lint + run: | + ./gradlew spotlessCheck \ + -Pgpr.user=$GPR_USER \ + -Pgpr.key=$GPR_KEY \ No newline at end of file diff --git a/lib/.github/workflows/publish.yaml b/lib/.github/workflows/publish.yaml new file mode 100644 index 00000000..a4de8d12 --- /dev/null +++ b/lib/.github/workflows/publish.yaml @@ -0,0 +1,30 @@ +name: Publish + +on: + release: + types: [created] + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Java + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: temurin + cache: gradle + + - name: Determine version + run: | + # Use the GitHub tag as version + echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + + - name: Publish to GitHub Packages + run: ./gradlew publish -PreleaseVersion=$RELEASE_VERSION + env: + GPR_USER: ${{ github.actor }} + GPR_KEY: ${{ secrets.GITHUB_TOKEN }} diff --git a/lib/.github/workflows/test.yaml b/lib/.github/workflows/test.yaml new file mode 100644 index 00000000..09c33b2a --- /dev/null +++ b/lib/.github/workflows/test.yaml @@ -0,0 +1,32 @@ +name: Test + +on: + pull_request: + +jobs: + Test: + runs-on: ubuntu-latest + container: wpilib/roborio-cross-ubuntu:2024-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Grant execute permission + run: chmod +x gradlew + + - name: Run tests with coverage check + run: | + ./gradlew clean test jacocoTestCoverageVerification \ + -Pgpr.user=$GPR_USER \ + -Pgpr.key=$GPR_KEY \ + --refresh-dependencies \ + --no-daemon \ + --stacktrace + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: | + build/test-results + build/reports/jacoco \ No newline at end of file diff --git a/lib/.gitignore b/lib/.gitignore new file mode 100644 index 00000000..553ff928 --- /dev/null +++ b/lib/.gitignore @@ -0,0 +1,45 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build + +*/BuildConstants.java + +### IntelliJ IDEA ### +/.idea +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store +examples/swerve/src/main/java/frc/robot/BuildConstants.java + +examples/swerve/ctre_sim \ No newline at end of file diff --git a/lib/build.gradle b/lib/build.gradle new file mode 100644 index 00000000..8fc090cc --- /dev/null +++ b/lib/build.gradle @@ -0,0 +1,195 @@ +import org.gradle.internal.os.OperatingSystem + +plugins { + id 'java-library' + id 'maven-publish' + id "com.diffplug.spotless" version "6.25.0" + id "io.freefair.lombok" version "8.4" + id "io.freefair.aspectj.post-compile-weaving" version "8.6" + id "jacoco" +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +group = 'edu.wpi.team190' +version = project.hasProperty("releaseVersion") ? project.property("releaseVersion") : "0.0.0-SNAPSHOT" + +// Detect OS +def osClassifier = { + def osName = System.getProperty("os.name").toLowerCase() + if (osName.contains("win")) return "windowsx86-64" + if (osName.contains("mac")) return "osxuniversal" + return "linuxx86-64" +}() + +repositories { + mavenCentral() + maven { url = uri("https://frcmaven.wpi.edu/artifactory/release") } + maven { url = uri("https://frcmaven.wpi.edu/artifactory/littletonrobotics-mvn-release") } + maven { url = uri("https://maven.ctr-electronics.com/release/") } + maven { url = uri("https://frcmaven.wpi.edu/artifactory/sleipnirgroup-mvn-release/") } + maven { url = uri("https://3015rangerrobotics.github.io/pathplannerlib/repo") } +} + +apply from: "gradle/native-utils.gradle" + +dependencies { + // WPILib bundle + compileOnly libs.bundles.wpilib + + // AspectJ + compileOnly libs.aspectj.rt + runtimeOnly libs.aspectj.tools + + // PathPlanner + compileOnly libs.pathplanner + + // CTRE + compileOnly libs.phoenix6 + + // Choreo + compileOnly libs.choreo + + // AdvantageKit + runtime dependencies + compileOnly(libs.bundles.advantagekit) + annotationProcessor libs.advantagekit.autolog + + // Tests + testImplementation platform('org.junit:junit-bom:5.11.0') + testImplementation "org.mockito:mockito-inline:5.2.0" + testImplementation 'org.junit.jupiter:junit-jupiter' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + testImplementation libs.bundles.wpilib + testImplementation libs.bundles.aspectj + testImplementation libs.bundles.advantagekit + + testRuntimeOnly libs.bundles.wpilib + testRuntimeOnly libs.bundles.aspectj + testRuntimeOnly libs.bundles.advantagekit +} + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + groupId = group + artifactId = "gompeilib" + version = version + } + } + + repositories { + // GitHub Packages ONLY in CI + if (System.getenv("CI") == "true") { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/Team-190/GompeiLib") + credentials { + username = System.getenv("GPR_USER") ?: project.findProperty("gpr.user") + password = System.getenv("GPR_KEY") ?: project.findProperty("gpr.key") + } + } + } + } +} + +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + showStandardStreams = true + } + jacoco { + enabled = true + } + + // After tests finish, generate coverage report automatically + finalizedBy tasks.jacocoTestReport +} + +jacoco { + toolVersion = "0.8.10" +} + +test { + useJUnitPlatform() + finalizedBy jacocoTestReport +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = true + html.required = true + } + afterEvaluate { + classDirectories.setFrom(files(classDirectories.files.collect { + fileTree(dir: it, exclude: [ + '**/*Aspect.class', + '**/*$AjcClosure*', + '**/ajc$*', + '**/*Tracer.class', + '**/*AutoLogged.class', + '**/*AutoLogged$*.class', + '**/*Tracing*' + ]) + })) + } +} + +jacocoTestCoverageVerification { + violationRules { + rule { + limit { + minimum = 0.00 + } + } + } +} + +// Spotless formatting +project.compileJava.dependsOn(spotlessApply) +spotless { + java { + target fileTree(".") { + include "**/*.java" + exclude "**/build/**", "**/build-*/**" + } + toggleOffOn() + googleJavaFormat() + removeUnusedImports() + trimTrailingWhitespace() + endWithNewline() + } + groovyGradle { + target fileTree(".") { + include "**/*.gradle" + exclude "**/build/**", "**/build-*/**" + } + greclipse() + indentWithSpaces(4) + trimTrailingWhitespace() + endWithNewline() + } + json { + target fileTree(".") { + include "**/*.json" + exclude "**/build/**", "**/build-*/**" + } + gson().indentWithSpaces(2) + } + format "misc", { + target fileTree(".") { + include "**/*.md", "**/.gitignore" + exclude "**/build/**", "**/build-*/**" + } + trimTrailingWhitespace() + indentWithSpaces(2) + endWithNewline() + } +} diff --git a/lib/examples/swerve/.wpilib/wpilib_preferences.json b/lib/examples/swerve/.wpilib/wpilib_preferences.json new file mode 100644 index 00000000..a1801715 --- /dev/null +++ b/lib/examples/swerve/.wpilib/wpilib_preferences.json @@ -0,0 +1,6 @@ +{ + "enableCppIntellisense": false, + "currentLanguage": "java", + "projectYear": "2026", + "teamNumber": 190 +} diff --git a/lib/examples/swerve/6328-License.md b/lib/examples/swerve/6328-License.md new file mode 100644 index 00000000..3d37d771 --- /dev/null +++ b/lib/examples/swerve/6328-License.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 FRC 6328 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/examples/swerve/AdvantageKit-License.md b/lib/examples/swerve/AdvantageKit-License.md new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/lib/examples/swerve/AdvantageKit-License.md @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + 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. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/lib/examples/swerve/README.md b/lib/examples/swerve/README.md new file mode 100644 index 00000000..60dad4ae --- /dev/null +++ b/lib/examples/swerve/README.md @@ -0,0 +1 @@ +# 2k26-Robot-Code diff --git a/lib/examples/swerve/WPILib-License.md b/lib/examples/swerve/WPILib-License.md new file mode 100644 index 00000000..051080d8 --- /dev/null +++ b/lib/examples/swerve/WPILib-License.md @@ -0,0 +1,25 @@ +Copyright (c) 2009-2025 FIRST and other WPILib contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +- Neither the name of FIRST, WPILib, nor the names of other WPILib + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY FIRST AND OTHER WPILIB CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY NONINFRINGEMENT AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL FIRST OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/config.json b/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/config.json new file mode 100644 index 00000000..3cc3b6e3 --- /dev/null +++ b/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/config.json @@ -0,0 +1,142 @@ +{ + "name": "Doomspiral", + "sourceUrl": "https://frc190.onshape.com/documents/b452dd5a31bf95c6384826fb/w/4d65f099a7d3627693a2bc3a/e/934f62b4a3bd58acfff00615", + "disableSimplification": true, + "rotations": [ + { + "axis": "x", + "degrees": 90 + }, + { + "axis": "z", + "degrees": 90 + } + ], + "position": [ + 0, + 0, + 0 + ], + "cameras": [], + "components": [ + { + "zeroedRotations": [ + { + "axis": "x", + "degrees": 90 + }, + { + "axis": "z", + "degrees": 90 + } + ], + "zeroedPosition": [ + 0.009525, + 0, + -0.067589 + ] + }, + { + "zeroedRotations": [ + { + "axis": "x", + "degrees": 90 + }, + { + "axis": "z", + "degrees": 90 + } + ], + "zeroedPosition": [ + 0.202788, + -0.090048, + -0.477077 + ] + }, + { + "zeroedRotations": [ + { + "axis": "x", + "degrees": 90 + }, + { + "axis": "z", + "degrees": 90 + } + ], + "zeroedPosition": [ + 0, + 0, + 0 + ] + }, + { + "zeroedRotations": [ + { + "axis": "x", + "degrees": 90 + }, + { + "axis": "z", + "degrees": 90 + } + ], + "zeroedPosition": [ + -0.142476, + 0, + -0.278075 + ] + }, + { + "zeroedRotations": [ + { + "axis": "x", + "degrees": 90 + }, + { + "axis": "z", + "degrees": 90 + } + ], + "zeroedPosition": [ + 0.030565, + 0, + -0.312095 + ] + }, + { + "zeroedRotations": [ + { + "axis": "x", + "degrees": 90 + }, + { + "axis": "z", + "degrees": 90 + } + ], + "zeroedPosition": [ + -0.278238, + 0, + -0.196629 + ] + }, + { + "zeroedRotations": [ + { + "axis": "x", + "degrees": 90 + }, + { + "axis": "z", + "degrees": 90 + } + ], + "zeroedPosition": [ + 0.009856, + 0, + -0.304569 + ] + } + ] +} diff --git a/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model.glb b/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model.glb new file mode 100644 index 00000000..780d3e14 Binary files /dev/null and b/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model.glb differ diff --git a/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model_0.glb b/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model_0.glb new file mode 100644 index 00000000..69d24ee9 Binary files /dev/null and b/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model_0.glb differ diff --git a/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model_1.glb b/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model_1.glb new file mode 100644 index 00000000..f29c3ac2 Binary files /dev/null and b/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model_1.glb differ diff --git a/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model_2.glb b/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model_2.glb new file mode 100644 index 00000000..9da3d878 Binary files /dev/null and b/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model_2.glb differ diff --git a/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model_3.glb b/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model_3.glb new file mode 100644 index 00000000..07c74d72 Binary files /dev/null and b/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model_3.glb differ diff --git a/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model_4.glb b/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model_4.glb new file mode 100644 index 00000000..29fc4cc9 Binary files /dev/null and b/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model_4.glb differ diff --git a/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model_5.glb b/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model_5.glb new file mode 100644 index 00000000..2e97af12 Binary files /dev/null and b/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model_5.glb differ diff --git a/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model_6.glb b/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model_6.glb new file mode 100644 index 00000000..22d3a452 Binary files /dev/null and b/lib/examples/swerve/advantagescope_assets/Robot_Doomspiral/model_6.glb differ diff --git a/lib/examples/swerve/build.gradle b/lib/examples/swerve/build.gradle new file mode 100755 index 00000000..c59ac10a --- /dev/null +++ b/lib/examples/swerve/build.gradle @@ -0,0 +1,456 @@ +plugins { + id "java" + id "edu.wpi.first.GradleRIO" version "2026.2.1" + id "com.peterabeles.gversion" version "1.10" + id "io.freefair.lombok" version "8.4" + id "com.diffplug.spotless" version "6.25.0" + id "io.freefair.aspectj.post-compile-weaving" version "8.6" +} + +repositories { + maven { url = uri("https://3015rangerrobotics.github.io/pathplannerlib/repo") } +} + +ext { + deploySrcDir = "src/main/deploy" + deployRobotDir = "/home/lvuser/deploy" + useLocalGompeiLib = project.hasProperty("useLocalGompeiLib") +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +def ROBOT_MAIN_CLASS = "frc.robot.Main" + +gradle.projectsEvaluated { + def buildDir = new File("${project.buildDir}/classes/java/main") + if (!buildDir.exists()) { + println "⚠️ Warning: compiled classes not found. Make sure compileJava runs first." + project.ext.deploySrcDir = "src/main/deploy" + return + } + + try { + def loader = new URLClassLoader([buildDir.toURI().toURL()] as URL[], this.class.classLoader) + def constantsClass = loader.loadClass("frc.robot.RobotConfig") + def robotField = constantsClass.getDeclaredField("ROBOT") + robotField.setAccessible(true) + def robotEnumValue = robotField.get(null) // static field + + println "Detected RobotType: ${robotEnumValue}" + + def baseName = robotEnumValue.toString().toLowerCase().replaceAll(/_sim$/, "") + def candidateDir = "src/main/deploy/${baseName}" + + if (file(candidateDir).exists()) { + project.ext.deploySrcDir = candidateDir + project.ext.deployRobotDir = "/home/lvuser/deploy/${baseName}" + } else { + println "Folder '${candidateDir}' does not exist. Using default deploy folder." + } + + println "deploySrcDir set to: ${project.ext.deploySrcDir}" + } catch (Throwable e) { + println "Could not read RobotType: ${e.message}" + project.ext.deploySrcDir = "src/main/deploy" + } +} + +// 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 = 190 + 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')) { + jvmArgs.add("-XX:+UnlockExperimentalVMOptions") + jvmArgs.add("-XX:GCTimeRatio=5") + jvmArgs.add("-XX:+UseSerialGC") + jvmArgs.add("-XX:MaxGCPauseMillis=50") + + // The options below may improve performance, but should only be enabled on the RIO 2 + + final MAX_JAVA_HEAP_SIZE_MB = 100 + jvmArgs.add("-Xmx" + MAX_JAVA_HEAP_SIZE_MB + "M") + jvmArgs.add("-Xms" + MAX_JAVA_HEAP_SIZE_MB + "M") + jvmArgs.add("-XX:+AlwaysPreTouch") + + // Enable VisualVM connection + // jvmArgs.add("-Dcom.sun.management.jmxremote=true") + // jvmArgs.add("-Dcom.sun.management.jmxremote.port=1198") + // jvmArgs.add("-Dcom.sun.management.jmxremote.local.only=false") + // jvmArgs.add("-Dcom.sun.management.jmxremote.ssl=false") + // jvmArgs.add("-Dcom.sun.management.jmxremote.authenticate=false") + // jvmArgs.add("-Djava.rmi.server.hostname=10.1.90.2") // Replace TE.AM with team number + } + + // Static files artifact + frcStaticFileDeploy(getArtifactTypeClass('FileTreeArtifact')) { + files = project.fileTree(project.ext.deploySrcDir) + directory = deployRobotDir + // Change to true to delete files on roboRIO that no + // longer exist in deploy directory on roboRIO + deleteOldFiles = false + } + } + } + } +} + +// Grab the roboRIO comment via SSH + +import com.jcraft.jsch.JSch +import edu.wpi.first.gradlerio.GradleRIOPlugin +import groovy.json.JsonSlurper + +ext { + // RoboRIO SSH details + roboRIOHost = 'roboRIO-190-FRC.local' // Replace with your team number + roboRIOUser = 'admin' // Default user + roboRIOPassword = '' // Default password + machineInfoFile = '/etc/machine-info' // Path to the file to read +} + +task checkRoboRIOtoRobotType { + dependsOn compileJava + doLast { + def ROBORIO_COMMENT = fetchNameUsingJSch( + roboRIOHost, roboRIOUser, roboRIOPassword, machineInfoFile + ) + println "RoboRIO Name: ${ROBORIO_COMMENT}" + + // Ensure the build output directory is in the classpath + def buildOutputDir = file('build/classes/java/main') + URLClassLoader loader = new URLClassLoader([ + buildOutputDir.toURI().toURL() + ] as URL[], this.class.classLoader) + + // Set file path names and constants + def robotTypeClassName = 'frc.robot.RobotConfig$RobotType' + def constantsClassName = 'frc.robot.RobotConfig' + def robotType = null + def constantsClass = null + def robotFieldName = 'ROBOT' + + try { + // Load the class from the extended classloader + robotType = loader.loadClass(robotTypeClassName) + println "Loaded class: ${robotType.name}" + + def constants = robotType.enumConstants + println "Enum constants: ${constants.collect { it.name() }}" + } catch (ClassNotFoundException e) { + println "Error: Class ${robotTypeClassName} not found. ${e.message}" + } + + // Check for valid name of roboRIO comment + boolean foundMatch = false + for (enumConstant in robotType.enumConstants) { + if (enumConstant.toString().equalsIgnoreCase(ROBORIO_COMMENT)) { + foundMatch = true + break // Exit the loop once a match is found + } + } + + if (foundMatch) { + println "Match found for possible RobotType" + } else { + throw new GradleException("Error: '${ROBORIO_COMMENT}' is not a valid RobotType\nTo Fix: Enter valid RoboRIO comment of valid RobotType in file '${machineInfoFile}' or via Webserver!") + } + + try { + // Load the class from the extended classloader + constantsClass = loader.loadClass(constantsClassName) + println "Loaded class: ${constantsClass.name}" + } catch (ClassNotFoundException e) { + println "Error: Class ${constantsClassName} not found. ${e.message}" + } + + // Get the ROBOT field + def robotField = constantsClass.getDeclaredField(robotFieldName) + robotField.setAccessible(true) + + // Get the value of the ROBOT field + def robotEnumValue = robotField.get(null) // For static fields, pass `null` + println "Declared ROBOT in Constants.java: ${robotEnumValue}" + + // Compare roboRIO name with ROBOT value + if (ROBORIO_COMMENT.equalsIgnoreCase(robotEnumValue.name())) { + println "The roboRIO name matches the ROBOT value in Constants.java!" + } else { + throw new GradleException("Mismatch! The RoboRIO comment '${ROBORIO_COMMENT}' does not match the ROBOT value '${robotEnumValue.name()}'\nTo Fix: Match RoboRIO comment to ROBOT in file '${constantsClassName}'") + } + } +} + +// Function to fetch the file content using JSch +def fetchNameUsingJSch(host, user, password, remoteFile) { + def session = null + def channel = null + try { + JSch jsch = new JSch() + session = jsch.getSession(user, host, 22) + session.setPassword(password) + + // Skip strict host key checking for simplicity + session.setConfig("StrictHostKeyChecking", "no") + + // Connect to the session + session.connect() + + // Open an exec channel to run the command + channel = session.openChannel("exec") + def command = "cat ${remoteFile}" + channel.setCommand(command) + + // Capture the output + def inputStream = channel.inputStream + channel.connect() + + def output = inputStream.text.trim() + if (output.isEmpty()) { + throw new GradleException("Failed to fetch data from ${remoteFile}") + } + + // Extract the name within quotes if the output contains PRETTY_HOSTNAME + def matcher = output =~ /PRETTY_HOSTNAME="(.+?)"/ + if (matcher.find()) { + return matcher.group(1) // The name within quotes + } else { + throw new GradleException("RoboRIO Comment is Empty in file '${remoteFile}'\nTo Fix: Add RoboRIO Comment in file '${remoteFile}' or via Webserver!") + } + } catch (Exception e) { + throw new GradleException("Error during SSH connection: ${e.message}", e) + } finally { + if (channel != null) { + channel.disconnect() + } + if (session != null) { + session.disconnect() + } + } +} + +deployroborio.dependsOn(checkRoboRIOtoRobotType) + + +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 +task(replayWatch, type: JavaExec) { + mainClass = "org.littletonrobotics.junction.ReplayWatch" + classpath = sourceSets.main.runtimeClasspath +} + +// Defining my dependencies. In this case, WPILib (+ friends), and vendor libraries. +// Also defines JUnit 4. +dependencies { + annotationProcessor wpi.java.deps.wpilibAnnotations() + implementation wpi.java.deps.wpilib() + implementation wpi.java.vendor.java() + implementation 'org.jgrapht:jgrapht-core:1.5.1' + implementation 'org.jgrapht:jgrapht-io:1.5.1' + + implementation 'com.pathplanner.lib:PathplannerLib-java:2026.1.2' + + + implementation project(":") // <- root project (GompeiLib library) + // This is required for the @Tracer annotations to work at runtime + implementation 'org.aspectj:aspectjrt:1.9.21' + implementation 'org.aspectj:aspectjtools:1.9.21' + + roborioRelease wpi.java.deps.wpilibJniRelease(wpi.platforms.roborio) + roborioRelease wpi.java.vendor.jniRelease(wpi.platforms.roborio) + + nativeDebug wpi.java.deps.wpilibJniDebug(wpi.platforms.desktop) + nativeDebug wpi.java.vendor.jniDebug(wpi.platforms.desktop) + simulationDebug wpi.sim.enableDebug() + + nativeRelease wpi.java.deps.wpilibJniRelease(wpi.platforms.desktop) + nativeRelease wpi.java.vendor.jniRelease(wpi.platforms.desktop) + simulationRelease wpi.sim.enableRelease() + + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + def akitJson = new JsonSlurper().parseText(new File(projectDir.getAbsolutePath() + "/vendordeps/AdvantageKit.json").text) + annotationProcessor "org.littletonrobotics.akit:akit-autolog:$akitJson.version" +} + +test { + useJUnitPlatform() + systemProperty 'junit.jupiter.extensions.autodetection.enabled', 'true' +} + +// Simulation configuration (e.g. environment variables). +// +// The sim GUI is *disabled* by default to support running +// AdvantageKit log replay from the command line. Set the +// value to "true" to enable the sim GUI by default (this +// is the standard WPILib behavior). +wpi.sim.addGui().defaultEnabled = false +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) + } + } + from sourceSets.main.allSource + manifest GradleRIOPlugin.javaManifest(ROBOT_MAIN_CLASS) + duplicatesStrategy = DuplicatesStrategy.INCLUDE +} + +// Configure jar and deploy tasks +deployArtifact.jarTask = jar +wpi.java.configureExecutableTasks(jar) +wpi.java.configureTestTasks(test) + +// Configure string concat to always inline compile +tasks.withType(JavaCompile).configureEach { + options.compilerArgs.add '-XDstringConcat=inline' +} + +// Create version file +project.compileJava.dependsOn(createVersionFile) +gversion { + srcDir = "src/main/java/" + classPackage = "frc.robot" + className = "BuildConstants" + dateFormat = "yyyy-MM-dd HH:mm:ss z" + timeZone = "America/New_York" + indent = " " +} + +// Create commit with working changes on event branches +tasks.register('eventDeploy') { + doLast { + if (project.gradle.startParameter.taskNames.any({ it.toLowerCase().contains("deploy") })) { + def branchPrefix = "event" + def branch = 'git branch --show-current'.execute().text.trim() + def commitMessage = "Update at '${new Date().toString()}'" + + if (branch.startsWith(branchPrefix)) { + exec { + workingDir(projectDir) + executable 'git' + args 'add', '-A' + } + exec { + workingDir(projectDir) + executable 'git' + args 'commit', '-m', commitMessage + ignoreExitValue = true + } + + println "Committed to branch: '$branch'" + println "Commit message: '$commitMessage'" + } else { + println "Not on an event branch, skipping commit" + } + } else { + println "Not running deploy task, skipping commit" + } + } +} +createVersionFile.dependsOn(eventDeploy) + +// Spotless formatting +project.compileJava.dependsOn(spotlessApply) +spotless { + java { + target fileTree(".") { + include "**/*.java" + exclude "**/build/**", "**/build-*/**" + } + toggleOffOn() + googleJavaFormat() + removeUnusedImports() + trimTrailingWhitespace() + endWithNewline() + } + groovyGradle { + target fileTree(".") { + include "**/*.gradle" + exclude "**/build/**", "**/build-*/**" + } + greclipse() + indentWithSpaces(4) + trimTrailingWhitespace() + endWithNewline() + } + json { + target fileTree(".") { + include "**/*.json" + exclude "**/build/**", "**/build-*/**" + } + gson().indentWithSpaces(2) + } + format "misc", { + target fileTree(".") { + include "**/*.md", "**/.gitignore" + exclude "**/build/**", "**/build-*/**" + } + trimTrailingWhitespace() + indentWithSpaces(2) + endWithNewline() + } +} + +//tasks.register('appendBuildVersion') { +// doLast { +// def src = useLocalGompeiLib +// ? "GompeiLib:local" +// : ((project.hasProperty("gpr.user") && project.hasProperty("gpr.key")) +// ? "GompeiLib:github-packages" +// : "GompeiLib:jitpack") +// +// def dep = configurations.detachedConfiguration( +// dependencies.create(GLibRepoPath) +// ).resolvedConfiguration.firstLevelModuleDependencies.find { +// it.moduleName == "gompeilib" +// } +// +// def ver = dep?.moduleVersion ?: "unknown" +// "${src}:${ver}" +// def outFile = file("src/main/java/frc/robot/BuildConstants.java") +// if (!outFile.exists()) throw new GradleException("BuildConstants.java not found. Run gversion first.") +// +// def contents = outFile.text +// +// // Replace existing VERSION field if present, otherwise append before last brace +// if (contents =~ /public static final String VERSION\s*=\s*".*";/) { +// contents = contents.replaceFirst(/public static final String VERSION\s*=\s*".*";/, +// "public static final String VERSION = \"${src}:${ver}\";") +// } else { +// contents = contents.replaceFirst(/\}\s*$/, " public static final String VERSION = \"${src}:${ver}\";\n}") +// } +// +// outFile.text = contents +// println "VERSION field set to ${src}:${ver}" +// } +//} + +//createVersionFile.finalizedBy appendBuildVersion diff --git a/lib/examples/swerve/gradle/wrapper/gradle-wrapper.jar b/lib/examples/swerve/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..a4b76b95 Binary files /dev/null and b/lib/examples/swerve/gradle/wrapper/gradle-wrapper.jar differ diff --git a/lib/examples/swerve/gradle/wrapper/gradle-wrapper.properties b/lib/examples/swerve/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..34bd9ce9 --- /dev/null +++ b/lib/examples/swerve/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=permwrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=permwrapper/dists diff --git a/lib/examples/swerve/gradlew b/lib/examples/swerve/gradlew new file mode 100755 index 00000000..1aa94a42 --- /dev/null +++ b/lib/examples/swerve/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + 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 +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# 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" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/lib/examples/swerve/gradlew.bat b/lib/examples/swerve/gradlew.bat new file mode 100644 index 00000000..93e3f59f --- /dev/null +++ b/lib/examples/swerve/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +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% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/lib/examples/swerve/lvrt.sh b/lib/examples/swerve/lvrt.sh new file mode 100644 index 00000000..ac3af941 --- /dev/null +++ b/lib/examples/swerve/lvrt.sh @@ -0,0 +1,17 @@ +#!/bin/sh +exec 2>&1 >/dev/null +while true; do + APP_PATH=`nirtcfg --file /etc/natinst/share/lvrt.conf --get section=LVRT,token=RTTarget.ApplicationPath` + if [ "$APP_PATH" = /home/lvuser/natinst/bin/TBLStartupApp.rtexe ] + then + APP_BOOT=`nirtcfg --file /etc/natinst/share/lvrt.conf --get section=LVRT,token=RTTarget.LaunchAppAtBoot,value=true | tr "[:upper:]" "[:lower:]"` + APP_DISABLED=`nirtcfg --get section=SYSTEMSETTINGS,token=NoApp.enabled,value=false | tr "[:upper:]" "[:lower:]"` + if [ "$APP_BOOT" = true ] && [ "$APP_DISABLED" = false ] + then + /usr/local/frc/bin/frcRunRobot.sh + fi + else + exec -a lvrt ./ni-lvrt + fi + sleep 1 +done \ No newline at end of file diff --git a/lib/examples/swerve/settings.gradle b/lib/examples/swerve/settings.gradle new file mode 100644 index 00000000..8ea4b5d1 --- /dev/null +++ b/lib/examples/swerve/settings.gradle @@ -0,0 +1,30 @@ +import org.gradle.internal.os.OperatingSystem + +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + String frcYear = '2026' + File frcHome + if (OperatingSystem.current().isWindows()) { + String publicFolder = System.getenv('PUBLIC') + if (publicFolder == null) { + publicFolder = "C:\\Users\\Public" + } + def homeRoot = new File(publicFolder, "wpilib") + frcHome = new File(homeRoot, frcYear) + } else { + def userFolder = System.getProperty("user.home") + def homeRoot = new File(userFolder, "wpilib") + frcHome = new File(homeRoot, frcYear) + } + def frcHomeMaven = new File(frcHome, 'maven') + maven { + name 'frcHome' + url frcHomeMaven + } + } +} + +Properties props = System.getProperties() +props.setProperty("org.gradle.internal.native.headers.unresolved.dependencies.ignore", "true") diff --git a/lib/examples/swerve/src/main/deploy/choreo/DEPOT.traj b/lib/examples/swerve/src/main/deploy/choreo/DEPOT.traj new file mode 100644 index 00000000..16e01b16 --- /dev/null +++ b/lib/examples/swerve/src/main/deploy/choreo/DEPOT.traj @@ -0,0 +1,153 @@ +{ + "name":"DEPOT", + "version":3, + "snapshot":{ + "waypoints":[ + {"x":3.7033300399780273, "y":5.980339527130127, "heading":3.141592653589793, "intervals":22, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":1.141133427619934, "y":5.967901706695557, "heading":3.127100394245751, "intervals":17, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":0.4082718193531037, "y":5.990222454071045, "heading":-3.1415914918446664, "intervals":17, "split":false, "fixTranslation":true, "fixHeading":false, "overrideIntervals":false}, + {"x":2.01178240776062, "y":5.955463409423828, "heading":0.0, "intervals":29, "split":false, "fixTranslation":true, "fixHeading":false, "overrideIntervals":false}, + {"x":1.9869070053100584, "y":3.828591346740722, "heading":1.5874614566235736, "intervals":40, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}], + "constraints":[ + {"from":"first", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"last", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"first", "to":"last", "data":{"type":"KeepInRectangle", "props":{"x":0.0, "y":0.0, "w":16.541, "h":8.0692}}, "enabled":true}, + {"from":1, "to":2, "data":{"type":"MaxVelocity", "props":{"max":1.0}}, "enabled":true}, + {"from":1, "to":3, "data":{"type":"MaxAngularVelocity", "props":{"max":0.0}}, "enabled":true}], + "targetDt":0.05 + }, + "params":{ + "waypoints":[ + {"x":{"exp":"3.7033300399780273 m", "val":3.7033300399780273}, "y":{"exp":"5.980339527130127 m", "val":5.980339527130127}, "heading":{"exp":"3.141592653589793 rad", "val":3.141592653589793}, "intervals":22, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":{"exp":"1.141133427619934 m", "val":1.141133427619934}, "y":{"exp":"5.967901706695557 m", "val":5.967901706695557}, "heading":{"exp":"3.127100394245751 rad", "val":3.127100394245751}, "intervals":17, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":{"exp":"0.40827181935310364 m", "val":0.4082718193531037}, "y":{"exp":"5.990222454071045 m", "val":5.990222454071045}, "heading":{"exp":"-3.1415914918446664 rad", "val":-3.1415914918446664}, "intervals":17, "split":false, "fixTranslation":true, "fixHeading":false, "overrideIntervals":false}, + {"x":{"exp":"2.01178240776062 m", "val":2.01178240776062}, "y":{"exp":"5.955463409423828 m", "val":5.955463409423828}, "heading":{"exp":"0 deg", "val":0.0}, "intervals":29, "split":false, "fixTranslation":true, "fixHeading":false, "overrideIntervals":false}, + {"x":{"exp":"1.9869070053100586 m", "val":1.9869070053100584}, "y":{"exp":"3.8285913467407227 m", "val":3.828591346740722}, "heading":{"exp":"1.5874614566235736 rad", "val":1.5874614566235736}, "intervals":40, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}], + "constraints":[ + {"from":"first", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"last", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"first", "to":"last", "data":{"type":"KeepInRectangle", "props":{"x":{"exp":"0 m", "val":0.0}, "y":{"exp":"0 m", "val":0.0}, "w":{"exp":"16.541 m", "val":16.541}, "h":{"exp":"8.0692 m", "val":8.0692}}}, "enabled":true}, + {"from":1, "to":2, "data":{"type":"MaxVelocity", "props":{"max":{"exp":"1 m / s", "val":1.0}}}, "enabled":true}, + {"from":1, "to":3, "data":{"type":"MaxAngularVelocity", "props":{"max":{"exp":"0 rad / s", "val":0.0}}}, "enabled":true}], + "targetDt":{ + "exp":"0.05 s", + "val":0.05 + } + }, + "trajectory":{ + "config":{ + "frontLeft":{ + "x":0.225425, + "y":0.333375 + }, + "backLeft":{ + "x":-0.225425, + "y":0.333375 + }, + "mass":68.0388555, + "inertia":7.5, + "gearing":7.03, + "radius":0.0508, + "vmax":607.3745796940267, + "tmax":1.2, + "cof":1.5, + "bumper":{ + "front":0.34925, + "side":0.4445, + "back":0.34925 + }, + "differentialTrackWidth":0.5588 + }, + "sampleType":"Swerve", + "waypoints":[0.0,0.94415,1.72511,2.4094,3.26434], + "samples":[ + {"t":0.0, "x":3.70333, "y":5.98034, "heading":3.14159, "vx":0.0, "vy":0.0, "omega":0.0, "ax":-9.75303, "ay":-0.03176, "alpha":-0.09915, "fx":[-165.90006,-165.89478,-165.89222,-165.89765], "fy":[0.28066,-1.35304,-1.37302,0.2848]}, + {"t":0.04292, "x":3.69435, "y":5.98031, "heading":3.14159, "vx":-0.41856, "vy":-0.00136, "omega":-0.00426, "ax":-9.75196, "ay":-0.03139, "alpha":-0.09884, "fx":[-165.88194,-165.87675,-165.87392,-165.87928], "fy":[0.28405,-1.34414,-1.36391,0.28823]}, + {"t":0.08583, "x":3.66741, "y":5.98022, "heading":3.14141, "vx":-0.83707, "vy":-0.00271, "omega":-0.0085, "ax":-9.75062, "ay":-0.03102, "alpha":-0.09845, "fx":[-165.8593,-165.85418,-165.85104,-165.85631], "fy":[0.28702,-1.33429,-1.35428,0.29078]}, + {"t":0.12875, "x":3.6225, "y":5.98008, "heading":3.14105, "vx":-1.25553, "vy":-0.00404, "omega":-0.01272, "ax":-9.7489, "ay":-0.03065, "alpha":-0.09795, "fx":[-165.83017,-165.82515,-165.8216,-165.82678], "fy":[0.28926,-1.32319,-1.34379,0.29215]}, + {"t":0.17166, "x":3.55964, "y":5.97988, "heading":3.1405, "vx":-1.67391, "vy":-0.00536, "omega":-0.01693, "ax":-9.7466, "ay":-0.03028, "alpha":-0.09727, "fx":[-165.79133,-165.78642,-165.78232,-165.7874], "fy":[0.29032,-1.31034,-1.33191,0.29189]}, + {"t":0.21458, "x":3.47883, "y":5.97962, "heading":3.13977, "vx":-2.09219, "vy":-0.00666, "omega":-0.0211, "ax":-9.74338, "ay":-0.02989, "alpha":-0.09634, "fx":[-165.73694,-165.73215,-165.7273,-165.73227], "fy":[0.28934,-1.29483,-1.31768,0.28916]}, + {"t":0.25749, "x":3.38007, "y":5.97931, "heading":3.13887, "vx":-2.51034, "vy":-0.00794, "omega":-0.02523, "ax":-9.73856, "ay":-0.0295, "alpha":-0.09493, "fx":[-165.65533,-165.65069,-165.64475,-165.64959], "fy":[0.28469,-1.27489,-1.29922,0.28233]}, + {"t":0.30041, "x":3.26337, "y":5.97894, "heading":3.13778, "vx":-2.92827, "vy":-0.00921, "omega":-0.02931, "ax":-9.73052, "ay":-0.02908, "alpha":-0.09261, "fx":[-165.51927,-165.51484,-165.50718,-165.51182], "fy":[0.27258,-1.24644,-1.2722,0.26765]}, + {"t":0.34333, "x":3.12874, "y":5.97852, "heading":3.13653, "vx":-3.34587, "vy":-0.01045, "omega":-0.03328, "ax":-9.71444, "ay":-0.02859, "alpha":-0.08808, "fx":[-165.24724,-165.24314,-165.23232,-165.23663], "fy":[0.24218,-1.19771,-1.22419,0.2344]}, + {"t":0.38624, "x":2.9762, "y":5.97804, "heading":3.1351, "vx":-3.76277, "vy":-0.01168, "omega":-0.03706, "ax":-9.66645, "ay":-0.02785, "alpha":-0.07537, "fx":[-164.43421,-164.43087,-164.41273,-164.41623], "fy":[0.14487,-1.07543,-1.0991,0.13461]}, + {"t":0.42916, "x":2.80582, "y":5.97751, "heading":3.13351, "vx":-4.17761, "vy":-0.01288, "omega":-0.0403, "ax":-4.82767, "ay":0.00382, "alpha":0.93655, "fx":[-79.12989,-79.09411,-85.10537,-85.13952], "fy":[-3.39746,3.45052,3.3697,-3.16305]}, + {"t":0.47207, "x":2.62209, "y":5.97696, "heading":3.13178, "vx":-4.3848, "vy":-0.01271, "omega":-0.0001, "ax":-0.00128, "ay":0.00422, "alpha":0.00029, "fx":[-0.02063,-0.02064,-0.02284,-0.02283], "fy":[0.07101,0.0725,0.07248,0.07099]}, + {"t":0.51499, "x":2.43391, "y":5.97642, "heading":3.13177, "vx":-4.38485, "vy":-0.01253, "omega":-0.00009, "ax":0.00029, "ay":-0.03094, "alpha":-0.00007, "fx":[0.0047,0.00472,0.00523,0.00521], "fy":[-0.52606,-0.52641,-0.52638,-0.52604]}, + {"t":0.55791, "x":2.24573, "y":5.97586, "heading":3.13177, "vx":-4.38484, "vy":-0.01386, "omega":-0.0001, "ax":1.05143, "ay":-0.24558, "alpha":-0.41961, "fx":[16.29224,16.26004,19.47581,19.50981], "fy":[-3.05602,-5.28375,-5.29398,-3.07554]}, + {"t":0.60082, "x":2.05852, "y":5.97504, "heading":3.13177, "vx":-4.33972, "vy":-0.0244, "omega":-0.0181, "ax":9.65858, "ay":0.01752, "alpha":-0.04815, "fx":[164.28282,164.28417,164.29659,164.2953], "fy":[0.69727,-0.08788,-0.09861,0.6811]}, + {"t":0.64374, "x":1.88117, "y":5.97401, "heading":3.13099, "vx":-3.92521, "vy":-0.02365, "omega":-0.02017, "ax":9.71258, "ay":0.02128, "alpha":0.02972, "fx":[165.21059,165.20957,165.2058,165.20686], "fy":[0.11346,0.60114,0.61144,0.12163]}, + {"t":0.68665, "x":1.72166, "y":5.97301, "heading":3.13012, "vx":-3.50839, "vy":-0.02273, "omega":-0.01889, "ax":9.72974, "ay":0.02228, "alpha":0.05453, "fx":[165.50336,165.5014,165.49678,165.49885], "fy":[-0.07843,0.81805,0.83996,-0.06373]}, + {"t":0.72957, "x":1.58006, "y":5.97205, "heading":3.12931, "vx":-3.09083, "vy":-0.02178, "omega":-0.01655, "ax":9.73815, "ay":0.0226, "alpha":0.06668, "fx":[165.64653,165.6441,165.63982,165.64243], "fy":[-0.17608,0.92118,0.9504,-0.15769]}, + {"t":0.77248, "x":1.45638, "y":5.97114, "heading":3.1286, "vx":-2.67291, "vy":-0.02081, "omega":-0.01369, "ax":9.74315, "ay":0.02265, "alpha":0.07388, "fx":[165.73137,165.72868,165.72484,165.72777], "fy":[-0.23671,0.9797,1.01406,-0.21567]}, + {"t":0.8154, "x":1.35064, "y":5.97027, "heading":3.12801, "vx":-2.25477, "vy":-0.01983, "omega":-0.01052, "ax":9.74645, "ay":0.02257, "alpha":0.07864, "fx":[165.78749,165.78464,165.78119,165.78431], "fy":[-0.27898,1.01625,1.05442,-0.25592]}, + {"t":0.85832, "x":1.26285, "y":5.96944, "heading":3.12756, "vx":-1.8365, "vy":-0.01887, "omega":-0.00715, "ax":9.7488, "ay":0.02241, "alpha":0.08201, "fx":[165.82734,165.8244,165.82127,165.82452], "fy":[-0.31078,1.0404,1.08145,-0.28619]}, + {"t":0.90123, "x":1.19301, "y":5.96865, "heading":3.12726, "vx":-1.41812, "vy":-0.0179, "omega":-0.00363, "ax":9.75056, "ay":0.0222, "alpha":0.08453, "fx":[165.85711,165.85411,165.85125,165.85458], "fy":[-0.33601,1.05694,1.10012,-0.31033]}, + {"t":0.94415, "x":1.14113, "y":5.9679, "heading":3.1271, "vx":-0.99966, "vy":-0.01695, "omega":0.0, "ax":0.04698, "ay":-1.09887, "alpha":0.0, "fx":[0.79911,0.79911,0.79911,0.79911], "fy":[-18.69149,-18.69149,-18.69149,-18.69149]}, + {"t":0.99009, "x":1.09526, "y":5.96596, "heading":3.1271, "vx":-0.99751, "vy":-0.06743, "omega":0.0, "ax":0.00061, "ay":-0.00899, "alpha":0.0, "fx":[0.01037,0.01037,0.01037,0.01037], "fy":[-0.15299,-0.15299,-0.15299,-0.15299]}, + {"t":1.03603, "x":1.04944, "y":5.96286, "heading":3.1271, "vx":-0.99748, "vy":-0.06785, "omega":0.0, "ax":-0.00002, "ay":0.00034, "alpha":0.0, "fx":[-0.0004,-0.0004,-0.0004,-0.0004], "fy":[0.00584,0.00584,0.00584,0.00584]}, + {"t":1.08196, "x":1.00361, "y":5.95974, "heading":3.1271, "vx":-0.99748, "vy":-0.06783, "omega":0.0, "ax":-0.00003, "ay":0.00043, "alpha":0.0, "fx":[-0.0005,-0.0005,-0.0005,-0.0005], "fy":[0.00724,0.00724,0.00724,0.00724]}, + {"t":1.1279, "x":0.95779, "y":5.95662, "heading":3.1271, "vx":-0.99748, "vy":-0.06781, "omega":0.0, "ax":-0.00003, "ay":0.00043, "alpha":0.0, "fx":[-0.0005,-0.0005,-0.0005,-0.0005], "fy":[0.00733,0.00733,0.00733,0.00733]}, + {"t":1.17384, "x":0.91197, "y":5.95351, "heading":3.1271, "vx":-0.99748, "vy":-0.06779, "omega":0.0, "ax":-0.00003, "ay":0.00044, "alpha":0.0, "fx":[-0.00051,-0.00051,-0.00051,-0.00051], "fy":[0.00741,0.00741,0.00741,0.00741]}, + {"t":1.21978, "x":0.86614, "y":5.9504, "heading":3.1271, "vx":-0.99748, "vy":-0.06777, "omega":0.0, "ax":-0.00003, "ay":0.00044, "alpha":0.0, "fx":[-0.00052,-0.00052,-0.00052,-0.00052], "fy":[0.00751,0.00751,0.00751,0.00751]}, + {"t":1.26572, "x":0.82032, "y":5.94728, "heading":3.1271, "vx":-0.99749, "vy":-0.06775, "omega":0.0, "ax":-0.00003, "ay":0.00045, "alpha":0.0, "fx":[-0.00052,-0.00052,-0.00052,-0.00052], "fy":[0.00763,0.00763,0.00763,0.00763]}, + {"t":1.31166, "x":0.7745, "y":5.94417, "heading":3.1271, "vx":-0.99749, "vy":-0.06773, "omega":0.0, "ax":-0.00003, "ay":0.00046, "alpha":0.0, "fx":[-0.00053,-0.00053,-0.00053,-0.00053], "fy":[0.00778,0.00778,0.00778,0.00778]}, + {"t":1.3576, "x":0.72867, "y":5.94106, "heading":3.1271, "vx":-0.99749, "vy":-0.06771, "omega":0.0, "ax":-0.00003, "ay":0.00047, "alpha":0.0, "fx":[-0.00055,-0.00055,-0.00055,-0.00055], "fy":[0.00797,0.00797,0.00797,0.00797]}, + {"t":1.40354, "x":0.68285, "y":5.93795, "heading":3.1271, "vx":-0.99749, "vy":-0.06769, "omega":0.0, "ax":-0.00003, "ay":0.00048, "alpha":0.0, "fx":[-0.00057,-0.00057,-0.00057,-0.00057], "fy":[0.00822,0.00822,0.00822,0.00822]}, + {"t":1.44947, "x":0.63703, "y":5.93484, "heading":3.1271, "vx":-0.99749, "vy":-0.06766, "omega":0.0, "ax":-0.00004, "ay":0.00058, "alpha":0.0, "fx":[-0.00067,-0.00067,-0.00067,-0.00067], "fy":[0.0098,0.0098,0.0098,0.0098]}, + {"t":1.49541, "x":0.5912, "y":5.93173, "heading":3.1271, "vx":-0.99749, "vy":-0.06764, "omega":0.0, "ax":-0.00063, "ay":0.00937, "alpha":0.0, "fx":[-0.01078,-0.01078,-0.01078,-0.01078], "fy":[0.1594,0.1594,0.1594,0.1594]}, + {"t":1.54135, "x":0.54538, "y":5.92864, "heading":3.1271, "vx":-0.99752, "vy":-0.06721, "omega":0.0, "ax":-0.04497, "ay":1.03556, "alpha":0.0, "fx":[-0.765,-0.765,-0.765,-0.765], "fy":[17.61461,17.61461,17.61461,17.61461]}, + {"t":1.58729, "x":0.49951, "y":5.92664, "heading":3.1271, "vx":-0.99959, "vy":-0.01963, "omega":0.0, "ax":1.85009, "ay":9.20613, "alpha":0.0, "fx":[31.46956,31.46956,31.46956,31.46956], "fy":[156.59357,156.59357,156.59357,156.59357]}, + {"t":1.63323, "x":0.45554, "y":5.93545, "heading":3.1271, "vx":-0.9146, "vy":0.40328, "omega":0.0, "ax":8.4588, "ay":4.8153, "alpha":0.0, "fx":[143.88173,143.88173,143.88173,143.88173], "fy":[81.90682,81.90682,81.90682,81.90682]}, + {"t":1.67917, "x":0.42245, "y":5.95906, "heading":3.1271, "vx":-0.52601, "vy":0.62449, "omega":0.0, "ax":9.4653, "ay":2.34333, "alpha":0.0, "fx":[161.00202,161.00202,161.00202,161.00202], "fy":[39.85944,39.85944,39.85944,39.85944]}, + {"t":1.72511, "x":0.40827, "y":5.99022, "heading":3.1271, "vx":-0.09119, "vy":0.73214, "omega":0.0, "ax":9.58027, "ay":1.82225, "alpha":0.0, "fx":[162.9577,162.9577,162.9577,162.9577], "fy":[30.99602,30.99602,30.99602,30.99602]}, + {"t":1.76536, "x":0.41236, "y":6.02117, "heading":3.1271, "vx":0.29445, "vy":0.80549, "omega":0.0, "ax":9.63169, "ay":1.51949, "alpha":0.0, "fx":[163.83224,163.83224,163.83224,163.83224], "fy":[25.84611,25.84611,25.84611,25.84611]}, + {"t":1.80561, "x":0.43202, "y":6.05482, "heading":3.1271, "vx":0.68215, "vy":0.86666, "omega":0.0, "ax":9.68275, "ay":1.13706, "alpha":0.0, "fx":[164.70079,164.70079,164.70079,164.70079], "fy":[19.34105,19.34105,19.34105,19.34105]}, + {"t":1.84587, "x":0.46732, "y":6.09063, "heading":3.1271, "vx":1.07191, "vy":0.91243, "omega":0.0, "ax":9.72619, "ay":0.64204, "alpha":0.0, "fx":[165.43967,165.43967,165.43967,165.43967], "fy":[10.92085,10.92085,10.92085,10.92085]}, + {"t":1.88612, "x":0.51835, "y":6.12788, "heading":3.1271, "vx":1.46341, "vy":0.93827, "omega":0.0, "ax":9.74485, "ay":-0.01709, "alpha":0.0, "fx":[165.75707,165.75707,165.75707,165.75707], "fy":[-0.29068,-0.29068,-0.29068,-0.29068]}, + {"t":1.92637, "x":0.58515, "y":6.16563, "heading":3.1271, "vx":1.85567, "vy":0.93758, "omega":0.0, "ax":9.69785, "ay":-0.92196, "alpha":0.0, "fx":[164.95766,164.95766,164.95766,164.95766], "fy":[-15.68234,-15.68234,-15.68234,-15.68234]}, + {"t":1.96662, "x":0.6677, "y":6.20263, "heading":3.1271, "vx":2.24603, "vy":0.90047, "omega":0.0, "ax":9.48599, "ay":-2.19757, "alpha":0.0, "fx":[161.35399,161.35399,161.35399,161.35399], "fy":[-37.38012,-37.38012,-37.38012,-37.38012]}, + {"t":2.00688, "x":0.7658, "y":6.23709, "heading":3.1271, "vx":2.62787, "vy":0.81201, "omega":0.0, "ax":8.87245, "ay":-3.99806, "alpha":0.0, "fx":[150.91776,150.91776,150.91776,150.91776], "fy":[-68.00594,-68.00594,-68.00594,-68.00594]}, + {"t":2.04713, "x":0.87876, "y":6.26654, "heading":3.1271, "vx":2.98501, "vy":0.65108, "omega":0.0, "ax":7.38032, "ay":-6.33403, "alpha":0.0, "fx":[125.53714,125.53714,125.53714,125.53714], "fy":[-107.74008,-107.74008,-107.74008,-107.74008]}, + {"t":2.08738, "x":1.0049, "y":6.28761, "heading":3.1271, "vx":3.28209, "vy":0.39612, "omega":0.0, "ax":4.56235, "ay":-8.58558, "alpha":0.0, "fx":[77.60432,77.60432,77.60432,77.60432], "fy":[-146.03829,-146.03829,-146.03829,-146.03829]}, + {"t":2.12764, "x":1.14071, "y":6.2966, "heading":3.1271, "vx":3.46574, "vy":0.05052, "omega":0.0, "ax":1.04918, "ay":-9.66822, "alpha":0.0, "fx":[17.84626,17.84626,17.84626,17.84626], "fy":[-164.4537,-164.4537,-164.4537,-164.4537]}, + {"t":2.16789, "x":1.28106, "y":6.29081, "heading":3.1271, "vx":3.50797, "vy":-0.33865, "omega":0.0, "ax":-1.84957, "ay":-9.55339, "alpha":0.0, "fx":[-31.4607,-31.4607,-31.4607,-31.4607], "fy":[-162.50035,-162.50035,-162.50035,-162.50035]}, + {"t":2.20814, "x":1.42077, "y":6.26943, "heading":3.1271, "vx":3.43352, "vy":-0.7232, "omega":0.0, "ax":-3.7718, "ay":-8.97623, "alpha":0.0, "fx":[-64.1572,-64.1572,-64.1572,-64.1572], "fy":[-152.68317,-152.68317,-152.68317,-152.68317]}, + {"t":2.24839, "x":1.55592, "y":6.23305, "heading":3.1271, "vx":3.28169, "vy":-1.08452, "omega":0.0, "ax":-4.98969, "ay":-8.36603, "alpha":0.0, "fx":[-84.87317,-84.87317,-84.87317,-84.87317], "fy":[-142.30373,-142.30373,-142.30373,-142.30373]}, + {"t":2.28865, "x":1.68398, "y":6.18262, "heading":3.1271, "vx":3.08085, "vy":-1.42127, "omega":0.0, "ax":-5.78309, "ay":-7.84282, "alpha":0.0, "fx":[-98.36874,-98.36874,-98.36874,-98.36874], "fy":[-133.40412,-133.40412,-133.40412,-133.40412]}, + {"t":2.3289, "x":1.8033, "y":6.11906, "heading":3.1271, "vx":2.84806, "vy":-1.73697, "omega":0.0, "ax":-6.32408, "ay":-7.4169, "alpha":0.0, "fx":[-107.5708,-107.5708,-107.5708,-107.5708], "fy":[-126.15934,-126.15934,-126.15934,-126.15934]}, + {"t":2.36915, "x":1.91282, "y":6.04313, "heading":3.1271, "vx":2.5935, "vy":-2.03552, "omega":0.0, "ax":-6.70971, "ay":-7.0727, "alpha":0.0, "fx":[-114.1303,-114.1303,-114.1303,-114.1303], "fy":[-120.30455,-120.30455,-120.30455,-120.30455]}, + {"t":2.4094, "x":2.01178, "y":5.95546, "heading":3.1271, "vx":2.32341, "vy":-2.32021, "omega":0.0, "ax":-5.9679, "ay":-6.77454, "alpha":-9.62165, "fx":[-158.2226,-126.2258,-37.23442,-84.36622], "fy":[-49.58274,-107.5984,-161.53004,-142.22086]}, + {"t":2.43889, "x":2.07768, "y":5.88412, "heading":3.1271, "vx":2.14748, "vy":-2.51993, "omega":-0.28365, "ax":-6.22896, "ay":-6.60978, "alpha":-9.13493, "fx":[-158.59772,-127.86037,-43.0544,-94.29898], "fy":[-48.28734,-105.61735,-160.04118,-135.77615]}, + {"t":2.46837, "x":2.13829, "y":5.80696, "heading":3.11874, "vx":1.96384, "vy":-2.71479, "omega":-0.55296, "ax":-6.5172, "ay":-6.39924, "alpha":-8.67285, "fx":[-159.18867,-130.14943,-50.27634,-103.80838], "fy":[-46.1962,-102.74128,-157.87795,-128.58164]}, + {"t":2.49785, "x":2.19335, "y":5.72414, "heading":3.10244, "vx":1.77171, "vy":-2.90345, "omega":-0.80864, "ax":-6.84461, "ay":-6.12749, "alpha":-8.19268, "fx":[-159.9767,-133.13028,-59.25873,-113.33388], "fy":[-43.24795,-98.79352,-154.67647,-120.18921]}, + {"t":2.52733, "x":2.24261, "y":5.63588, "heading":3.0786, "vx":1.56993, "vy":-3.08409, "omega":-1.05017, "ax":-7.22531, "ay":-5.76879, "alpha":-7.64447, "fx":[-160.93522,-136.86214,-70.53855,-123.2658], "fy":[-39.33667,-93.48113,-149.79879,-109.88545]}, + {"t":2.55681, "x":2.28575, "y":5.54246, "heading":3.04764, "vx":1.35692, "vy":-3.25416, "omega":-1.27553, "ax":-7.67403, "ay":-5.28138, "alpha":-6.96355, "fx":[-162.02471,-141.42947,-84.87796,-133.80038], "fy":[-34.28798,-86.30957,-142.07025,-96.67093]}, + {"t":2.58629, "x":2.32242, "y":5.44423, "heading":3.01003, "vx":1.13068, "vy":-3.40986, "omega":-1.48082, "ax":-8.20109, "ay":-4.59726, "alpha":-6.05807, "fx":[-163.1807,-146.9306,-103.19642,-144.68479], "fy":[-27.81337,-76.40591,-129.24053,-79.33242]}, + {"t":2.61577, "x":2.35219, "y":5.3417, "heading":2.96638, "vx":0.88891, "vy":-3.54539, "omega":-1.65942, "ax":-8.79525, "ay":-3.60722, "alpha":-4.79665, "fx":[-164.2832,-153.38937,-125.87916,-154.86677], "fy":[-19.42086,-62.14523,-107.05653,-56.80843]}, + {"t":2.64525, "x":2.37457, "y":5.23561, "heading":2.91746, "vx":0.62962, "vy":-3.65173, "omega":-1.80082, "ax":-9.37004, "ay":-2.16452, "alpha":-3.06979, "fx":[-165.07286,-160.29083,-149.83644,-162.32639], "fy":[-8.24256,-40.53617,-69.3038,-29.18892]}, + {"t":2.67473, "x":2.38906, "y":5.12702, "heading":2.86437, "vx":0.35338, "vy":-3.71554, "omega":-1.89132, "ax":-9.66296, "ay":-0.47806, "alpha":-1.55074, "fx":[-164.93844,-164.6331,-163.02442,-164.8609], "fy":[6.98388,-12.68236,-25.01402,-1.81437]}, + {"t":2.70421, "x":2.39528, "y":5.01727, "heading":2.80861, "vx":0.06851, "vy":-3.72964, "omega":-1.93704, "ax":-9.63608, "ay":0.54738, "alpha":-1.80678, "fx":[-162.80398,-164.91789,-164.3415,-163.56433], "fy":[26.24697,3.04869,-10.40675,18.35398]}, + {"t":2.73369, "x":2.39311, "y":4.90756, "heading":2.75151, "vx":-0.21556, "vy":-3.7135, "omega":-1.99031, "ax":-9.45559, "ay":1.59404, "alpha":-2.69027, "fx":[-157.02956,-163.86996,-164.24786,-158.20011], "fy":[49.73084,17.1615,-2.11677,43.68077]}, + {"t":2.76317, "x":2.38265, "y":4.79878, "heading":2.69283, "vx":-0.49432, "vy":-3.66651, "omega":-2.06962, "ax":-9.00544, "ay":2.84238, "alpha":-4.27347, "fx":[-145.51414,-161.35878,-163.55181,-142.29539], "fy":[76.77915,32.37511,3.98101,80.25676]}, + {"t":2.79265, "x":2.36416, "y":4.69192, "heading":2.63182, "vx":-0.75981, "vy":-3.58271, "omega":-2.1956, "ax":-7.98278, "ay":4.38014, "alpha":-7.00269, "fx":[-126.30498,-156.30916,-161.93137,-98.59381], "fy":[105.18263,50.8421,12.93244,129.06252]}, + {"t":2.82213, "x":2.33829, "y":4.5882, "heading":2.56709, "vx":-0.99515, "vy":-3.45358, "omega":-2.40205, "ax":-6.17213, "ay":5.9355, "alpha":-10.36658, "fx":[-98.87944,-146.45829,-155.69908,-18.90777], "fy":[131.17126,74.15216,37.57254,160.94867]}, + {"t":2.85162, "x":2.30627, "y":4.48897, "heading":2.49627, "vx":-1.17711, "vy":-3.2786, "omega":-2.70766, "ax":-4.01683, "ay":6.97646, "alpha":-12.69006, "fx":[-67.02797,-132.82502,-132.55122,59.10397], "fy":[149.96806,96.21382,77.11218,151.3766]}, + {"t":2.8811, "x":2.26983, "y":4.39534, "heading":2.41645, "vx":-1.29552, "vy":-3.07293, "omega":-3.08177, "ax":-0.34937, "ay":8.55357, "alpha":-11.07355, "fx":[-30.65918,-101.71791,20.39353,88.2126], "fy":[161.42691,128.50169,154.68873,137.35753]}, + {"t":2.91058, "x":2.23148, "y":4.30847, "heading":2.3256, "vx":-1.30582, "vy":-2.82076, "omega":-3.40823, "ax":1.59761, "ay":9.52496, "alpha":0.74197, "fx":[29.17513,33.2747,25.2491,21.00086], "fy":[161.67052,160.93927,162.45445,163.003]}, + {"t":2.94006, "x":2.19368, "y":4.22945, "heading":2.22512, "vx":-1.25873, "vy":-2.53996, "omega":-3.38635, "ax":2.83092, "ay":8.95992, "alpha":6.54608, "fx":[71.4913,95.06673,32.42521,-6.37103], "fy":[148.01543,134.79297,161.96722,164.8473]}, + {"t":2.96954, "x":2.1578, "y":4.15846, "heading":2.12529, "vx":-1.17527, "vy":-2.27581, "omega":-3.19337, "ax":3.51483, "ay":8.41599, "alpha":9.09785, "fx":[94.91377,120.36955,39.98687,-16.12508], "fy":[134.32075,113.21959,160.56392,164.50992]}, + {"t":2.99902, "x":2.12468, "y":4.09503, "heading":2.03115, "vx":-1.07165, "vy":-2.0277, "omega":-2.92516, "ax":3.94388, "ay":8.02729, "alpha":10.23151, "fx":[105.95459,133.88009,47.26326,-18.76076], "fy":[125.8602,97.15522,158.72962,164.42292]}, + {"t":3.0285, "x":2.0948, "y":4.03874, "heading":1.94491, "vx":-0.95538, "vy":-1.79105, "omega":-2.62353, "ax":4.21824, "ay":7.77393, "alpha":10.64812, "fx":[108.90806,142.41963,53.97943,-18.30317], "fy":[123.31836,84.34513,156.66864,164.59706]}, + {"t":3.05798, "x":2.06847, "y":3.98931, "heading":1.86757, "vx":-0.83102, "vy":-1.56187, "omega":-2.30961, "ax":4.37092, "ay":7.63037, "alpha":10.69562, "fx":[105.51601,148.34576,60.02446,-16.49387], "fy":[126.1894,73.58237,154.51852,164.87137]}, + {"t":3.08746, "x":2.04587, "y":3.94659, "heading":1.79948, "vx":-0.70217, "vy":-1.33693, "omega":-1.9943, "ax":4.40758, "ay":7.57339, "alpha":10.60339, "fx":[96.02209,152.66818,65.36339,-14.16728], "fy":[133.48691,64.26718,152.38402,165.14683]}, + {"t":3.11694, "x":2.02709, "y":3.91046, "heading":1.74068, "vx":-0.57223, "vy":-1.11366, "omega":-1.6817, "ax":4.3262, "ay":7.57397, "alpha":10.56761, "fx":[80.21112,155.89122,69.99579,-11.74871], "fy":[143.48066,56.11471,150.34825,165.38036]}, + {"t":3.14642, "x":2.0121, "y":3.88092, "heading":1.69111, "vx":-0.44469, "vy":-0.89037, "omega":-1.37016, "ax":4.13776, "ay":7.59191, "alpha":10.75621, "fx":[58.76247,158.29962,73.93249,-9.46628], "fy":[153.48733,49.01746,148.48003,165.56011]}, + {"t":3.1759, "x":2.00078, "y":3.85797, "heading":1.65071, "vx":-0.3227, "vy":-0.66656, "omega":-1.05306, "ax":3.88424, "ay":7.58699, "alpha":11.23683, "fx":[34.48087,160.07594,77.18057,-7.45845], "fy":[160.71569,42.96547,146.84041,165.68873]}, + {"t":3.20538, "x":1.99296, "y":3.84162, "heading":1.61967, "vx":-0.20819, "vy":-0.44289, "omega":-0.72179, "ax":3.63056, "ay":7.5438, "alpha":11.91068, "fx":[11.76645,161.35282,79.73373,-5.8337], "fy":[164.01727,37.99115,145.4883,165.77451]}, + {"t":3.23486, "x":1.9884, "y":3.83184, "heading":1.59839, "vx":-0.10116, "vy":-0.22049, "omega":-0.37066, "ax":3.43148, "ay":7.4791, "alpha":12.57291, "fx":[-5.63216,162.23384,81.5694,-4.69735], "fy":[164.42428,34.13534,144.48244,165.82715]}, + {"t":3.26434, "x":1.98691, "y":3.82859, "heading":1.58746, "vx":0.0, "vy":0.0, "omega":0.0, "ax":0.0, "ay":0.0, "alpha":0.0, "fx":[0.0,0.0,0.0,0.0], "fy":[0.0,0.0,0.0,0.0]}], + "splits":[0] + }, + "events":[] +} diff --git a/lib/examples/swerve/src/main/deploy/choreo/LEFT_TRENCH.traj b/lib/examples/swerve/src/main/deploy/choreo/LEFT_TRENCH.traj new file mode 100644 index 00000000..863d839f --- /dev/null +++ b/lib/examples/swerve/src/main/deploy/choreo/LEFT_TRENCH.traj @@ -0,0 +1,387 @@ +{ + "name":"LEFT_TRENCH", + "version":3, + "snapshot":{ + "waypoints":[ + {"x":4.441253185272217, "y":7.693458557128906, "heading":-1.5707972442597578, "intervals":38, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":7.795207023620605, "y":7.227133750915527, "heading":-1.5707963267948966, "intervals":56, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":7.795207023620605, "y":4.531103515625, "heading":-1.5707963267948966, "intervals":129, "split":false, "fixTranslation":true, "fixHeading":false, "overrideIntervals":false}, + {"x":5.723647117614746, "y":7.42442512512207, "heading":0.0, "intervals":51, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":3.2933757305145264, "y":7.325779438018799, "heading":-1.5707963267948966, "intervals":33, "split":false, "fixTranslation":true, "fixHeading":false, "overrideIntervals":false}, + {"x":2.809114933013916, "y":5.299058437347412, "heading":0.8379811662545806, "intervals":40, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}], + "constraints":[ + {"from":"first", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"last", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"first", "to":"last", "data":{"type":"KeepInRectangle", "props":{"x":0.0, "y":0.0, "w":16.541, "h":8.0692}}, "enabled":true}, + {"from":1, "to":2, "data":{"type":"KeepInLane", "props":{"tolerance":0.00001}}, "enabled":true}, + {"from":1, "to":2, "data":{"type":"MaxAngularVelocity", "props":{"max":0.0}}, "enabled":true}, + {"from":"first", "to":"last", "data":{"type":"MaxVelocity", "props":{"max":2.0}}, "enabled":true}, + {"from":3, "to":4, "data":{"type":"MaxAngularVelocity", "props":{"max":0.0}}, "enabled":true}, + {"from":1, "to":2, "data":{"type":"MaxVelocity", "props":{"max":1.0}}, "enabled":true}, + {"from":2, "to":3, "data":{"type":"MaxAngularVelocity", "props":{"max":0.5}}, "enabled":true}, + {"from":3, "to":4, "data":{"type":"MaxVelocity", "props":{"max":1.0}}, "enabled":true}], + "targetDt":0.05 + }, + "params":{ + "waypoints":[ + {"x":{"exp":"4.441253185272217 m", "val":4.441253185272217}, "y":{"exp":"7.693458557128906 m", "val":7.693458557128906}, "heading":{"exp":"-1.5707972442597578 rad", "val":-1.5707972442597578}, "intervals":38, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":{"exp":"7.7952070236206055 m", "val":7.795207023620605}, "y":{"exp":"7.227133750915527 m", "val":7.227133750915527}, "heading":{"exp":"-1.5707963267948966 rad", "val":-1.5707963267948966}, "intervals":56, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":{"exp":"7.7952070236206055 m", "val":7.795207023620605}, "y":{"exp":"4.531103515625 m", "val":4.531103515625}, "heading":{"exp":"-1.5707963267948966 rad", "val":-1.5707963267948966}, "intervals":129, "split":false, "fixTranslation":true, "fixHeading":false, "overrideIntervals":false}, + {"x":{"exp":"5.723647117614746 m", "val":5.723647117614746}, "y":{"exp":"7.42442512512207 m", "val":7.42442512512207}, "heading":{"exp":"0 rad", "val":0.0}, "intervals":51, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":{"exp":"3.2933757305145264 m", "val":3.2933757305145264}, "y":{"exp":"7.325779438018799 m", "val":7.325779438018799}, "heading":{"exp":"-90 deg", "val":-1.5707963267948966}, "intervals":33, "split":false, "fixTranslation":true, "fixHeading":false, "overrideIntervals":false}, + {"x":{"exp":"2.809114933013916 m", "val":2.809114933013916}, "y":{"exp":"5.299058437347412 m", "val":5.299058437347412}, "heading":{"exp":"0.8379811662545806 rad", "val":0.8379811662545806}, "intervals":40, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}], + "constraints":[ + {"from":"first", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"last", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"first", "to":"last", "data":{"type":"KeepInRectangle", "props":{"x":{"exp":"0 m", "val":0.0}, "y":{"exp":"0 m", "val":0.0}, "w":{"exp":"16.541 m", "val":16.541}, "h":{"exp":"8.0692 m", "val":8.0692}}}, "enabled":true}, + {"from":1, "to":2, "data":{"type":"KeepInLane", "props":{"tolerance":{"exp":"1e-5 m", "val":0.00001}}}, "enabled":true}, + {"from":1, "to":2, "data":{"type":"MaxAngularVelocity", "props":{"max":{"exp":"0 rad / s", "val":0.0}}}, "enabled":true}, + {"from":"first", "to":"last", "data":{"type":"MaxVelocity", "props":{"max":{"exp":"2 m / s", "val":2.0}}}, "enabled":true}, + {"from":3, "to":4, "data":{"type":"MaxAngularVelocity", "props":{"max":{"exp":"0 rad / s", "val":0.0}}}, "enabled":true}, + {"from":1, "to":2, "data":{"type":"MaxVelocity", "props":{"max":{"exp":"1 m / s", "val":1.0}}}, "enabled":true}, + {"from":2, "to":3, "data":{"type":"MaxAngularVelocity", "props":{"max":{"exp":"0.5 rad / s", "val":0.5}}}, "enabled":true}, + {"from":3, "to":4, "data":{"type":"MaxVelocity", "props":{"max":{"exp":"1 m / s", "val":1.0}}}, "enabled":true}], + "targetDt":{ + "exp":"0.05 s", + "val":0.05 + } + }, + "trajectory":{ + "config":{ + "frontLeft":{ + "x":0.225425, + "y":0.333375 + }, + "backLeft":{ + "x":-0.225425, + "y":0.333375 + }, + "mass":68.0388555, + "inertia":7.5, + "gearing":7.03, + "radius":0.0508, + "vmax":607.3745796940267, + "tmax":1.2, + "cof":1.5, + "bumper":{ + "front":0.34925, + "side":0.4445, + "back":0.34925 + }, + "differentialTrackWidth":0.5588 + }, + "sampleType":"Swerve", + "waypoints":[0.0,1.88626,4.58447,7.75189,10.19451,11.3735], + "samples":[ + {"t":0.0, "x":4.44125, "y":7.69346, "heading":-1.5708, "vx":0.0, "vy":0.0, "omega":0.0, "ax":9.71375, "ay":-0.89248, "alpha":0.0, "fx":[165.22814,165.22814,165.22814,165.22814], "fy":[-15.18078,-15.18078,-15.18079,-15.18079]}, + {"t":0.04964, "x":4.45322, "y":7.69236, "heading":-1.5708, "vx":0.48217, "vy":-0.0443, "omega":0.0, "ax":9.71164, "ay":-0.88564, "alpha":0.0, "fx":[165.1922,165.1922,165.1922,165.1922], "fy":[-15.06444,-15.06444,-15.06445,-15.06445]}, + {"t":0.09928, "x":4.48912, "y":7.68907, "heading":-1.5708, "vx":0.96424, "vy":-0.08826, "omega":0.0, "ax":9.70675, "ay":-0.87909, "alpha":0.0, "fx":[165.10907,165.10907,165.10907,165.10907], "fy":[-14.95309,-14.9531,-14.9531,-14.9531]}, + {"t":0.14891, "x":4.54894, "y":7.6836, "heading":-1.5708, "vx":1.44607, "vy":-0.1319, "omega":0.0, "ax":9.69092, "ay":-0.87202, "alpha":0.0, "fx":[164.83972,164.83972,164.83972,164.83972], "fy":[-14.83278,-14.83278,-14.83279,-14.83279]}, + {"t":0.19855, "x":4.63266, "y":7.67598, "heading":-1.5708, "vx":1.92711, "vy":-0.17518, "omega":0.0, "ax":1.30117, "ay":-0.04707, "alpha":0.00006, "fx":[22.13265,22.13235,22.13235,22.13265], "fy":[-0.80035,-0.80035,-0.80081,-0.80081]}, + {"t":0.24819, "x":4.72992, "y":7.66723, "heading":-1.5708, "vx":1.9917, "vy":-0.17752, "omega":0.0, "ax":0.00291, "ay":0.03269, "alpha":0.00002, "fx":[0.04948,0.0494,0.0494,0.04948], "fy":[0.55606,0.55606,0.55593,0.55593]}, + {"t":0.29783, "x":4.82879, "y":7.65846, "heading":-1.5708, "vx":1.99184, "vy":-0.1759, "omega":0.0, "ax":0.00247, "ay":0.02806, "alpha":-0.00001, "fx":[0.04196,0.04199,0.04199,0.04196], "fy":[0.47736,0.47736,0.4774,0.4774]}, + {"t":0.34747, "x":4.92767, "y":7.64976, "heading":-1.5708, "vx":1.99197, "vy":-0.17451, "omega":0.0, "ax":0.00219, "ay":0.02507, "alpha":-0.00001, "fx":[0.03718,0.03725,0.03725,0.03718], "fy":[0.42642,0.42642,0.42653,0.42653]}, + {"t":0.39711, "x":5.02655, "y":7.64113, "heading":-1.5708, "vx":1.99207, "vy":-0.17326, "omega":0.0, "ax":0.00197, "ay":0.02276, "alpha":-0.00002, "fx":[0.03352,0.03359,0.03359,0.03352], "fy":[0.38711,0.38711,0.38723,0.38723]}, + {"t":0.44674, "x":5.12543, "y":7.63256, "heading":-1.5708, "vx":1.99217, "vy":-0.17213, "omega":0.0, "ax":0.0018, "ay":0.02089, "alpha":-0.00001, "fx":[0.03056,0.03064,0.03064,0.03056], "fy":[0.35526,0.35526,0.35537,0.35537]}, + {"t":0.49638, "x":5.22432, "y":7.62404, "heading":-1.5708, "vx":1.99226, "vy":-0.17109, "omega":0.0, "ax":0.00166, "ay":0.01934, "alpha":-0.00001, "fx":[0.02813,0.02819,0.02819,0.02813], "fy":[0.32886,0.32886,0.32895,0.32895]}, + {"t":0.54602, "x":5.32322, "y":7.61557, "heading":-1.5708, "vx":1.99234, "vy":-0.17013, "omega":0.0, "ax":0.00154, "ay":0.01803, "alpha":-0.00001, "fx":[0.02609,0.02613,0.02613,0.02609], "fy":[0.30663,0.30663,0.30669,0.30669]}, + {"t":0.59566, "x":5.42211, "y":7.60715, "heading":-1.5708, "vx":1.99242, "vy":-0.16924, "omega":0.0, "ax":0.00143, "ay":0.01691, "alpha":-0.00001, "fx":[0.02435,0.02438,0.02438,0.02435], "fy":[0.28761,0.28761,0.28765,0.28765]}, + {"t":0.6453, "x":5.52102, "y":7.59877, "heading":-1.5708, "vx":1.99249, "vy":-0.1684, "omega":0.0, "ax":0.00134, "ay":0.01594, "alpha":0.0, "fx":[0.02285,0.02287,0.02287,0.02285], "fy":[0.27113,0.27113,0.27116,0.27116]}, + {"t":0.69494, "x":5.61992, "y":7.59043, "heading":-1.5708, "vx":1.99256, "vy":-0.16761, "omega":0.0, "ax":0.00127, "ay":0.01509, "alpha":0.0, "fx":[0.02153,0.02155,0.02155,0.02153], "fy":[0.25671,0.25671,0.25673,0.25673]}, + {"t":0.74457, "x":5.71883, "y":7.58213, "heading":-1.5708, "vx":1.99262, "vy":-0.16686, "omega":0.0, "ax":0.0012, "ay":0.01434, "alpha":0.0, "fx":[0.02038,0.02038,0.02038,0.02038], "fy":[0.24396,0.24396,0.24397,0.24397]}, + {"t":0.79421, "x":5.81774, "y":7.57386, "heading":-1.5708, "vx":1.99268, "vy":-0.16615, "omega":0.0, "ax":0.00114, "ay":0.01367, "alpha":0.0, "fx":[0.01935,0.01935,0.01935,0.01935], "fy":[0.2326,0.2326,0.23261,0.23261]}, + {"t":0.84385, "x":5.91666, "y":7.56563, "heading":-1.5708, "vx":1.99274, "vy":-0.16547, "omega":0.0, "ax":0.00108, "ay":0.01308, "alpha":0.0, "fx":[0.01843,0.01843,0.01843,0.01843], "fy":[0.22241,0.22241,0.22241,0.22241]}, + {"t":0.89349, "x":6.01557, "y":7.55743, "heading":-1.5708, "vx":1.99279, "vy":-0.16482, "omega":0.0, "ax":0.00103, "ay":0.01253, "alpha":0.0, "fx":[0.0176,0.01759,0.01759,0.0176], "fy":[0.2132,0.2132,0.2132,0.2132]}, + {"t":0.94313, "x":6.11449, "y":7.54927, "heading":-1.5708, "vx":1.99284, "vy":-0.1642, "omega":0.0, "ax":0.00099, "ay":0.01204, "alpha":0.0, "fx":[0.01684,0.01684,0.01684,0.01684], "fy":[0.20484,0.20484,0.20484,0.20484]}, + {"t":0.99277, "x":6.21342, "y":7.54113, "heading":-1.5708, "vx":1.99289, "vy":-0.1636, "omega":0.0, "ax":0.00095, "ay":0.01159, "alpha":0.0, "fx":[0.01616,0.01615,0.01615,0.01616], "fy":[0.1972,0.1972,0.1972,0.1972]}, + {"t":1.0424, "x":6.31234, "y":7.53303, "heading":-1.5708, "vx":1.99294, "vy":-0.16302, "omega":0.0, "ax":0.00091, "ay":0.01118, "alpha":0.0, "fx":[0.01553,0.01553,0.01553,0.01553], "fy":[0.1902,0.1902,0.1902,0.1902]}, + {"t":1.09204, "x":6.41127, "y":7.52495, "heading":-1.5708, "vx":1.99298, "vy":-0.16247, "omega":0.0, "ax":0.00088, "ay":0.0108, "alpha":0.0, "fx":[0.01495,0.01495,0.01495,0.01495], "fy":[0.18376,0.18376,0.18375,0.18375]}, + {"t":1.14168, "x":6.5102, "y":7.5169, "heading":-1.5708, "vx":1.99303, "vy":-0.16193, "omega":0.0, "ax":0.00085, "ay":0.01045, "alpha":0.0, "fx":[0.01442,0.01442,0.01442,0.01442], "fy":[0.1778,0.1778,0.17779,0.17779]}, + {"t":1.19132, "x":6.60913, "y":7.50887, "heading":-1.5708, "vx":1.99307, "vy":-0.16141, "omega":0.0, "ax":0.00082, "ay":0.01013, "alpha":0.0, "fx":[0.01393,0.01393,0.01393,0.01393], "fy":[0.17227,0.17227,0.17227,0.17227]}, + {"t":1.24096, "x":6.70806, "y":7.50087, "heading":-1.5708, "vx":1.99311, "vy":-0.16091, "omega":0.0, "ax":0.00079, "ay":0.00983, "alpha":0.0, "fx":[0.01347,0.01347,0.01347,0.01347], "fy":[0.16713,0.16713,0.16713,0.16713]}, + {"t":1.2906, "x":6.807, "y":7.49289, "heading":-1.5708, "vx":1.99315, "vy":-0.16042, "omega":0.0, "ax":0.00077, "ay":0.00954, "alpha":0.0, "fx":[0.01304,0.01304,0.01304,0.01304], "fy":[0.16229,0.16229,0.16229,0.16229]}, + {"t":1.34023, "x":6.90594, "y":7.48494, "heading":-1.5708, "vx":1.99319, "vy":-0.15995, "omega":0.0, "ax":0.00074, "ay":0.00927, "alpha":0.0, "fx":[0.01263,0.01263,0.01263,0.01263], "fy":[0.15771,0.15771,0.15771,0.15771]}, + {"t":1.38987, "x":7.00488, "y":7.47702, "heading":-1.5708, "vx":1.99322, "vy":-0.15949, "omega":0.0, "ax":0.00072, "ay":0.00904, "alpha":0.0, "fx":[0.01229,0.01229,0.01229,0.01229], "fy":[0.15382,0.15382,0.15382,0.15382]}, + {"t":1.43951, "x":7.10382, "y":7.46911, "heading":-1.5708, "vx":1.99326, "vy":-0.15904, "omega":0.0, "ax":0.0007, "ay":0.00881, "alpha":0.0, "fx":[0.01194,0.01194,0.01194,0.01194], "fy":[0.14992,0.14992,0.14992,0.14992]}, + {"t":1.48915, "x":7.20276, "y":7.46123, "heading":-1.5708, "vx":1.99329, "vy":-0.1586, "omega":0.0, "ax":0.00059, "ay":0.00742, "alpha":0.0, "fx":[0.01002,0.01002,0.01002,0.01002], "fy":[0.12614,0.12614,0.12614,0.12614]}, + {"t":1.53879, "x":7.3017, "y":7.45336, "heading":-1.5708, "vx":1.99332, "vy":-0.15824, "omega":0.0, "ax":-0.00226, "ay":-0.02832, "alpha":0.0, "fx":[-0.03841,-0.03841,-0.03841,-0.03841], "fy":[-0.48167,-0.48167,-0.48167,-0.48167]}, + {"t":1.58843, "x":7.40065, "y":7.44547, "heading":-1.5708, "vx":1.99321, "vy":-0.15964, "omega":0.0, "ax":-0.13389, "ay":-1.37465, "alpha":0.0, "fx":[-2.27744,-2.27744,-2.27744,-2.27744], "fy":[-23.38237,-23.38237,-23.38237,-23.38237]}, + {"t":1.63806, "x":7.49942, "y":7.43586, "heading":-1.5708, "vx":1.98657, "vy":-0.22788, "omega":0.0, "ax":-1.99103, "ay":-8.68601, "alpha":0.0, "fx":[-33.86677,-33.86677,-33.86677,-33.86677], "fy":[-147.7466,-147.7466,-147.7466,-147.7466]}, + {"t":1.6877, "x":7.59558, "y":7.41384, "heading":-1.5708, "vx":1.88773, "vy":-0.65904, "omega":0.0, "ax":-7.82486, "ay":-5.68329, "alpha":0.0, "fx":[-133.09866,-133.09866,-133.09866,-133.09866], "fy":[-96.67106,-96.67106,-96.67106,-96.67106]}, + {"t":1.73734, "x":7.67964, "y":7.37413, "heading":-1.5708, "vx":1.49932, "vy":-0.94114, "omega":0.0, "ax":-9.68718, "ay":-0.98874, "alpha":0.0, "fx":[-164.77616,-164.77616,-164.77616,-164.77616], "fy":[-16.81816,-16.81816,-16.81816,-16.81816]}, + {"t":1.78698, "x":7.74213, "y":7.32619, "heading":-1.5708, "vx":1.01847, "vy":-0.99022, "omega":0.0, "ax":-9.74584, "ay":-0.22781, "alpha":0.0, "fx":[-165.77401,-165.77401,-165.77401,-165.77401], "fy":[-3.87492,-3.87492,-3.87492,-3.87492]}, + {"t":1.83662, "x":7.78068, "y":7.27676, "heading":-1.5708, "vx":0.5347, "vy":-1.00153, "omega":0.0, "ax":-9.75259, "ay":0.07234, "alpha":0.0, "fx":[-165.88882,-165.88882,-165.88882,-165.88882], "fy":[1.23044,1.23044,1.23044,1.23044]}, + {"t":1.88626, "x":7.79521, "y":7.22713, "heading":-1.5708, "vx":0.0506, "vy":-0.99794, "omega":0.0, "ax":-2.09267, "ay":-0.01224, "alpha":0.0, "fx":[-35.59575,-35.59575,-35.59575,-35.59575], "fy":[-0.20819,-0.20819,-0.20819,-0.20819]}, + {"t":1.93444, "x":7.79522, "y":7.17904, "heading":-1.5708, "vx":-0.05023, "vy":-0.99853, "omega":0.0, "ax":2.07007, "ay":-0.00075, "alpha":0.0, "fx":[35.21127,35.21127,35.21127,35.21127], "fy":[-0.01279,-0.01279,-0.01279,-0.01279]}, + {"t":1.98262, "x":7.7952, "y":7.13092, "heading":-1.5708, "vx":0.04951, "vy":-0.99857, "omega":0.0, "ax":-2.04001, "ay":-0.00074, "alpha":0.0, "fx":[-34.69992,-34.69992,-34.69992,-34.69992], "fy":[-0.01255,-0.01255,-0.01255,-0.01255]}, + {"t":2.0308, "x":7.79522, "y":7.08281, "heading":-1.5708, "vx":-0.04878, "vy":-0.9986, "omega":0.0, "ax":2.01006, "ay":-0.00072, "alpha":0.0, "fx":[34.19054,34.19054,34.19054,34.19054], "fy":[-0.01232,-0.01232,-0.01232,-0.01232]}, + {"t":2.07899, "x":7.7952, "y":7.03469, "heading":-1.5708, "vx":0.04806, "vy":-0.99864, "omega":0.0, "ax":-1.98023, "ay":-0.00071, "alpha":0.0, "fx":[-33.68323,-33.68323,-33.68323,-33.68323], "fy":[-0.01209,-0.01209,-0.01209,-0.01209]}, + {"t":2.12717, "x":7.79522, "y":6.98658, "heading":-1.5708, "vx":-0.04735, "vy":-0.99867, "omega":0.0, "ax":1.95054, "ay":-0.0007, "alpha":0.0, "fx":[33.17806,33.17806,33.17806,33.17806], "fy":[-0.01185,-0.01185,-0.01185,-0.01185]}, + {"t":2.17535, "x":7.7952, "y":6.93846, "heading":-1.5708, "vx":0.04663, "vy":-0.9987, "omega":0.0, "ax":-1.92097, "ay":-0.00068, "alpha":0.0, "fx":[-32.6751,-32.6751,-32.6751,-32.6751], "fy":[-0.01162,-0.01162,-0.01162,-0.01162]}, + {"t":2.22353, "x":7.79522, "y":6.89034, "heading":-1.5708, "vx":-0.04592, "vy":-0.99874, "omega":0.0, "ax":1.89153, "ay":-0.00067, "alpha":0.0, "fx":[32.17446,32.17446,32.17446,32.17446], "fy":[-0.01139,-0.01139,-0.01139,-0.01139]}, + {"t":2.27172, "x":7.7952, "y":6.84221, "heading":-1.5708, "vx":0.04522, "vy":-0.99877, "omega":0.0, "ax":-1.86224, "ay":-0.00066, "alpha":0.0, "fx":[-31.67621,-31.67621,-31.67621,-31.67621], "fy":[-0.01116,-0.01116,-0.01116,-0.01116]}, + {"t":2.3199, "x":7.79522, "y":6.79409, "heading":-1.5708, "vx":-0.04451, "vy":-0.9988, "omega":0.0, "ax":1.8331, "ay":-0.00064, "alpha":0.0, "fx":[31.18047,31.18047,31.18047,31.18047], "fy":[-0.01093,-0.01093,-0.01093,-0.01093]}, + {"t":2.36808, "x":7.7952, "y":6.74596, "heading":-1.5708, "vx":0.04381, "vy":-0.99883, "omega":0.0, "ax":-1.80411, "ay":-0.00063, "alpha":0.0, "fx":[-30.68732,-30.68732,-30.68732,-30.68732], "fy":[-0.0107,-0.0107,-0.0107,-0.0107]}, + {"t":2.41626, "x":7.79522, "y":6.69784, "heading":-1.5708, "vx":-0.04311, "vy":-0.99886, "omega":0.0, "ax":1.77527, "ay":-0.00062, "alpha":0.0, "fx":[30.19689,30.19689,30.19689,30.19689], "fy":[-0.01047,-0.01047,-0.01047,-0.01047]}, + {"t":2.46445, "x":7.7952, "y":6.64971, "heading":-1.5708, "vx":0.04242, "vy":-0.99889, "omega":0.0, "ax":-1.74661, "ay":-0.0006, "alpha":0.0, "fx":[-29.70927,-29.70927,-29.70927,-29.70927], "fy":[-0.01024,-0.01024,-0.01024,-0.01024]}, + {"t":2.51263, "x":7.79522, "y":6.60158, "heading":-1.5708, "vx":-0.04173, "vy":-0.99892, "omega":0.0, "ax":1.71811, "ay":-0.00059, "alpha":0.0, "fx":[29.2246,29.2246,29.2246,29.2246], "fy":[-0.01001,-0.01001,-0.01001,-0.01001]}, + {"t":2.56081, "x":7.7952, "y":6.55345, "heading":-1.5708, "vx":0.04105, "vy":-0.99895, "omega":0.0, "ax":-1.6898, "ay":-0.00057, "alpha":0.0, "fx":[-28.74299,-28.74299,-28.74299,-28.74299], "fy":[-0.00978,-0.00978,-0.00978,-0.00978]}, + {"t":2.60899, "x":7.79522, "y":6.50532, "heading":-1.5708, "vx":-0.04037, "vy":-0.99898, "omega":0.0, "ax":1.66167, "ay":-0.00056, "alpha":0.0, "fx":[28.26459,28.26459,28.26459,28.26459], "fy":[-0.00955,-0.00955,-0.00955,-0.00955]}, + {"t":2.65718, "x":7.7952, "y":6.45718, "heading":-1.5708, "vx":0.03969, "vy":-0.999, "omega":0.0, "ax":-1.63374, "ay":-0.00055, "alpha":0.0, "fx":[-27.78953,-27.78953,-27.78953,-27.78953], "fy":[-0.00932,-0.00932,-0.00932,-0.00932]}, + {"t":2.70536, "x":7.79522, "y":6.40905, "heading":-1.5708, "vx":-0.03902, "vy":-0.99903, "omega":0.0, "ax":1.60602, "ay":-0.00053, "alpha":0.0, "fx":[27.31796,27.31796,27.31796,27.31796], "fy":[-0.00909,-0.00909,-0.00909,-0.00909]}, + {"t":2.75354, "x":7.7952, "y":6.36091, "heading":-1.5708, "vx":0.03836, "vy":-0.99906, "omega":0.0, "ax":-1.57851, "ay":-0.00052, "alpha":0.0, "fx":[-26.85004,-26.85004,-26.85004,-26.85004], "fy":[-0.00887,-0.00887,-0.00887,-0.00887]}, + {"t":2.80172, "x":7.79521, "y":6.31277, "heading":-1.5708, "vx":-0.0377, "vy":-0.99908, "omega":0.0, "ax":1.55123, "ay":-0.00051, "alpha":0.0, "fx":[26.38594,26.38594,26.38594,26.38594], "fy":[-0.00864,-0.00864,-0.00864,-0.00864]}, + {"t":2.84991, "x":7.7952, "y":6.26463, "heading":-1.5708, "vx":0.03704, "vy":-0.99911, "omega":0.0, "ax":-1.52418, "ay":-0.00049, "alpha":0.0, "fx":[-25.92584,-25.92584,-25.92584,-25.92584], "fy":[-0.00841,-0.00841,-0.00841,-0.00841]}, + {"t":2.89809, "x":7.79521, "y":6.21649, "heading":-1.5708, "vx":-0.03639, "vy":-0.99913, "omega":0.0, "ax":1.49737, "ay":-0.00048, "alpha":0.0, "fx":[25.46992,25.46992,25.46992,25.46992], "fy":[-0.00819,-0.00819,-0.00819,-0.00819]}, + {"t":2.94627, "x":7.7952, "y":6.16835, "heading":-1.5708, "vx":0.03575, "vy":-0.99915, "omega":0.0, "ax":-1.47083, "ay":-0.00047, "alpha":0.0, "fx":[-25.01838,-25.01838,-25.01838,-25.01838], "fy":[-0.00796,-0.00796,-0.00796,-0.00796]}, + {"t":2.99445, "x":7.79521, "y":6.12021, "heading":-1.5708, "vx":-0.03512, "vy":-0.99918, "omega":0.0, "ax":1.44455, "ay":-0.00045, "alpha":0.0, "fx":[24.57145,24.57145,24.57145,24.57145], "fy":[-0.00774,-0.00774,-0.00774,-0.00774]}, + {"t":3.04264, "x":7.7952, "y":6.07207, "heading":-1.5708, "vx":0.03449, "vy":-0.9992, "omega":0.0, "ax":-1.41856, "ay":-0.00044, "alpha":0.0, "fx":[-24.12934,-24.12934,-24.12934,-24.12934], "fy":[-0.00752,-0.00752,-0.00752,-0.00752]}, + {"t":3.09082, "x":7.79521, "y":6.02392, "heading":-1.5708, "vx":-0.03386, "vy":-0.99922, "omega":0.0, "ax":1.39287, "ay":-0.00043, "alpha":0.0, "fx":[23.6923,23.6923,23.6923,23.6923], "fy":[-0.00729,-0.00729,-0.00729,-0.00729]}, + {"t":3.139, "x":7.7952, "y":5.97578, "heading":-1.5708, "vx":0.03325, "vy":-0.99924, "omega":0.0, "ax":-1.36749, "ay":-0.00042, "alpha":0.0, "fx":[-23.26058,-23.26058,-23.26058,-23.26058], "fy":[-0.00707,-0.00707,-0.00707,-0.00707]}, + {"t":3.18718, "x":7.79521, "y":5.92763, "heading":-1.5708, "vx":-0.03264, "vy":-0.99926, "omega":0.0, "ax":1.34244, "ay":-0.0004, "alpha":0.0, "fx":[22.83447,22.83447,22.83447,22.83447], "fy":[-0.00685,-0.00685,-0.00685,-0.00685]}, + {"t":3.23537, "x":7.7952, "y":5.87948, "heading":-1.5708, "vx":0.03204, "vy":-0.99928, "omega":0.0, "ax":-1.31773, "ay":-0.00039, "alpha":0.0, "fx":[-22.41425,-22.41425,-22.41425,-22.41425], "fy":[-0.00662,-0.00662,-0.00662,-0.00662]}, + {"t":3.28355, "x":7.79521, "y":5.83134, "heading":-1.5708, "vx":-0.03145, "vy":-0.9993, "omega":0.0, "ax":1.29339, "ay":-0.00038, "alpha":0.0, "fx":[22.00025,22.00025,22.00025,22.00025], "fy":[-0.0064,-0.0064,-0.0064,-0.0064]}, + {"t":3.33173, "x":7.7952, "y":5.78319, "heading":-1.5708, "vx":0.03087, "vy":-0.99932, "omega":0.0, "ax":-1.26944, "ay":-0.00036, "alpha":0.0, "fx":[-21.59279,-21.59279,-21.59279,-21.59279], "fy":[-0.00618,-0.00618,-0.00618,-0.00618]}, + {"t":3.37991, "x":7.79521, "y":5.73504, "heading":-1.5708, "vx":-0.0303, "vy":-0.99933, "omega":0.0, "ax":1.24589, "ay":-0.00035, "alpha":0.0, "fx":[21.19224,21.19224,21.19224,21.19224], "fy":[-0.00596,-0.00596,-0.00596,-0.00596]}, + {"t":3.42809, "x":7.7952, "y":5.68689, "heading":-1.5708, "vx":0.02973, "vy":-0.99935, "omega":0.0, "ax":-1.22277, "ay":-0.00034, "alpha":0.0, "fx":[-20.799,-20.799,-20.799,-20.799], "fy":[-0.00574,-0.00574,-0.00574,-0.00574]}, + {"t":3.47628, "x":7.79521, "y":5.63873, "heading":-1.5708, "vx":-0.02918, "vy":-0.99937, "omega":0.0, "ax":1.20011, "ay":-0.00032, "alpha":0.0, "fx":[20.41346,20.41346,20.41346,20.41346], "fy":[-0.00552,-0.00552,-0.00552,-0.00552]}, + {"t":3.52446, "x":7.7952, "y":5.59058, "heading":-1.5708, "vx":0.02864, "vy":-0.99938, "omega":0.0, "ax":-1.17792, "ay":-0.00031, "alpha":0.0, "fx":[-20.03609,-20.03609,-20.03609,-20.03609], "fy":[-0.0053,-0.0053,-0.0053,-0.0053]}, + {"t":3.57264, "x":7.79521, "y":5.54243, "heading":-1.5708, "vx":-0.02811, "vy":-0.9994, "omega":0.0, "ax":1.15624, "ay":-0.0003, "alpha":0.0, "fx":[19.66737,19.66737,19.66737,19.66737], "fy":[-0.00507,-0.00507,-0.00507,-0.00507]}, + {"t":3.62082, "x":7.7952, "y":5.49427, "heading":-1.5708, "vx":0.0276, "vy":-0.99941, "omega":0.0, "ax":-1.1351, "ay":-0.00029, "alpha":0.0, "fx":[-19.30781,-19.30781,-19.30781,-19.30781], "fy":[-0.00485,-0.00485,-0.00485,-0.00485]}, + {"t":3.66901, "x":7.79521, "y":5.44612, "heading":-1.5708, "vx":-0.02709, "vy":-0.99943, "omega":0.0, "ax":1.11454, "ay":-0.00027, "alpha":0.0, "fx":[18.95797,18.95797,18.95797,18.95797], "fy":[-0.00463,-0.00463,-0.00463,-0.00463]}, + {"t":3.71719, "x":7.7952, "y":5.39796, "heading":-1.5708, "vx":0.02661, "vy":-0.99944, "omega":0.0, "ax":-1.09458, "ay":-0.00026, "alpha":0.0, "fx":[-18.61845,-18.61845,-18.61845,-18.61845], "fy":[-0.00441,-0.00441,-0.00441,-0.00441]}, + {"t":3.76537, "x":7.79521, "y":5.34981, "heading":-1.5708, "vx":-0.02613, "vy":-0.99945, "omega":0.0, "ax":1.07526, "ay":-0.00025, "alpha":0.0, "fx":[18.28991,18.28991,18.28991,18.28991], "fy":[-0.00418,-0.00418,-0.00418,-0.00418]}, + {"t":3.81355, "x":7.7952, "y":5.30165, "heading":-1.5708, "vx":0.02568, "vy":-0.99946, "omega":0.0, "ax":-1.05663, "ay":-0.00023, "alpha":0.0, "fx":[-17.97304,-17.97304,-17.97304,-17.97304], "fy":[-0.00396,-0.00396,-0.00396,-0.00396]}, + {"t":3.86174, "x":7.79521, "y":5.2535, "heading":-1.5708, "vx":-0.02524, "vy":-0.99947, "omega":0.0, "ax":1.03874, "ay":-0.00022, "alpha":0.0, "fx":[17.66859,17.66859,17.66859,17.66859], "fy":[-0.00373,-0.00373,-0.00373,-0.00373]}, + {"t":3.90992, "x":7.7952, "y":5.20534, "heading":-1.5708, "vx":0.02481, "vy":-0.99948, "omega":0.0, "ax":-1.02161, "ay":-0.00021, "alpha":0.0, "fx":[-17.37737,-17.37737,-17.37737,-17.37737], "fy":[-0.0035,-0.0035,-0.0035,-0.0035]}, + {"t":3.9581, "x":7.79521, "y":5.15718, "heading":-1.5708, "vx":-0.02441, "vy":-0.99949, "omega":0.0, "ax":1.00532, "ay":-0.00019, "alpha":0.0, "fx":[17.10025,17.10025,17.10025,17.10025], "fy":[-0.00327,-0.00327,-0.00327,-0.00327]}, + {"t":4.00628, "x":7.7952, "y":5.10902, "heading":-1.5708, "vx":0.02403, "vy":-0.9995, "omega":0.0, "ax":-0.98991, "ay":-0.00018, "alpha":0.0, "fx":[-16.83814,-16.83814,-16.83814,-16.83814], "fy":[-0.00303,-0.00303,-0.00303,-0.00303]}, + {"t":4.05447, "x":7.79521, "y":5.06086, "heading":-1.5708, "vx":-0.02367, "vy":-0.99951, "omega":0.0, "ax":0.97544, "ay":-0.00016, "alpha":0.0, "fx":[16.59203,16.59203,16.59203,16.59203], "fy":[-0.0028,-0.0028,-0.0028,-0.0028]}, + {"t":4.10265, "x":7.7952, "y":5.0127, "heading":-1.5708, "vx":0.02333, "vy":-0.99952, "omega":0.0, "ax":-0.96198, "ay":-0.00015, "alpha":0.0, "fx":[-16.36294,-16.36294,-16.36294,-16.36294], "fy":[-0.00255,-0.00255,-0.00255,-0.00255]}, + {"t":4.15083, "x":7.79521, "y":4.96454, "heading":-1.5708, "vx":-0.02302, "vy":-0.99953, "omega":0.0, "ax":0.94957, "ay":-0.00014, "alpha":0.0, "fx":[16.15197,16.15197,16.15197,16.15197], "fy":[-0.00231,-0.00231,-0.00231,-0.00231]}, + {"t":4.19901, "x":7.7952, "y":4.91639, "heading":-1.5708, "vx":0.02273, "vy":-0.99953, "omega":0.0, "ax":-0.9383, "ay":-0.00012, "alpha":0.0, "fx":[-15.96021,-15.96021,-15.96021,-15.96021], "fy":[-0.00206,-0.00206,-0.00206,-0.00206]}, + {"t":4.2472, "x":7.79521, "y":4.86822, "heading":-1.5708, "vx":-0.02248, "vy":-0.99954, "omega":0.0, "ax":0.92822, "ay":-0.00011, "alpha":0.0, "fx":[15.78881,15.78881,15.78881,15.78881], "fy":[-0.0018,-0.0018,-0.0018,-0.0018]}, + {"t":4.29538, "x":7.7952, "y":4.82006, "heading":-1.5708, "vx":0.02225, "vy":-0.99955, "omega":0.0, "ax":-0.91941, "ay":-0.00009, "alpha":0.0, "fx":[-15.63889,-15.63889,-15.63889,-15.63889], "fy":[-0.00154,-0.00154,-0.00154,-0.00154]}, + {"t":4.34356, "x":7.79521, "y":4.7719, "heading":-1.5708, "vx":-0.02205, "vy":-0.99955, "omega":0.0, "ax":0.91192, "ay":-0.00007, "alpha":0.0, "fx":[15.51153,15.51153,15.51153,15.51153], "fy":[-0.00127,-0.00127,-0.00127,-0.00127]}, + {"t":4.39174, "x":7.79521, "y":4.72374, "heading":-1.5708, "vx":0.02189, "vy":-0.99955, "omega":0.0, "ax":-0.90582, "ay":-0.00006, "alpha":0.0, "fx":[-15.40774,-15.40774,-15.40774,-15.40774], "fy":[-0.001,-0.001,-0.001,-0.001]}, + {"t":4.43993, "x":7.79521, "y":4.67558, "heading":-1.5708, "vx":-0.02176, "vy":-0.99956, "omega":0.0, "ax":0.90116, "ay":-0.00004, "alpha":0.0, "fx":[15.3284,15.3284,15.3284,15.3284], "fy":[-0.00073,-0.00073,-0.00073,-0.00073]}, + {"t":4.48811, "x":7.79521, "y":4.62742, "heading":-1.5708, "vx":0.02166, "vy":-0.99956, "omega":0.0, "ax":-0.89797, "ay":-0.00003, "alpha":0.0, "fx":[-15.27424,-15.27424,-15.27424,-15.27424], "fy":[-0.00045,-0.00045,-0.00045,-0.00045]}, + {"t":4.53629, "x":7.79521, "y":4.57926, "heading":-1.5708, "vx":-0.0216, "vy":-0.99956, "omega":0.0, "ax":0.89633, "ay":0.00439, "alpha":0.0, "fx":[15.2464,15.2464,15.2464,15.2464], "fy":[0.07462,0.07462,0.07462,0.07462]}, + {"t":4.58447, "x":7.79521, "y":4.5311, "heading":-1.5708, "vx":0.02158, "vy":-0.99935, "omega":0.0, "ax":-1.30151, "ay":2.51868, "alpha":20.35533, "fx":[25.14587,-64.03427,-99.35853,49.69392], "fy":[122.27364,111.83408,-26.19061,-36.54879]}, + {"t":4.60903, "x":7.79534, "y":4.50733, "heading":-1.5708, "vx":-0.01037, "vy":-0.93751, "omega":0.4998, "ax":-2.03873, "ay":4.70262, "alpha":0.00002, "fx":[-34.67812,-34.67823,-34.67823,-34.67813], "fy":[79.99028,79.99027,79.99014,79.99015]}, + {"t":4.63358, "x":7.79448, "y":4.48572, "heading":-1.55852, "vx":-0.06043, "vy":-0.82204, "omega":0.4998, "ax":-1.95127, "ay":4.49795, "alpha":0.0, "fx":[-33.19054,-33.19054,-33.19054,-33.19054], "fy":[76.50886,76.50886,76.50886,76.50886]}, + {"t":4.65814, "x":7.7924, "y":4.4669, "heading":-1.54625, "vx":-0.10834, "vy":-0.7116, "omega":0.4998, "ax":-1.86168, "ay":4.2944, "alpha":0.0, "fx":[-31.66671,-31.66671,-31.66671,-31.66671], "fy":[73.0466,73.0466,73.0466,73.0466]}, + {"t":4.68269, "x":7.78918, "y":4.45072, "heading":-1.53398, "vx":-0.15405, "vy":-0.60615, "omega":0.4998, "ax":-1.77052, "ay":4.09198, "alpha":0.0, "fx":[-30.11611,-30.11611,-30.11611,-30.11611], "fy":[69.60336,69.60336,69.60336,69.60336]}, + {"t":4.70724, "x":7.78487, "y":4.43707, "heading":-1.52171, "vx":-0.19753, "vy":-0.50568, "omega":0.4998, "ax":-1.67836, "ay":3.89092, "alpha":0.0, "fx":[-28.54844,-28.54844,-28.54844,-28.54844], "fy":[66.18352,66.18352,66.18352,66.18352]}, + {"t":4.7318, "x":7.77951, "y":4.42582, "heading":-1.50944, "vx":-0.23874, "vy":-0.41015, "omega":0.4998, "ax":-1.58577, "ay":3.69171, "alpha":0.0, "fx":[-26.97346,-26.97346,-26.97346,-26.97346], "fy":[62.79485,62.79485,62.79485,62.79485]}, + {"t":4.75635, "x":7.77317, "y":4.41687, "heading":-1.49717, "vx":-0.27767, "vy":-0.3195, "omega":0.4998, "ax":-1.49333, "ay":3.49491, "alpha":0.0, "fx":[-25.40112,-25.40112,-25.40112,-25.40112], "fy":[59.44742,59.44742,59.44742,59.44742]}, + {"t":4.7809, "x":7.7659, "y":4.41008, "heading":-1.48489, "vx":-0.31434, "vy":-0.23369, "omega":0.4998, "ax":-1.40162, "ay":3.30123, "alpha":0.0, "fx":[-23.84123,-23.84123,-23.84123,-23.84123], "fy":[56.15296,56.15296,56.15296,56.15296]}, + {"t":4.80546, "x":7.75776, "y":4.40533, "heading":-1.47262, "vx":-0.34875, "vy":-0.15263, "omega":0.4998, "ax":-1.31122, "ay":3.1114, "alpha":0.0, "fx":[-22.30342,-22.30342,-22.30342,-22.30342], "fy":[52.92404,52.92404,52.92404,52.92404]}, + {"t":4.83001, "x":7.7488, "y":4.40252, "heading":-1.46035, "vx":-0.38095, "vy":-0.07623, "omega":0.4998, "ax":-1.22264, "ay":2.92618, "alpha":0.0, "fx":[-20.79681,-20.79681,-20.79681,-20.79682], "fy":[49.7735,49.7735,49.7735,49.7735]}, + {"t":4.85456, "x":7.73908, "y":4.40153, "heading":-1.44808, "vx":-0.41097, "vy":-0.00439, "omega":0.4998, "ax":-1.1364, "ay":2.74631, "alpha":0.0, "fx":[-19.32987,-19.32987,-19.32988,-19.32988], "fy":[46.71391,46.71391,46.71391,46.71391]}, + {"t":4.87912, "x":7.72865, "y":4.40225, "heading":-1.43581, "vx":-0.43887, "vy":0.06305, "omega":0.4998, "ax":-1.05294, "ay":2.57247, "alpha":0.0, "fx":[-17.91021,-17.91021,-17.91021,-17.91021], "fy":[43.75701,43.75702,43.75701,43.75701]}, + {"t":4.90367, "x":7.71755, "y":4.40458, "heading":-1.42353, "vx":-0.46473, "vy":0.12621, "omega":0.4998, "ax":-0.97265, "ay":2.40529, "alpha":0.0, "fx":[-16.54443,-16.54443,-16.54443,-16.54443], "fy":[40.91324,40.91324,40.91324,40.91324]}, + {"t":4.92823, "x":7.70585, "y":4.4084, "heading":-1.41126, "vx":-0.48861, "vy":0.18527, "omega":0.4998, "ax":-0.89584, "ay":2.24528, "alpha":0.0, "fx":[-15.23801,-15.23801,-15.23802,-15.23802], "fy":[38.19158,38.19158,38.19158,38.19158]}, + {"t":4.95278, "x":7.69358, "y":4.41363, "heading":-1.39899, "vx":-0.5106, "vy":0.2404, "omega":0.4998, "ax":-0.82279, "ay":2.09288, "alpha":0.0, "fx":[-13.99537,-13.99537,-13.99538,-13.99538], "fy":[35.59924,35.59924,35.59924,35.59924]}, + {"t":4.97733, "x":7.6808, "y":4.42016, "heading":-1.38672, "vx":-0.53081, "vy":0.29179, "omega":0.4998, "ax":-0.75366, "ay":1.9484, "alpha":0.0, "fx":[-12.81955,-12.81955,-12.81955,-12.81955], "fy":[33.14171,33.14171,33.14171,33.14171]}, + {"t":5.00189, "x":7.66754, "y":4.42791, "heading":-1.37445, "vx":-0.54931, "vy":0.33963, "omega":0.4998, "ax":-0.68858, "ay":1.81204, "alpha":0.0, "fx":[-11.71254,-11.71254,-11.71254,-11.71254], "fy":[30.82231,30.82231,30.82231,30.82231]}, + {"t":5.02644, "x":7.65384, "y":4.4368, "heading":-1.36217, "vx":-0.56622, "vy":0.38412, "omega":0.4998, "ax":-0.6276, "ay":1.68391, "alpha":0.0, "fx":[-10.67525,-10.67525,-10.67526,-10.67526], "fy":[28.64287,28.64287,28.64287,28.64287]}, + {"t":5.05099, "x":7.63975, "y":4.44674, "heading":-1.3499, "vx":-0.58163, "vy":0.42546, "omega":0.4998, "ax":-0.57071, "ay":1.56403, "alpha":0.0, "fx":[-9.70761,-9.70761,-9.70761,-9.70761], "fy":[26.60362,26.60362,26.60362,26.60362]}, + {"t":5.07555, "x":7.6253, "y":4.45765, "heading":-1.33763, "vx":-0.59564, "vy":0.46387, "omega":0.4998, "ax":-0.51786, "ay":1.45227, "alpha":0.0, "fx":[-8.80871,-8.80871,-8.80871,-8.80871], "fy":[24.70272,24.70272,24.70272,24.70272]}, + {"t":5.1001, "x":7.61052, "y":4.46948, "heading":-1.32536, "vx":-0.60836, "vy":0.49953, "omega":0.4998, "ax":-0.46896, "ay":1.3485, "alpha":0.0, "fx":[-7.97691,-7.97691,-7.97691,-7.97691], "fy":[22.93759,22.93759,22.93759,22.93759]}, + {"t":5.12465, "x":7.59544, "y":4.48215, "heading":-1.31309, "vx":-0.61987, "vy":0.53264, "omega":0.4998, "ax":-0.42387, "ay":1.25248, "alpha":0.0, "fx":[-7.20997,-7.20997,-7.20997,-7.20997], "fy":[21.30439,21.30439,21.30439,21.30439]}, + {"t":5.14921, "x":7.58009, "y":4.49561, "heading":-1.30082, "vx":-0.63028, "vy":0.56339, "omega":0.4998, "ax":-0.38243, "ay":1.16395, "alpha":0.0, "fx":[-6.50511,-6.50511,-6.50511,-6.50511], "fy":[19.79838,19.79838,19.79838,19.79838]}, + {"t":5.17376, "x":7.5645, "y":4.50979, "heading":-1.28854, "vx":-0.63967, "vy":0.59197, "omega":0.4998, "ax":-0.34447, "ay":1.08257, "alpha":0.0, "fx":[-5.85935,-5.85935,-5.85936,-5.85936], "fy":[18.41422,18.41422,18.41422,18.41422]}, + {"t":5.19831, "x":7.54869, "y":4.52465, "heading":-1.27627, "vx":-0.64813, "vy":0.61855, "omega":0.4998, "ax":-0.30979, "ay":1.00801, "alpha":0.0, "fx":[-5.26943,-5.26943,-5.26944,-5.26944], "fy":[17.14597,17.14597,17.14597,17.14597]}, + {"t":5.22287, "x":7.53268, "y":4.54015, "heading":-1.264, "vx":-0.65573, "vy":0.6433, "omega":0.4998, "ax":-0.27819, "ay":0.93991, "alpha":0.0, "fx":[-4.73185,-4.73185,-4.73185,-4.73185], "fy":[15.98754,15.98754,15.98754,15.98754]}, + {"t":5.24742, "x":7.5165, "y":4.55622, "heading":-1.25173, "vx":-0.66256, "vy":0.66638, "omega":0.4998, "ax":-0.24945, "ay":0.87788, "alpha":0.0, "fx":[-4.24307,-4.24308,-4.24308,-4.24308], "fy":[14.93244,14.93244,14.93244,14.93244]}, + {"t":5.27198, "x":7.50015, "y":4.57285, "heading":-1.23946, "vx":-0.66869, "vy":0.68793, "omega":0.4998, "ax":-0.22338, "ay":0.82154, "alpha":0.0, "fx":[-3.79957,-3.79957,-3.79957,-3.79957], "fy":[13.97419,13.97419,13.97419,13.97419]}, + {"t":5.29653, "x":7.48367, "y":4.58999, "heading":-1.22718, "vx":-0.67417, "vy":0.7081, "omega":0.4998, "ax":-0.19976, "ay":0.77052, "alpha":0.0, "fx":[-3.39782,-3.39782,-3.39782,-3.39782], "fy":[13.10641,13.10641,13.10641,13.10641]}, + {"t":5.32108, "x":7.46705, "y":4.60761, "heading":-1.21491, "vx":-0.67908, "vy":0.72702, "omega":0.4998, "ax":-0.17839, "ay":0.72444, "alpha":0.0, "fx":[-3.03442,-3.03442,-3.03442,-3.03442], "fy":[12.32255,12.32255,12.32255,12.32255]}, + {"t":5.34564, "x":7.45033, "y":4.62568, "heading":-1.20264, "vx":-0.68346, "vy":0.74481, "omega":0.4998, "ax":-0.1591, "ay":0.68295, "alpha":0.0, "fx":[-2.70619,-2.70619,-2.70619,-2.70619], "fy":[11.61671,11.61671,11.61671,11.61671]}, + {"t":5.37019, "x":7.4335, "y":4.64417, "heading":-1.19037, "vx":-0.68736, "vy":0.76158, "omega":0.4998, "ax":-0.14168, "ay":0.64569, "alpha":0.0, "fx":[-2.41,-2.41001,-2.41001,-2.41001], "fy":[10.98302,10.98302,10.98302,10.98302]}, + {"t":5.39474, "x":7.41658, "y":4.66307, "heading":-1.1781, "vx":-0.69084, "vy":0.77743, "omega":0.4998, "ax":-0.12598, "ay":0.61234, "alpha":0.0, "fx":[-2.14293,-2.14293,-2.14293,-2.14293], "fy":[10.41581,10.41581,10.41581,10.4158]}, + {"t":5.4193, "x":7.39958, "y":4.68234, "heading":-1.16583, "vx":-0.69394, "vy":0.79247, "omega":0.4998, "ax":-0.11183, "ay":0.58259, "alpha":0.0, "fx":[-1.90218,-1.90218,-1.90218,-1.90218], "fy":[9.90973,9.90973,9.90973,9.90973]}, + {"t":5.44385, "x":7.3825, "y":4.70197, "heading":-1.15355, "vx":-0.69668, "vy":0.80677, "omega":0.4998, "ax":-0.09907, "ay":0.55614, "alpha":0.0, "fx":[-1.68515,-1.68515,-1.68516,-1.68515], "fy":[9.45975,9.45976,9.45975,9.45975]}, + {"t":5.4684, "x":7.36537, "y":4.72195, "heading":-1.14128, "vx":-0.69912, "vy":0.82043, "omega":0.4998, "ax":-0.08757, "ay":0.53269, "alpha":0.0, "fx":[-1.48954,-1.48954,-1.48954,-1.48954], "fy":[9.06084,9.06084,9.06084,9.06084]}, + {"t":5.49296, "x":7.34818, "y":4.74225, "heading":-1.12901, "vx":-0.70127, "vy":0.83351, "omega":0.4998, "ax":-0.07719, "ay":0.51199, "alpha":0.0, "fx":[-1.31301,-1.31302,-1.31302,-1.31302], "fy":[8.70883,8.70883,8.70883,8.70883]}, + {"t":5.51751, "x":7.33093, "y":4.76287, "heading":-1.11674, "vx":-0.70316, "vy":0.84608, "omega":0.4998, "ax":-0.06783, "ay":0.49381, "alpha":0.0, "fx":[-1.15376,-1.15376,-1.15376,-1.15376], "fy":[8.39956,8.39956,8.39956,8.39956]}, + {"t":5.54207, "x":7.31365, "y":4.7838, "heading":-1.10447, "vx":-0.70483, "vy":0.8582, "omega":0.4998, "ax":-0.05936, "ay":0.47791, "alpha":0.0, "fx":[-1.00969,-1.00969,-1.00969,-1.00969], "fy":[8.12917,8.12918,8.12918,8.12917]}, + {"t":5.56662, "x":7.29632, "y":4.80501, "heading":-1.09219, "vx":-0.70628, "vy":0.86994, "omega":0.4998, "ax":-0.05168, "ay":0.46409, "alpha":0.0, "fx":[-0.87912,-0.87912,-0.87912,-0.87912], "fy":[7.89407,7.89407,7.89407,7.89407]}, + {"t":5.59117, "x":7.27897, "y":4.82651, "heading":-1.07992, "vx":-0.70755, "vy":0.88133, "omega":0.4998, "ax":-0.04471, "ay":0.45213, "alpha":0.0, "fx":[-0.76048,-0.76048,-0.76048,-0.76048], "fy":[7.69054,7.69054,7.69054,7.69054]}, + {"t":5.61573, "x":7.26158, "y":4.84829, "heading":-1.06765, "vx":-0.70865, "vy":0.89244, "omega":0.4998, "ax":-0.03835, "ay":0.44186, "alpha":0.0, "fx":[-0.65238,-0.65238,-0.65238,-0.65238], "fy":[7.51595,7.51595,7.51595,7.51595]}, + {"t":5.64028, "x":7.24417, "y":4.87034, "heading":-1.05538, "vx":-0.70959, "vy":0.90328, "omega":0.4998, "ax":-0.03254, "ay":0.43313, "alpha":0.0, "fx":[-0.55352,-0.55352,-0.55352,-0.55352], "fy":[7.36746,7.36746,7.36746,7.36746]}, + {"t":5.66483, "x":7.22674, "y":4.89265, "heading":-1.04311, "vx":-0.71039, "vy":0.91392, "omega":0.4998, "ax":-0.0272, "ay":0.42579, "alpha":0.0, "fx":[-0.46272,-0.46272,-0.46273,-0.46272], "fy":[7.24249,7.24249,7.24249,7.24249]}, + {"t":5.68939, "x":7.20928, "y":4.91521, "heading":-1.03084, "vx":-0.71106, "vy":0.92437, "omega":0.4998, "ax":-0.02228, "ay":0.41968, "alpha":0.0, "fx":[-0.37893,-0.37893,-0.37894,-0.37894], "fy":[7.13866,7.13866,7.13866,7.13866]}, + {"t":5.71394, "x":7.19182, "y":4.93804, "heading":-1.01856, "vx":-0.71161, "vy":0.93468, "omega":0.4998, "ax":-0.01771, "ay":0.41469, "alpha":0.0, "fx":[-0.3012,-0.3012,-0.3012,-0.3012], "fy":[7.05378,7.05378,7.05378,7.05378]}, + {"t":5.73849, "x":7.17434, "y":4.96111, "heading":-1.00629, "vx":-0.71204, "vy":0.94486, "omega":0.4998, "ax":-0.01344, "ay":0.4107, "alpha":0.0, "fx":[-0.22869,-0.22869,-0.22869,-0.22869], "fy":[6.98584,6.98584,6.98584,6.98584]}, + {"t":5.76305, "x":7.15685, "y":4.98444, "heading":-0.99402, "vx":-0.71237, "vy":0.95495, "omega":0.4998, "ax":-0.00944, "ay":0.40758, "alpha":0.0, "fx":[-0.16062,-0.16062,-0.16062,-0.16062], "fy":[6.9328,6.9328,6.9328,6.9328]}, + {"t":5.7876, "x":7.13936, "y":5.00801, "heading":-0.98175, "vx":-0.7126, "vy":0.96495, "omega":0.4998, "ax":-0.00566, "ay":0.40525, "alpha":0.0, "fx":[-0.09631,-0.09631,-0.09631,-0.09631], "fy":[6.8932,6.8932,6.8932,6.8932]}, + {"t":5.81216, "x":7.12186, "y":5.03182, "heading":-0.96948, "vx":-0.71274, "vy":0.9749, "omega":0.4998, "ax":-0.00207, "ay":0.40362, "alpha":0.0, "fx":[-0.03517,-0.03517,-0.03517,-0.03517], "fy":[6.8655,6.8655,6.8655,6.8655]}, + {"t":5.83671, "x":7.10436, "y":5.05588, "heading":-0.9572, "vx":-0.71279, "vy":0.98481, "omega":0.4998, "ax":0.00137, "ay":0.40261, "alpha":0.0, "fx":[0.02333,0.02333,0.02333,0.02333], "fy":[6.8483,6.8483,6.8483,6.8483]}, + {"t":5.86126, "x":7.08686, "y":5.08018, "heading":-0.94493, "vx":-0.71276, "vy":0.9947, "omega":0.4998, "ax":0.00468, "ay":0.40214, "alpha":0.0, "fx":[0.07968,0.07968,0.07968,0.07968], "fy":[6.84032,6.84032,6.84032,6.84032]}, + {"t":5.88582, "x":7.06936, "y":5.10473, "heading":-0.93266, "vx":-0.71264, "vy":1.00457, "omega":0.4998, "ax":0.0079, "ay":0.40215, "alpha":0.0, "fx":[0.13432,0.13431,0.13431,0.13431], "fy":[6.84038,6.84038,6.84038,6.84038]}, + {"t":5.91037, "x":7.05186, "y":5.12951, "heading":-0.92039, "vx":-0.71245, "vy":1.01445, "omega":0.4998, "ax":0.01101, "ay":0.40257, "alpha":0.0, "fx":[0.18736,0.18736,0.18736,0.18736], "fy":[6.84758,6.84758,6.84758,6.84758]}, + {"t":5.93492, "x":7.03437, "y":5.15454, "heading":-0.90812, "vx":-0.71218, "vy":1.02433, "omega":0.4998, "ax":0.01407, "ay":0.40333, "alpha":0.0, "fx":[0.23936,0.23936,0.23936,0.23936], "fy":[6.86061,6.86061,6.86061,6.86061]}, + {"t":5.95948, "x":7.01689, "y":5.17982, "heading":-0.89584, "vx":-0.71183, "vy":1.03424, "omega":0.4998, "ax":0.01708, "ay":0.4044, "alpha":0.0, "fx":[0.29059,0.29059,0.29059,0.29059], "fy":[6.87873,6.87873,6.87873,6.87873]}, + {"t":5.98403, "x":6.99942, "y":5.20533, "heading":-0.88357, "vx":-0.71141, "vy":1.04416, "omega":0.4998, "ax":0.02006, "ay":0.40573, "alpha":0.0, "fx":[0.34128,0.34128,0.34128,0.34128], "fy":[6.90138,6.90138,6.90138,6.90138]}, + {"t":6.00858, "x":6.98196, "y":5.23109, "heading":-0.8713, "vx":-0.71092, "vy":1.05413, "omega":0.4998, "ax":0.02302, "ay":0.40729, "alpha":0.0, "fx":[0.39164,0.39164,0.39164,0.39164], "fy":[6.92781,6.92781,6.92781,6.92781]}, + {"t":6.03314, "x":6.96451, "y":5.2571, "heading":-0.85903, "vx":-0.71036, "vy":1.06413, "omega":0.4998, "ax":0.02598, "ay":0.409, "alpha":0.0, "fx":[0.44185,0.44185,0.44185,0.44185], "fy":[6.95703,6.95703,6.95703,6.95703]}, + {"t":6.05769, "x":6.94707, "y":5.28335, "heading":-0.84676, "vx":-0.70972, "vy":1.07417, "omega":0.4998, "ax":0.02893, "ay":0.41085, "alpha":0.0, "fx":[0.49205,0.49205,0.49205,0.49205], "fy":[6.98849,6.98849,6.98849,6.98849]}, + {"t":6.08225, "x":6.92966, "y":5.30985, "heading":-0.83449, "vx":-0.70901, "vy":1.08426, "omega":0.4998, "ax":0.03188, "ay":0.41281, "alpha":0.0, "fx":[0.54234,0.54234,0.54234,0.54234], "fy":[7.02183,7.02183,7.02183,7.02183]}, + {"t":6.1068, "x":6.91226, "y":5.3366, "heading":-0.82221, "vx":-0.70823, "vy":1.09439, "omega":0.4998, "ax":0.03485, "ay":0.41485, "alpha":0.0, "fx":[0.59282,0.59282,0.59282,0.59282], "fy":[7.0565,7.0565,7.0565,7.0565]}, + {"t":6.13135, "x":6.89488, "y":5.36359, "heading":-0.80994, "vx":-0.70737, "vy":1.10458, "omega":0.4998, "ax":0.03783, "ay":0.41695, "alpha":0.0, "fx":[0.64355,0.64355,0.64355,0.64355], "fy":[7.09224,7.09224,7.09224,7.09224]}, + {"t":6.15591, "x":6.87752, "y":5.39084, "heading":-0.79767, "vx":-0.70644, "vy":1.11482, "omega":0.4998, "ax":0.04083, "ay":0.41906, "alpha":0.0, "fx":[0.69455,0.69455,0.69455,0.69455], "fy":[7.12814,7.12814,7.12814,7.12814]}, + {"t":6.18046, "x":6.86019, "y":5.41834, "heading":-0.7854, "vx":-0.70544, "vy":1.12511, "omega":0.4998, "ax":0.04385, "ay":0.42119, "alpha":0.0, "fx":[0.74583,0.74583,0.74583,0.74583], "fy":[7.16424,7.16424,7.16424,7.16424]}, + {"t":6.20501, "x":6.84288, "y":5.44609, "heading":-0.77313, "vx":-0.70436, "vy":1.13545, "omega":0.4998, "ax":0.04688, "ay":0.42328, "alpha":0.0, "fx":[0.7974,0.7974,0.7974,0.7974], "fy":[7.19995,7.19995,7.19995,7.19995]}, + {"t":6.22957, "x":6.8256, "y":5.4741, "heading":-0.76085, "vx":-0.70321, "vy":1.14584, "omega":0.4998, "ax":0.04993, "ay":0.42535, "alpha":0.0, "fx":[0.84923,0.84923,0.84923,0.84923], "fy":[7.2351,7.2351,7.2351,7.2351]}, + {"t":6.25412, "x":6.80835, "y":5.50236, "heading":-0.74858, "vx":-0.70199, "vy":1.15629, "omega":0.4998, "ax":0.05297, "ay":0.42735, "alpha":0.0, "fx":[0.90101,0.90101,0.90101,0.90101], "fy":[7.26913,7.26913,7.26913,7.26913]}, + {"t":6.27867, "x":6.79113, "y":5.53088, "heading":-0.73631, "vx":-0.70068, "vy":1.16678, "omega":0.4998, "ax":0.05602, "ay":0.42928, "alpha":0.0, "fx":[0.95289,0.95289,0.95289,0.95289], "fy":[7.30197,7.30197,7.30197,7.30197]}, + {"t":6.30323, "x":6.77394, "y":5.55966, "heading":-0.72404, "vx":-0.69931, "vy":1.17732, "omega":0.4998, "ax":0.05907, "ay":0.43114, "alpha":0.0, "fx":[1.00479,1.00479,1.00479,1.00479], "fy":[7.33355,7.33355,7.33355,7.33355]}, + {"t":6.32778, "x":6.75679, "y":5.5887, "heading":-0.71177, "vx":-0.69786, "vy":1.18791, "omega":0.4998, "ax":0.06211, "ay":0.43288, "alpha":0.0, "fx":[1.05655,1.05655,1.05655,1.05655], "fy":[7.36309,7.36309,7.36309,7.36309]}, + {"t":6.35234, "x":6.73967, "y":5.61799, "heading":-0.6995, "vx":-0.69633, "vy":1.19853, "omega":0.4998, "ax":0.06514, "ay":0.43451, "alpha":0.0, "fx":[1.10803,1.10803,1.10803,1.10803], "fy":[7.39081,7.39081,7.39081,7.39081]}, + {"t":6.37689, "x":6.72259, "y":5.64755, "heading":-0.68722, "vx":-0.69473, "vy":1.2092, "omega":0.4998, "ax":0.06814, "ay":0.43601, "alpha":0.0, "fx":[1.15904,1.15904,1.15904,1.15904], "fy":[7.41643,7.41643,7.41643,7.41643]}, + {"t":6.40144, "x":6.70556, "y":5.67737, "heading":-0.67495, "vx":-0.69306, "vy":1.21991, "omega":0.4998, "ax":0.0711, "ay":0.43737, "alpha":0.0, "fx":[1.20936,1.20936,1.20936,1.20936], "fy":[7.43946,7.43946,7.43946,7.43946]}, + {"t":6.426, "x":6.68856, "y":5.70746, "heading":-0.66268, "vx":-0.69132, "vy":1.23065, "omega":0.4998, "ax":0.074, "ay":0.43858, "alpha":0.0, "fx":[1.25872,1.25872,1.25872,1.25872], "fy":[7.46009,7.46009,7.46009,7.46009]}, + {"t":6.45055, "x":6.67161, "y":5.73781, "heading":-0.65041, "vx":-0.6895, "vy":1.24142, "omega":0.4998, "ax":0.07683, "ay":0.43962, "alpha":0.0, "fx":[1.30689,1.30689,1.30689,1.30689], "fy":[7.47781,7.47781,7.47781,7.47781]}, + {"t":6.4751, "x":6.6547, "y":5.76842, "heading":-0.63814, "vx":-0.68761, "vy":1.25221, "omega":0.4998, "ax":0.07956, "ay":0.44048, "alpha":0.0, "fx":[1.35337,1.35337,1.35337,1.35337], "fy":[7.49246,7.49246,7.49246,7.49246]}, + {"t":6.49966, "x":6.63784, "y":5.7993, "heading":-0.62586, "vx":-0.68566, "vy":1.26303, "omega":0.4998, "ax":0.08218, "ay":0.44117, "alpha":0.0, "fx":[1.39789,1.39789,1.39789,1.39789], "fy":[7.50414,7.50414,7.50414,7.50414]}, + {"t":6.52421, "x":6.62103, "y":5.83045, "heading":-0.61359, "vx":-0.68364, "vy":1.27386, "omega":0.4998, "ax":0.08466, "ay":0.44164, "alpha":0.0, "fx":[1.44005,1.44005,1.44005,1.44005], "fy":[7.51217,7.51217,7.51217,7.51217]}, + {"t":6.54876, "x":6.60427, "y":5.86186, "heading":-0.60132, "vx":-0.68156, "vy":1.2847, "omega":0.4998, "ax":0.08697, "ay":0.44191, "alpha":0.0, "fx":[1.4794,1.4794,1.4794,1.4794], "fy":[7.51668,7.51668,7.51668,7.51668]}, + {"t":6.57332, "x":6.58756, "y":5.89353, "heading":-0.58905, "vx":-0.67943, "vy":1.29555, "omega":0.4998, "ax":0.08909, "ay":0.44194, "alpha":0.0, "fx":[1.51541,1.51541,1.51541,1.51541], "fy":[7.51736,7.51736,7.51736,7.51736]}, + {"t":6.59787, "x":6.57091, "y":5.92548, "heading":-0.57678, "vx":-0.67724, "vy":1.3064, "omega":0.4998, "ax":0.09097, "ay":0.44174, "alpha":0.0, "fx":[1.54744,1.54744,1.54744,1.54744], "fy":[7.51387,7.51387,7.51387,7.51387]}, + {"t":6.62243, "x":6.55431, "y":5.95769, "heading":-0.5645, "vx":-0.675, "vy":1.31725, "omega":0.4998, "ax":0.09258, "ay":0.44127, "alpha":0.0, "fx":[1.57476,1.57476,1.57476,1.57476], "fy":[7.50591,7.50591,7.50591,7.50591]}, + {"t":6.64698, "x":6.53776, "y":5.99016, "heading":-0.55223, "vx":-0.67273, "vy":1.32808, "omega":0.4998, "ax":0.09387, "ay":0.44053, "alpha":0.0, "fx":[1.59675,1.59675,1.59675,1.59675], "fy":[7.49321,7.49321,7.49321,7.49321]}, + {"t":6.67153, "x":6.52127, "y":6.02291, "heading":-0.53996, "vx":-0.67043, "vy":1.3389, "omega":0.4998, "ax":0.09479, "ay":0.43949, "alpha":0.0, "fx":[1.61236,1.61236,1.61236,1.61236], "fy":[7.47561,7.47561,7.47561,7.47561]}, + {"t":6.69609, "x":6.50484, "y":6.05591, "heading":-0.52769, "vx":-0.6681, "vy":1.34969, "omega":0.4998, "ax":0.09528, "ay":0.43812, "alpha":0.0, "fx":[1.62064,1.62064,1.62064,1.62064], "fy":[7.45238,7.45238,7.45238,7.45238]}, + {"t":6.72064, "x":6.48846, "y":6.08919, "heading":-0.51542, "vx":-0.66576, "vy":1.36045, "omega":0.4998, "ax":0.09527, "ay":0.43641, "alpha":0.0, "fx":[1.62047,1.62047,1.62047,1.62047], "fy":[7.42313,7.42313,7.42313,7.42313]}, + {"t":6.74519, "x":6.47214, "y":6.12272, "heading":-0.50315, "vx":-0.66342, "vy":1.37117, "omega":0.4998, "ax":0.09468, "ay":0.43431, "alpha":0.0, "fx":[1.61056,1.61056,1.61056,1.61056], "fy":[7.38748,7.38748,7.38748,7.38748]}, + {"t":6.76975, "x":6.45588, "y":6.15652, "heading":-0.49087, "vx":-0.6611, "vy":1.38183, "omega":0.4998, "ax":0.09345, "ay":0.43179, "alpha":0.0, "fx":[1.58951,1.58951,1.58951,1.58951], "fy":[7.34465,7.34465,7.34465,7.34465]}, + {"t":6.7943, "x":6.43968, "y":6.19058, "heading":-0.4786, "vx":-0.6588, "vy":1.39243, "omega":0.4998, "ax":0.09147, "ay":0.42881, "alpha":0.0, "fx":[1.55581,1.55581,1.55581,1.55581], "fy":[7.29389,7.29389,7.29389,7.29389]}, + {"t":6.81885, "x":6.42353, "y":6.2249, "heading":-0.46633, "vx":-0.65656, "vy":1.40296, "omega":0.4998, "ax":0.08862, "ay":0.4253, "alpha":0.0, "fx":[1.50743,1.50743,1.50743,1.50743], "fy":[7.23424,7.23424,7.23424,7.23424]}, + {"t":6.84341, "x":6.40744, "y":6.25947, "heading":-0.45406, "vx":-0.65438, "vy":1.4134, "omega":0.4998, "ax":0.08479, "ay":0.42121, "alpha":0.0, "fx":[1.44231,1.44231,1.44231,1.44231], "fy":[7.16464,7.16464,7.16464,7.16464]}, + {"t":6.86796, "x":6.39139, "y":6.2943, "heading":-0.44179, "vx":-0.6523, "vy":1.42374, "omega":0.4998, "ax":0.07984, "ay":0.41645, "alpha":0.0, "fx":[1.35808,1.35807,1.35807,1.35808], "fy":[7.08363,7.08364,7.08363,7.08363]}, + {"t":6.89252, "x":6.3754, "y":6.32939, "heading":-0.42951, "vx":-0.65034, "vy":1.43397, "omega":0.4998, "ax":0.07361, "ay":0.41093, "alpha":0.0, "fx":[1.25209,1.25209,1.25209,1.25209], "fy":[6.98979,6.98979,6.98979,6.98979]}, + {"t":6.91707, "x":6.35946, "y":6.36472, "heading":-0.41724, "vx":-0.64853, "vy":1.44406, "omega":0.4998, "ax":0.06594, "ay":0.40453, "alpha":0.0, "fx":[1.1216,1.1216,1.1216,1.1216], "fy":[6.88101,6.88101,6.88101,6.88101]}, + {"t":6.94162, "x":6.34355, "y":6.4003, "heading":-0.40497, "vx":-0.64691, "vy":1.45399, "omega":0.4998, "ax":0.05661, "ay":0.39712, "alpha":0.0, "fx":[0.96287,0.96287,0.96287,0.96287], "fy":[6.75488,6.75488,6.75488,6.75488]}, + {"t":6.96618, "x":6.32769, "y":6.43612, "heading":-0.3927, "vx":-0.64552, "vy":1.46374, "omega":0.4998, "ax":0.04538, "ay":0.3885, "alpha":0.0, "fx":[0.77195,0.77195,0.77195,0.77195], "fy":[6.60835,6.60835,6.60835,6.60835]}, + {"t":6.99073, "x":6.31185, "y":6.47218, "heading":-0.38043, "vx":-0.64441, "vy":1.47328, "omega":0.4998, "ax":0.03203, "ay":0.37846, "alpha":0.0, "fx":[0.54479,0.54479,0.54478,0.54479], "fy":[6.43752,6.43752,6.43752,6.43752]}, + {"t":7.01528, "x":6.29604, "y":6.50847, "heading":-0.36816, "vx":-0.64362, "vy":1.48257, "omega":0.4998, "ax":0.01625, "ay":0.36671, "alpha":0.0, "fx":[0.27639,0.27639,0.27639,0.27639], "fy":[6.23769,6.23769,6.23769,6.23769]}, + {"t":7.03984, "x":6.28024, "y":6.54498, "heading":-0.35588, "vx":-0.64322, "vy":1.49158, "omega":0.4998, "ax":-0.00228, "ay":0.3529, "alpha":0.0, "fx":[-0.03882,-0.03882,-0.03883,-0.03882], "fy":[6.00281,6.00281,6.00281,6.00281]}, + {"t":7.06439, "x":6.26444, "y":6.58171, "heading":-0.34361, "vx":-0.64328, "vy":1.50024, "omega":0.4998, "ax":-0.02392, "ay":0.33659, "alpha":0.0, "fx":[-0.40685,-0.40685,-0.40685,-0.40685], "fy":[5.72524,5.72524,5.72524,5.72524]}, + {"t":7.08894, "x":6.24864, "y":6.61865, "heading":-0.33134, "vx":-0.64386, "vy":1.50851, "omega":0.4998, "ax":-0.04902, "ay":0.3172, "alpha":0.0, "fx":[-0.83388,-0.83388,-0.83388,-0.83388], "fy":[5.39549,5.39549,5.39549,5.39549]}, + {"t":7.1135, "x":6.23282, "y":6.65578, "heading":-0.31907, "vx":-0.64507, "vy":1.5163, "omega":0.4998, "ax":-0.07806, "ay":0.294, "alpha":0.0, "fx":[-1.32778,-1.32778,-1.32778,-1.32778], "fy":[5.00079,5.00079,5.00079,5.00079]}, + {"t":7.13805, "x":6.21696, "y":6.6931, "heading":-0.3068, "vx":-0.64698, "vy":1.52352, "omega":0.4998, "ax":-0.11144, "ay":0.26603, "alpha":0.0, "fx":[-1.89561,-1.89561,-1.89561,-1.89561], "fy":[4.52516,4.52516,4.52516,4.52516]}, + {"t":7.16261, "x":6.20104, "y":6.73059, "heading":-0.29452, "vx":-0.64972, "vy":1.53005, "omega":0.4998, "ax":-0.14969, "ay":0.23208, "alpha":0.0, "fx":[-2.54625,-2.54626,-2.54626,-2.54626], "fy":[3.94769,3.94769,3.94769,3.94769]}, + {"t":7.18716, "x":6.18504, "y":6.76823, "heading":-0.28225, "vx":-0.6534, "vy":1.53575, "omega":0.4998, "ax":-0.19328, "ay":0.19055, "alpha":0.0, "fx":[-3.28759,-3.28759,-3.28759,-3.28759], "fy":[3.24113,3.24113,3.24113,3.24113]}, + {"t":7.21171, "x":6.16894, "y":6.80599, "heading":-0.26998, "vx":-0.65814, "vy":1.54042, "omega":0.4998, "ax":-0.24263, "ay":0.13934, "alpha":0.0, "fx":[-4.12706,-4.12706,-4.12707,-4.12706], "fy":[2.37011,2.37011,2.37011,2.37011]}, + {"t":7.23627, "x":6.1527, "y":6.84386, "heading":-0.25771, "vx":-0.6641, "vy":1.54385, "omega":0.4998, "ax":-0.29818, "ay":0.07571, "alpha":0.0, "fx":[-5.07195,-5.07195,-5.07196,-5.07195], "fy":[1.28778,1.28778,1.28778,1.28778]}, + {"t":7.26082, "x":6.13631, "y":6.88179, "heading":-0.24544, "vx":-0.67142, "vy":1.54571, "omega":0.4998, "ax":-0.36012, "ay":-0.00388, "alpha":0.0, "fx":[-6.12551,-6.12551,-6.12551,-6.12551], "fy":[-0.06598,-0.06598,-0.06598,-0.06598]}, + {"t":7.28537, "x":6.11971, "y":6.91974, "heading":-0.23317, "vx":-0.68026, "vy":1.54561, "omega":0.4998, "ax":-0.42847, "ay":-0.10408, "alpha":0.0, "fx":[-7.28809,-7.2881,-7.2881,-7.28809], "fy":[-1.77031,-1.77031,-1.77031,-1.77032]}, + {"t":7.30993, "x":6.10288, "y":6.95766, "heading":-0.22089, "vx":-0.69078, "vy":1.54305, "omega":0.4998, "ax":-0.50283, "ay":-0.2308, "alpha":0.0, "fx":[-8.55299,-8.553,-8.553,-8.55299], "fy":[-3.92579,-3.92579,-3.92579,-3.92579]}, + {"t":7.33448, "x":6.08577, "y":6.99548, "heading":-0.20862, "vx":-0.70313, "vy":1.53739, "omega":0.4998, "ax":-0.58227, "ay":-0.39155, "alpha":0.0, "fx":[-9.90424,-9.90425,-9.90425,-9.90425], "fy":[-6.66009,-6.66009,-6.6601,-6.6601]}, + {"t":7.35903, "x":6.06833, "y":7.03311, "heading":-0.19635, "vx":-0.71743, "vy":1.52777, "omega":0.4998, "ax":-0.66496, "ay":-0.59536, "alpha":0.0, "fx":[-11.31079,-11.31079,-11.31079,-11.31079], "fy":[-10.12683,-10.12683,-10.12683,-10.12683]}, + {"t":7.38359, "x":6.05051, "y":7.07044, "heading":-0.18408, "vx":-0.73375, "vy":1.51316, "omega":0.4998, "ax":-0.74798, "ay":-0.8526, "alpha":0.0, "fx":[-12.72284,-12.72285,-12.72285,-12.72285], "fy":[-14.50256,-14.50256,-14.50257,-14.50257]}, + {"t":7.40814, "x":6.03227, "y":7.10734, "heading":-0.17181, "vx":-0.75212, "vy":1.49222, "omega":0.4998, "ax":-0.82705, "ay":-1.17401, "alpha":0.0, "fx":[-14.06782,-14.06782,-14.06782,-14.06782], "fy":[-19.96964,-19.96964,-19.96965,-19.96965]}, + {"t":7.4327, "x":6.01355, "y":7.14362, "heading":-0.15953, "vx":-0.77243, "vy":1.46339, "omega":0.4998, "ax":-0.89643, "ay":-1.56863, "alpha":0.0, "fx":[-15.24798,-15.24799,-15.24799,-15.24798], "fy":[-26.682,-26.682,-26.682,-26.68201]}, + {"t":7.45725, "x":5.99432, "y":7.17908, "heading":-0.14726, "vx":-0.79444, "vy":1.42488, "omega":0.4998, "ax":-0.94929, "ay":-2.04036, "alpha":0.0, "fx":[-16.14707,-16.14708,-16.14708,-16.14707], "fy":[-34.70589,-34.70589,-34.70589,-34.70589]}, + {"t":7.4818, "x":5.97453, "y":7.21345, "heading":-0.13499, "vx":-0.81775, "vy":1.37478, "omega":0.4998, "ax":-0.9786, "ay":-2.58411, "alpha":0.0, "fx":[-16.64569,-16.64569,-16.64569,-16.64569], "fy":[-43.9549,-43.9549,-43.9549,-43.9549]}, + {"t":7.50636, "x":5.95415, "y":7.24643, "heading":-0.12272, "vx":-0.84177, "vy":1.31133, "omega":0.4998, "ax":-0.97888, "ay":-3.18268, "alpha":0.0, "fx":[-16.65055,-16.65055,-16.65055,-16.65055], "fy":[-54.13653,-54.13653,-54.13653,-54.13653]}, + {"t":7.53091, "x":5.93319, "y":7.27767, "heading":-0.11045, "vx":-0.86581, "vy":1.23319, "omega":0.4998, "ax":-0.94824, "ay":-3.80816, "alpha":0.0, "fx":[-16.12931,-16.12931,-16.12931,-16.12931], "fy":[-64.77571,-64.77571,-64.77571,-64.77571]}, + {"t":7.55546, "x":5.91164, "y":7.3068, "heading":-0.09817, "vx":-0.88909, "vy":1.13968, "omega":0.4998, "ax":-0.88944, "ay":-4.42806, "alpha":0.0, "fx":[-15.12907,-15.12907,-15.12907,-15.12907], "fy":[-75.32008,-75.32008,-75.32008,-75.32008]}, + {"t":7.58002, "x":5.88955, "y":7.33345, "heading":-0.0859, "vx":-0.91093, "vy":1.03096, "omega":0.4998, "ax":-0.80919, "ay":-5.01404, "alpha":0.0, "fx":[-13.76416,-13.76416,-13.76416,-13.76416], "fy":[-85.28744,-85.28744,-85.28744,-85.28744]}, + {"t":7.60457, "x":5.86693, "y":7.35725, "heading":-0.07363, "vx":-0.9308, "vy":0.90784, "omega":0.4998, "ax":-0.71604, "ay":-5.54782, "alpha":0.0, "fx":[-12.1797,-12.1797,-12.1797,-12.1797], "fy":[-94.36686,-94.36686,-94.36686,-94.36686]}, + {"t":7.62912, "x":5.84386, "y":7.37787, "heading":-0.06136, "vx":-0.94838, "vy":0.77162, "omega":0.4998, "ax":-0.61806, "ay":-6.022, "alpha":0.0, "fx":[-10.51308,-10.51308,-10.51308,-10.51308], "fy":[-102.43252,-102.43252,-102.43252,-102.43252]}, + {"t":7.65368, "x":5.82039, "y":7.395, "heading":-0.04909, "vx":-0.96356, "vy":0.62376, "omega":0.4998, "ax":-0.52157, "ay":-6.43713, "alpha":0.0, "fx":[-8.87169,-8.87169,-8.87169,-8.87169], "fy":[-109.49375,-109.49375,-109.49375,-109.49375]}, + {"t":7.67823, "x":5.79658, "y":7.40837, "heading":-0.03682, "vx":-0.97636, "vy":0.46571, "omega":0.4998, "ax":-0.4308, "ay":-6.79807, "alpha":0.0, "fx":[-7.32773,-7.32773,-7.32773,-7.32773], "fy":[-115.63324,-115.63324,-115.63324,-115.63324]}, + {"t":7.70279, "x":5.77247, "y":7.41776, "heading":-0.02454, "vx":-0.98694, "vy":0.29879, "omega":0.4998, "ax":-0.34828, "ay":-7.11122, "alpha":-0.00003, "fx":[-5.924,-5.92401,-5.92429,-5.92428], "fy":[-120.9599,-120.95984,-120.95983,-120.95989]}, + {"t":7.72734, "x":5.74814, "y":7.42295, "heading":-0.01227, "vx":-0.99549, "vy":0.12418, "omega":0.4998, "ax":-0.14922, "ay":-5.22788, "alpha":-20.35532, "fx":[65.16188,110.36447,-114.32447,-71.35441], "fy":[-126.19003,-55.46564,-50.91362,-123.12938]}, + {"t":7.75189, "x":5.72365, "y":7.42443, "heading":0.0, "vx":-0.99916, "vy":-0.00418, "omega":0.0, "ax":-0.00406, "ay":-0.53282, "alpha":0.0, "fx":[-0.06907,-0.06907,-0.06907,-0.06907], "fy":[-9.06304,-9.06304,-9.06304,-9.06304]}, + {"t":7.79979, "x":5.67579, "y":7.42361, "heading":0.0, "vx":-0.99935, "vy":-0.0297, "omega":0.0, "ax":0.00004, "ay":-0.00138, "alpha":0.0, "fx":[0.0007,0.0007,0.0007,0.0007], "fy":[-0.02352,-0.02352,-0.02352,-0.02352]}, + {"t":7.84768, "x":5.62793, "y":7.42219, "heading":0.0, "vx":-0.99935, "vy":-0.02976, "omega":0.0, "ax":-0.00008, "ay":0.00258, "alpha":0.0, "fx":[-0.00131,-0.00131,-0.00131,-0.00131], "fy":[0.04396,0.04396,0.04396,0.04396]}, + {"t":7.89558, "x":5.58006, "y":7.42077, "heading":0.0, "vx":-0.99935, "vy":-0.02964, "omega":0.0, "ax":-0.00008, "ay":0.0026, "alpha":0.0, "fx":[-0.00131,-0.00131,-0.00131,-0.00131], "fy":[0.04424,0.04424,0.04424,0.04424]}, + {"t":7.94347, "x":5.5322, "y":7.41935, "heading":0.0, "vx":-0.99936, "vy":-0.02952, "omega":0.0, "ax":-0.00008, "ay":0.00259, "alpha":0.0, "fx":[-0.0013,-0.0013,-0.0013,-0.0013], "fy":[0.04398,0.04398,0.04398,0.04398]}, + {"t":7.99136, "x":5.48433, "y":7.41794, "heading":0.0, "vx":-0.99936, "vy":-0.02939, "omega":0.0, "ax":-0.00008, "ay":0.00257, "alpha":0.0, "fx":[-0.00128,-0.00128,-0.00128,-0.00128], "fy":[0.04373,0.04373,0.04373,0.04373]}, + {"t":8.03926, "x":5.43647, "y":7.41654, "heading":0.0, "vx":-0.99936, "vy":-0.02927, "omega":0.0, "ax":-0.00007, "ay":0.00256, "alpha":0.0, "fx":[-0.00127,-0.00127,-0.00127,-0.00127], "fy":[0.04348,0.04348,0.04348,0.04348]}, + {"t":8.08715, "x":5.38861, "y":7.41514, "heading":0.0, "vx":-0.99937, "vy":-0.02915, "omega":0.0, "ax":-0.00007, "ay":0.00254, "alpha":0.0, "fx":[-0.00126,-0.00126,-0.00126,-0.00126], "fy":[0.04323,0.04323,0.04323,0.04323]}, + {"t":8.13505, "x":5.34074, "y":7.41374, "heading":0.0, "vx":-0.99937, "vy":-0.02903, "omega":0.0, "ax":-0.00007, "ay":0.00253, "alpha":0.0, "fx":[-0.00125,-0.00125,-0.00125,-0.00125], "fy":[0.04299,0.04299,0.04299,0.04299]}, + {"t":8.18294, "x":5.29288, "y":7.41236, "heading":0.0, "vx":-0.99937, "vy":-0.0289, "omega":0.0, "ax":-0.00007, "ay":0.00251, "alpha":0.0, "fx":[-0.00123,-0.00123,-0.00123,-0.00123], "fy":[0.04276,0.04276,0.04276,0.04276]}, + {"t":8.23084, "x":5.24501, "y":7.41097, "heading":0.0, "vx":-0.99938, "vy":-0.02878, "omega":0.0, "ax":-0.00007, "ay":0.0025, "alpha":0.0, "fx":[-0.00122,-0.00122,-0.00122,-0.00122], "fy":[0.04252,0.04252,0.04252,0.04252]}, + {"t":8.27873, "x":5.19715, "y":7.4096, "heading":0.0, "vx":-0.99938, "vy":-0.02866, "omega":0.0, "ax":-0.00007, "ay":0.00249, "alpha":0.0, "fx":[-0.00121,-0.00121,-0.00121,-0.00121], "fy":[0.04229,0.04229,0.04229,0.04229]}, + {"t":8.32663, "x":5.14928, "y":7.40823, "heading":0.0, "vx":-0.99938, "vy":-0.02855, "omega":0.0, "ax":-0.00007, "ay":0.00247, "alpha":0.0, "fx":[-0.0012,-0.0012,-0.0012,-0.0012], "fy":[0.04207,0.04207,0.04207,0.04207]}, + {"t":8.37452, "x":5.10142, "y":7.40686, "heading":0.0, "vx":-0.99939, "vy":-0.02843, "omega":0.0, "ax":-0.00007, "ay":0.00246, "alpha":0.0, "fx":[-0.00119,-0.00119,-0.00119,-0.00119], "fy":[0.04185,0.04185,0.04185,0.04185]}, + {"t":8.42241, "x":5.05355, "y":7.40551, "heading":0.0, "vx":-0.99939, "vy":-0.02831, "omega":0.0, "ax":-0.00007, "ay":0.00245, "alpha":0.0, "fx":[-0.00118,-0.00118,-0.00118,-0.00118], "fy":[0.04163,0.04163,0.04163,0.04163]}, + {"t":8.47031, "x":5.00569, "y":7.40415, "heading":0.0, "vx":-0.99939, "vy":-0.02819, "omega":0.0, "ax":-0.00007, "ay":0.00243, "alpha":0.0, "fx":[-0.00117,-0.00117,-0.00117,-0.00117], "fy":[0.04141,0.04141,0.04141,0.04141]}, + {"t":8.5182, "x":4.95782, "y":7.40281, "heading":0.0, "vx":-0.9994, "vy":-0.02808, "omega":0.0, "ax":-0.00007, "ay":0.00242, "alpha":0.0, "fx":[-0.00116,-0.00116,-0.00116,-0.00116], "fy":[0.0412,0.0412,0.0412,0.0412]}, + {"t":8.5661, "x":4.90996, "y":7.40146, "heading":0.0, "vx":-0.9994, "vy":-0.02796, "omega":0.0, "ax":-0.00007, "ay":0.00241, "alpha":0.0, "fx":[-0.00114,-0.00114,-0.00114,-0.00114], "fy":[0.04099,0.04099,0.04099,0.04099]}, + {"t":8.61399, "x":4.86209, "y":7.40013, "heading":0.0, "vx":-0.9994, "vy":-0.02784, "omega":0.0, "ax":-0.00007, "ay":0.0024, "alpha":0.0, "fx":[-0.00113,-0.00113,-0.00113,-0.00113], "fy":[0.04079,0.04079,0.04079,0.04079]}, + {"t":8.66189, "x":4.81423, "y":7.3988, "heading":0.0, "vx":-0.99941, "vy":-0.02773, "omega":0.0, "ax":-0.00007, "ay":0.00239, "alpha":0.0, "fx":[-0.00112,-0.00112,-0.00112,-0.00112], "fy":[0.04059,0.04059,0.04059,0.04059]}, + {"t":8.70978, "x":4.76636, "y":7.39747, "heading":0.0, "vx":-0.99941, "vy":-0.02761, "omega":0.0, "ax":-0.00007, "ay":0.00237, "alpha":0.0, "fx":[-0.00111,-0.00111,-0.00111,-0.00111], "fy":[0.04039,0.04039,0.04039,0.04039]}, + {"t":8.75767, "x":4.71849, "y":7.39615, "heading":0.0, "vx":-0.99941, "vy":-0.0275, "omega":0.0, "ax":-0.00006, "ay":0.00236, "alpha":0.0, "fx":[-0.0011,-0.0011,-0.0011,-0.0011], "fy":[0.04019,0.04019,0.04019,0.04019]}, + {"t":8.80557, "x":4.67063, "y":7.39484, "heading":0.0, "vx":-0.99942, "vy":-0.02739, "omega":0.0, "ax":-0.00006, "ay":0.00235, "alpha":0.0, "fx":[-0.00109,-0.00109,-0.00109,-0.00109], "fy":[0.04,0.04,0.04,0.04]}, + {"t":8.85346, "x":4.62276, "y":7.39353, "heading":0.0, "vx":-0.99942, "vy":-0.02728, "omega":0.0, "ax":-0.00006, "ay":0.00234, "alpha":0.0, "fx":[-0.00108,-0.00108,-0.00108,-0.00108], "fy":[0.03981,0.03981,0.03981,0.03981]}, + {"t":8.90136, "x":4.57489, "y":7.39222, "heading":0.0, "vx":-0.99942, "vy":-0.02716, "omega":0.0, "ax":-0.00006, "ay":0.00233, "alpha":0.0, "fx":[-0.00107,-0.00107,-0.00107,-0.00107], "fy":[0.03962,0.03962,0.03962,0.03962]}, + {"t":8.94925, "x":4.52703, "y":7.39093, "heading":0.0, "vx":-0.99943, "vy":-0.02705, "omega":0.0, "ax":-0.00006, "ay":0.00232, "alpha":0.0, "fx":[-0.00107,-0.00107,-0.00107,-0.00107], "fy":[0.03944,0.03944,0.03944,0.03944]}, + {"t":8.99715, "x":4.47916, "y":7.38963, "heading":0.0, "vx":-0.99943, "vy":-0.02694, "omega":0.0, "ax":-0.00006, "ay":0.00231, "alpha":0.0, "fx":[-0.00106,-0.00106,-0.00106,-0.00106], "fy":[0.03926,0.03926,0.03926,0.03926]}, + {"t":9.04504, "x":4.43129, "y":7.38834, "heading":0.0, "vx":-0.99943, "vy":-0.02683, "omega":0.0, "ax":-0.00006, "ay":0.0023, "alpha":0.0, "fx":[-0.00105,-0.00105,-0.00105,-0.00105], "fy":[0.03908,0.03908,0.03908,0.03908]}, + {"t":9.09294, "x":4.38343, "y":7.38706, "heading":0.0, "vx":-0.99943, "vy":-0.02672, "omega":0.0, "ax":-0.00006, "ay":0.00229, "alpha":0.0, "fx":[-0.00104,-0.00104,-0.00104,-0.00104], "fy":[0.0389,0.0389,0.0389,0.0389]}, + {"t":9.14083, "x":4.33556, "y":7.38579, "heading":0.0, "vx":-0.99944, "vy":-0.02661, "omega":0.0, "ax":-0.00006, "ay":0.00228, "alpha":0.0, "fx":[-0.00103,-0.00103,-0.00103,-0.00103], "fy":[0.03872,0.03872,0.03872,0.03872]}, + {"t":9.18872, "x":4.28769, "y":7.38451, "heading":0.0, "vx":-0.99944, "vy":-0.0265, "omega":0.0, "ax":-0.00006, "ay":0.00227, "alpha":0.0, "fx":[-0.00102,-0.00102,-0.00102,-0.00102], "fy":[0.03855,0.03855,0.03855,0.03855]}, + {"t":9.23662, "x":4.23982, "y":7.38325, "heading":0.0, "vx":-0.99944, "vy":-0.02639, "omega":0.0, "ax":-0.00006, "ay":0.00226, "alpha":0.0, "fx":[-0.00101,-0.00101,-0.00101,-0.00101], "fy":[0.03838,0.03838,0.03838,0.03838]}, + {"t":9.28451, "x":4.19196, "y":7.38199, "heading":0.0, "vx":-0.99945, "vy":-0.02628, "omega":0.0, "ax":-0.00006, "ay":0.00225, "alpha":0.0, "fx":[-0.001,-0.001,-0.001,-0.001], "fy":[0.03822,0.03822,0.03822,0.03822]}, + {"t":9.33241, "x":4.14409, "y":7.38073, "heading":0.0, "vx":-0.99945, "vy":-0.02618, "omega":0.0, "ax":-0.00006, "ay":0.00224, "alpha":0.0, "fx":[-0.00099,-0.00099,-0.00099,-0.00099], "fy":[0.03805,0.03805,0.03805,0.03805]}, + {"t":9.3803, "x":4.09622, "y":7.37948, "heading":0.0, "vx":-0.99945, "vy":-0.02607, "omega":0.0, "ax":-0.00006, "ay":0.00223, "alpha":0.0, "fx":[-0.00099,-0.00099,-0.00099,-0.00099], "fy":[0.03789,0.03789,0.03789,0.03789]}, + {"t":9.4282, "x":4.04835, "y":7.37823, "heading":0.0, "vx":-0.99945, "vy":-0.02596, "omega":0.0, "ax":-0.00006, "ay":0.00222, "alpha":0.0, "fx":[-0.00098,-0.00098,-0.00098,-0.00098], "fy":[0.03773,0.03773,0.03773,0.03773]}, + {"t":9.47609, "x":4.00048, "y":7.37699, "heading":0.0, "vx":-0.99946, "vy":-0.02586, "omega":0.0, "ax":-0.00006, "ay":0.00221, "alpha":0.0, "fx":[-0.00097,-0.00097,-0.00097,-0.00097], "fy":[0.03757,0.03757,0.03757,0.03757]}, + {"t":9.52399, "x":3.95262, "y":7.37576, "heading":0.0, "vx":-0.99946, "vy":-0.02575, "omega":0.0, "ax":-0.00006, "ay":0.0022, "alpha":0.0, "fx":[-0.00096,-0.00096,-0.00096,-0.00096], "fy":[0.03741,0.03741,0.03741,0.03741]}, + {"t":9.57188, "x":3.90475, "y":7.37452, "heading":0.0, "vx":-0.99946, "vy":-0.02565, "omega":0.0, "ax":-0.00006, "ay":0.00219, "alpha":0.0, "fx":[-0.00095,-0.00095,-0.00095,-0.00095], "fy":[0.03726,0.03726,0.03726,0.03726]}, + {"t":9.61977, "x":3.85688, "y":7.3733, "heading":0.0, "vx":-0.99947, "vy":-0.02554, "omega":0.0, "ax":-0.00006, "ay":0.00218, "alpha":0.0, "fx":[-0.00095,-0.00095,-0.00095,-0.00095], "fy":[0.03711,0.03711,0.03711,0.03711]}, + {"t":9.66767, "x":3.80901, "y":7.37208, "heading":0.0, "vx":-0.99947, "vy":-0.02544, "omega":0.0, "ax":-0.00006, "ay":0.00217, "alpha":0.0, "fx":[-0.00094,-0.00094,-0.00094,-0.00094], "fy":[0.03696,0.03696,0.03696,0.03696]}, + {"t":9.71556, "x":3.76114, "y":7.37086, "heading":0.0, "vx":-0.99947, "vy":-0.02533, "omega":0.0, "ax":-0.00005, "ay":0.00216, "alpha":0.0, "fx":[-0.00093,-0.00093,-0.00093,-0.00093], "fy":[0.03681,0.03681,0.03681,0.03681]}, + {"t":9.76346, "x":3.71327, "y":7.36965, "heading":0.0, "vx":-0.99947, "vy":-0.02523, "omega":0.0, "ax":-0.00005, "ay":0.00216, "alpha":0.0, "fx":[-0.00092,-0.00092,-0.00092,-0.00092], "fy":[0.03666,0.03666,0.03666,0.03666]}, + {"t":9.81135, "x":3.6654, "y":7.36845, "heading":0.0, "vx":-0.99948, "vy":-0.02513, "omega":0.0, "ax":-0.00005, "ay":0.00215, "alpha":0.0, "fx":[-0.00092,-0.00092,-0.00092,-0.00092], "fy":[0.03652,0.03652,0.03652,0.03652]}, + {"t":9.85925, "x":3.61753, "y":7.36724, "heading":0.0, "vx":-0.99948, "vy":-0.02502, "omega":0.0, "ax":-0.00005, "ay":0.00214, "alpha":0.0, "fx":[-0.00091,-0.00091,-0.00091,-0.00091], "fy":[0.03638,0.03638,0.03638,0.03638]}, + {"t":9.90714, "x":3.56966, "y":7.36605, "heading":0.0, "vx":-0.99948, "vy":-0.02492, "omega":0.0, "ax":-0.00005, "ay":0.00213, "alpha":0.0, "fx":[-0.0009,-0.0009,-0.0009,-0.0009], "fy":[0.03624,0.03624,0.03624,0.03624]}, + {"t":9.95504, "x":3.52179, "y":7.36486, "heading":0.0, "vx":-0.99948, "vy":-0.02482, "omega":0.0, "ax":-0.00005, "ay":0.00212, "alpha":0.0, "fx":[-0.00089,-0.00089,-0.00089,-0.00089], "fy":[0.036,0.036,0.036,0.036]}, + {"t":10.00293, "x":3.47392, "y":7.36367, "heading":0.0, "vx":-0.99949, "vy":-0.02472, "omega":0.0, "ax":-0.00003, "ay":0.00134, "alpha":0.0, "fx":[-0.00056,-0.00056,-0.00056,-0.00056], "fy":[0.02275,0.02275,0.02275,0.02275]}, + {"t":10.05082, "x":3.42605, "y":7.36249, "heading":0.0, "vx":-0.99949, "vy":-0.02465, "omega":0.0, "ax":0.00275, "ay":-0.10126, "alpha":0.0, "fx":[0.04673,0.04673,0.04673,0.04673], "fy":[-1.72236,-1.72236,-1.72236,-1.72236]}, + {"t":10.09872, "x":3.37819, "y":7.36119, "heading":0.0, "vx":-0.99936, "vy":-0.0295, "omega":0.0, "ax":1.35107, "ay":-6.79397, "alpha":0.0, "fx":[22.98136,22.98136,22.98136,22.98136], "fy":[-115.56353,-115.56353,-115.56353,-115.56353]}, + {"t":10.14661, "x":3.33187, "y":7.35199, "heading":0.0, "vx":-0.93465, "vy":-0.3549, "omega":0.0, "ax":5.4641, "ay":-8.02996, "alpha":0.0, "fx":[92.9427,92.9427,92.9427,92.9427], "fy":[-136.58729,-136.58729,-136.58729,-136.58729]}, + {"t":10.19451, "x":3.29338, "y":7.32578, "heading":0.0, "vx":-0.67295, "vy":-0.73949, "omega":0.0, "ax":-1.68946, "ay":-9.59453, "alpha":0.04225, "fx":[-28.9994,-28.94334,-28.47564,-28.53083], "fy":[-163.1537,-163.16432,-163.24642,-163.23611]}, + {"t":10.23023, "x":3.26826, "y":7.29324, "heading":0.0, "vx":-0.73331, "vy":-1.08227, "omega":0.00151, "ax":-1.55749, "ay":-9.60644, "alpha":0.05731, "fx":[-26.8456,-26.77529,-26.13998,-26.2087], "fy":[-163.34486,-163.35776,-163.4603,-163.44794]}, + {"t":10.26596, "x":3.24106, "y":7.24844, "heading":0.00005, "vx":-0.78895, "vy":-1.42548, "omega":0.00356, "ax":-1.16738, "ay":-9.63105, "alpha":0.10199, "fx":[-20.47267,-20.37749,-19.24363,-19.33366], "fy":[-163.74471,-163.76131,-163.89758,-163.88224]}, + {"t":10.30169, "x":3.21213, "y":7.19136, "heading":0.00018, "vx":-0.83066, "vy":-1.76957, "omega":0.0072, "ax":7.94846, "ay":-4.30137, "alpha":0.66806, "fx":[135.13088,131.94123,135.43156,138.30013], "fy":[-72.28454,-78.56537,-73.953,-67.85718]}, + {"t":10.33742, "x":3.18753, "y":7.1254, "heading":0.00044, "vx":-0.54668, "vy":-1.92324, "omega":0.03107, "ax":3.73105, "ay":-0.92368, "alpha":5.81351, "fx":[44.00803,42.04529,82.79482,85.00801], "fy":[2.55237,-37.21006,-30.4723,2.28413]}, + {"t":10.37314, "x":3.17038, "y":7.0561, "heading":0.00155, "vx":-0.41338, "vy":-1.95624, "omega":0.23877, "ax":0.31077, "ay":-0.06475, "alpha":7.00137, "fx":[-21.72223,-21.51997,32.14892,32.23766], "fy":[17.52294,-20.08319,-19.00365,17.15824]}, + {"t":10.40887, "x":3.15581, "y":6.98616, "heading":0.01008, "vx":-0.40228, "vy":-1.95856, "omega":0.48891, "ax":0.01431, "ay":-0.00294, "alpha":5.99484, "fx":[-23.11175,-22.65922,23.57196,23.17247], "fy":[15.33719,-16.0819,-15.32552,15.87051]}, + {"t":10.4446, "x":3.14144, "y":6.91619, "heading":0.02755, "vx":-0.40177, "vy":-1.95866, "omega":0.70308, "ax":-0.00004, "ay":0.00001, "alpha":5.19946, "fx":[-20.49584,-19.62093,20.49016,19.62418], "fy":[12.92758,-14.21393,-12.91072,14.19765]}, + {"t":10.48032, "x":3.12709, "y":6.84621, "heading":0.05266, "vx":-0.40177, "vy":-1.95866, "omega":0.88884, "ax":-0.00151, "ay":0.00031, "alpha":4.25552, "fx":[-17.06436,-15.78192,17.01286,15.7307], "fy":[10.14516,-12.03056,-10.13392,12.04047]}, + {"t":10.51605, "x":3.11273, "y":6.77624, "heading":0.08442, "vx":-0.40183, "vy":-1.95865, "omega":1.04088, "ax":-0.00209, "ay":0.00043, "alpha":3.54022, "fx":[-14.47259,-12.81627,14.40217,12.74461], "fy":[7.9836,-10.41935,-7.97042,10.4354]}, + {"t":10.55178, "x":3.09838, "y":6.70626, "heading":0.12161, "vx":-0.4019, "vy":-1.95863, "omega":1.16736, "ax":-0.0021, "ay":0.00043, "alpha":2.84998, "fx":[-11.88884,-10.00495,11.81813,9.93309], "fy":[5.99142,-8.76367,-5.97787,8.77945]}, + {"t":10.5875, "x":3.08402, "y":6.63628, "heading":0.16331, "vx":-0.40198, "vy":-1.95862, "omega":1.26918, "ax":-0.00199, "ay":0.00041, "alpha":2.12121, "fx":[-9.03418,-7.17508,8.96688,7.10701], "fy":[4.08907,-6.82511,-4.07577,6.83965]}, + {"t":10.62323, "x":3.06965, "y":6.56631, "heading":0.20866, "vx":-0.40205, "vy":-1.9586, "omega":1.34497, "ax":-0.00193, "ay":0.0004, "alpha":1.65148, "fx":[-7.1758,-5.34792,7.11052,5.2822], "fy":[2.86689,-5.55695,-2.85371,5.57073]}, + {"t":10.65896, "x":3.05529, "y":6.49633, "heading":0.25671, "vx":-0.40212, "vy":-1.95859, "omega":1.40397, "ax":-0.0019, "ay":0.00039, "alpha":0.97022, "fx":[-4.30432,-2.99495,4.23981,2.93024], "fy":[1.48433,-3.40758,-1.47114,3.42097]}, + {"t":10.69469, "x":3.04092, "y":6.42636, "heading":0.30687, "vx":-0.40218, "vy":-1.95858, "omega":1.43863, "ax":-0.00188, "ay":0.00039, "alpha":0.5636, "fx":[-2.55328,-1.65228,2.48923,1.58817], "fy":[0.74054,-2.05987,-0.72738,2.07307]}, + {"t":10.73041, "x":3.02655, "y":6.35639, "heading":0.35827, "vx":-0.40225, "vy":-1.95856, "omega":1.45877, "ax":-0.00187, "ay":0.00039, "alpha":-0.0061, "fx":[-0.00399,-0.01631,-0.05974,-0.04742], "fy":[0.00098,0.03035,0.01213,-0.01723]}, + {"t":10.76614, "x":3.01218, "y":6.28641, "heading":0.41039, "vx":-0.40232, "vy":-1.95855, "omega":1.45855, "ax":-0.00186, "ay":0.00038, "alpha":-0.44486, "fx":[2.00723,1.07353,-2.07056,-1.13692], "fy":[-0.36611,1.75993,0.37913,-1.74688]}, + {"t":10.80187, "x":2.9978, "y":6.21644, "heading":0.4625, "vx":-0.40238, "vy":-1.95853, "omega":1.44266, "ax":-0.00186, "ay":0.00038, "alpha":-0.96356, "fx":[4.42063,2.16209,-4.4836,-2.22534], "fy":[-0.57051,3.9234,0.58344,-3.91035]}, + {"t":10.83759, "x":2.98343, "y":6.14647, "heading":0.51404, "vx":-0.40245, "vy":-1.95852, "omega":1.40823, "ax":-0.00185, "ay":0.00038, "alpha":-1.44886, "fx":[6.69858,2.96356,-6.76113,-3.02674], "fy":[-0.51998,6.05608,0.53281,-6.04304]}, + {"t":10.87332, "x":2.96905, "y":6.0765, "heading":0.56435, "vx":-0.40252, "vy":-1.95851, "omega":1.35647, "ax":-0.00184, "ay":0.00038, "alpha":-2.00384, "fx":[9.30135,3.68913,-9.36344,-3.7524], "fy":[-0.25739,8.56917,0.27015,-8.55614]}, + {"t":10.90905, "x":2.95466, "y":6.00652, "heading":0.61281, "vx":-0.40258, "vy":-1.95849, "omega":1.28488, "ax":-0.00184, "ay":0.00038, "alpha":-2.55618, "fx":[11.87653,4.18633,-11.93802,-4.24976], "fy":[0.24016,11.14409,-0.22744,-11.1311]}, + {"t":10.94477, "x":2.94028, "y":5.93655, "heading":0.65872, "vx":-0.40265, "vy":-1.95848, "omega":1.19355, "ax":-0.00183, "ay":0.00038, "alpha":-3.1995, "fx":[14.84499,4.60983,-14.90573,-4.67356], "fy":[0.97489,14.17221,-0.96216,-14.15933]}, + {"t":10.9805, "x":2.92589, "y":5.86658, "heading":0.70136, "vx":-0.40271, "vy":-1.95847, "omega":1.07925, "ax":-0.00182, "ay":0.00037, "alpha":-3.86742, "fx":[17.88551,4.85224,-17.94532,-4.91637], "fy":[1.93361,17.3505,-1.92079,-17.33781]}, + {"t":11.01623, "x":2.9115, "y":5.79661, "heading":0.73992, "vx":-0.40278, "vy":-1.95845, "omega":0.94107, "ax":-0.00181, "ay":0.00037, "alpha":-4.63604, "fx":[21.34298,5.02604,-21.40165,-5.09072], "fy":[3.13353,21.0054,-3.12052,-20.99303]}, + {"t":11.05196, "x":2.89711, "y":5.72664, "heading":0.77354, "vx":-0.40284, "vy":-1.95844, "omega":0.77544, "ax":-0.00179, "ay":0.00037, "alpha":-5.4723, "fx":[25.06234,5.11103,-25.11936,-5.17612], "fy":[4.53425,24.97781,-4.52101,-24.96592]}, + {"t":11.08768, "x":2.88272, "y":5.65667, "heading":0.80124, "vx":-0.40291, "vy":-1.95843, "omega":0.57993, "ax":-0.00164, "ay":0.00034, "alpha":-6.40481, "fx":[29.1849,5.18601,-29.23545,-5.24701], "fy":[6.10837,29.38667,-6.09617,-29.37591]}, + {"t":11.12341, "x":2.86832, "y":5.58671, "heading":0.82196, "vx":-0.40297, "vy":-1.95841, "omega":0.35111, "ax":0.00017, "ay":0.00032, "alpha":-7.44726, "fx":[33.81244,5.35994,-33.79406,-5.36676], "fy":[7.7885,34.30644,-7.8038,-34.26904]}, + {"t":11.15914, "x":2.85393, "y":5.51674, "heading":0.83451, "vx":-0.40296, "vy":-1.9584, "omega":0.08504, "ax":1.46379, "ay":7.14205, "alpha":-2.23813, "fx":[40.84857,25.24997,8.12129,25.37473], "fy":[120.3549,126.8043,122.53659,116.24117]}, + {"t":11.19486, "x":2.84047, "y":5.45133, "heading":0.83754, "vx":-0.35066, "vy":-1.70324, "omega":0.00508, "ax":1.95623, "ay":9.50251, "alpha":-0.05325, "fx":[33.80583,33.23846,32.74298,33.31262], "fy":[161.52691,161.64682,161.74232,161.62371]}, + {"t":11.23059, "x":2.82919, "y":5.39654, "heading":0.83773, "vx":-0.28077, "vy":-1.36374, "omega":0.00318, "ax":1.96232, "ay":9.53206, "alpha":-0.0305, "fx":[33.68397,33.35701,33.07255,33.40026], "fy":[162.07504,162.14329,162.19993,162.13212]}, + {"t":11.26632, "x":2.82041, "y":5.3539, "heading":0.83784, "vx":-0.21066, "vy":-1.02319, "omega":0.00209, "ax":1.96448, "ay":9.54205, "alpha":-0.02282, "fx":[33.64415,33.39899,33.18598,33.43157], "fy":[162.26049,162.31145,162.35432,162.30361]}, + {"t":11.30204, "x":2.81413, "y":5.32344, "heading":0.83791, "vx":-0.14048, "vy":-0.68228, "omega":0.00127, "ax":1.96564, "ay":9.54705, "alpha":-0.01896, "fx":[33.62533,33.42141,33.24437,33.44858], "fy":[162.35353,162.39583,162.43166,162.38953]}, + {"t":11.33777, "x":2.81037, "y":5.30515, "heading":0.83796, "vx":-0.07025, "vy":-0.34119, "omega":0.00059, "ax":1.9664, "ay":9.55005, "alpha":-0.01664, "fx":[33.61499,33.43594,33.28057,33.45985], "fy":[162.40933,162.44642,162.47797,162.441]}, + {"t":11.3735, "x":2.80911, "y":5.29906, "heading":0.83798, "vx":0.0, "vy":0.0, "omega":0.0, "ax":0.0, "ay":0.0, "alpha":0.0, "fx":[0.0,0.0,0.0,0.0], "fy":[0.0,0.0,0.0,0.0]}], + "splits":[0] + }, + "events":[] +} diff --git a/lib/examples/swerve/src/main/deploy/choreo/OUTPOST_1.traj b/lib/examples/swerve/src/main/deploy/choreo/OUTPOST_1.traj new file mode 100644 index 00000000..be0713c4 --- /dev/null +++ b/lib/examples/swerve/src/main/deploy/choreo/OUTPOST_1.traj @@ -0,0 +1,82 @@ +{ + "name":"OUTPOST_1", + "version":3, + "snapshot":{ + "waypoints":[ + {"x":3.690892219543457, "y":0.6445010900497437, "heading":3.1415922179358766, "intervals":24, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":0.4072994291782379, "y":0.6569390296936035, "heading":3.141592653589793, "intervals":34, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}], + "constraints":[ + {"from":"first", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"last", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"first", "to":"last", "data":{"type":"KeepInRectangle", "props":{"x":0.0, "y":0.0, "w":16.541, "h":8.0692}}, "enabled":true}], + "targetDt":0.05 + }, + "params":{ + "waypoints":[ + {"x":{"exp":"3.690892219543457 m", "val":3.690892219543457}, "y":{"exp":"0.6445010900497437 m", "val":0.6445010900497437}, "heading":{"exp":"3.1415922179358766 rad", "val":3.1415922179358766}, "intervals":24, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":{"exp":"0.4072994291782379 m", "val":0.4072994291782379}, "y":{"exp":"0.6569390296936035 m", "val":0.6569390296936035}, "heading":{"exp":"3.141592653589793 rad", "val":3.141592653589793}, "intervals":34, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}], + "constraints":[ + {"from":"first", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"last", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"first", "to":"last", "data":{"type":"KeepInRectangle", "props":{"x":{"exp":"0 m", "val":0.0}, "y":{"exp":"0 m", "val":0.0}, "w":{"exp":"16.541 m", "val":16.541}, "h":{"exp":"8.0692 m", "val":8.0692}}}, "enabled":true}], + "targetDt":{ + "exp":"0.05 s", + "val":0.05 + } + }, + "trajectory":{ + "config":{ + "frontLeft":{ + "x":0.225425, + "y":0.333375 + }, + "backLeft":{ + "x":-0.225425, + "y":0.333375 + }, + "mass":68.0388555, + "inertia":7.5, + "gearing":7.03, + "radius":0.0508, + "vmax":607.3745796940267, + "tmax":1.2, + "cof":1.5, + "bumper":{ + "front":0.34925, + "side":0.4445, + "back":0.34925 + }, + "differentialTrackWidth":0.5588 + }, + "sampleType":"Swerve", + "waypoints":[0.0,1.19873], + "samples":[ + {"t":0.0, "x":3.69089, "y":0.6445, "heading":3.14159, "vx":0.0, "vy":0.0, "omega":0.0, "ax":-9.75433, "ay":0.07475, "alpha":0.0, "fx":[-165.91833,-165.91833,-165.91833,-165.91833], "fy":[1.27151,1.27156,1.27156,1.27151]}, + {"t":0.04995, "x":3.67873, "y":0.64459, "heading":3.14159, "vx":-0.4872, "vy":0.00373, "omega":0.0, "ax":-9.75328, "ay":0.07192, "alpha":0.0, "fx":[-165.90055,-165.90055,-165.90055,-165.90055], "fy":[1.22324,1.22329,1.22329,1.22324]}, + {"t":0.09989, "x":3.64223, "y":0.64487, "heading":3.14159, "vx":-0.97435, "vy":0.00733, "omega":0.0, "ax":-9.75192, "ay":0.06906, "alpha":0.0, "fx":[-165.87735,-165.87735,-165.87735,-165.87735], "fy":[1.17462,1.17467,1.17467,1.17462]}, + {"t":0.14984, "x":3.5814, "y":0.64532, "heading":3.14159, "vx":-1.46143, "vy":0.01077, "omega":0.0, "ax":-9.75007, "ay":0.06617, "alpha":0.0, "fx":[-165.84591,-165.84591,-165.84591,-165.84591], "fy":[1.12545,1.12549,1.12549,1.12545]}, + {"t":0.19979, "x":3.49624, "y":0.64594, "heading":3.14159, "vx":-1.94842, "vy":0.01408, "omega":0.0, "ax":-9.74743, "ay":0.06322, "alpha":0.0, "fx":[-165.80096,-165.80096,-165.80096,-165.80096], "fy":[1.07532,1.07535,1.07535,1.07532]}, + {"t":0.24974, "x":3.38676, "y":0.64673, "heading":3.14159, "vx":-2.43527, "vy":0.01724, "omega":0.0, "ax":-9.74335, "ay":0.06017, "alpha":0.0, "fx":[-165.73164,-165.73164,-165.73164,-165.73164], "fy":[1.0234,1.02342,1.02342,1.0234]}, + {"t":0.29968, "x":3.25297, "y":0.64766, "heading":3.14159, "vx":-2.92192, "vy":0.02024, "omega":0.0, "ax":-9.73626, "ay":0.05689, "alpha":0.0, "fx":[-165.61106,-165.61106,-165.61106,-165.61106], "fy":[0.96763,0.96763,0.96763,0.96763]}, + {"t":0.34963, "x":3.09489, "y":0.64874, "heading":3.14159, "vx":-3.40822, "vy":0.02308, "omega":0.0, "ax":-9.72091, "ay":0.05298, "alpha":0.0, "fx":[-165.34993,-165.34993,-165.34993,-165.34993], "fy":[0.90115,0.90112,0.90112,0.90115]}, + {"t":0.39958, "x":2.91253, "y":0.64996, "heading":3.14159, "vx":-3.89375, "vy":0.02573, "omega":0.0, "ax":-9.66322, "ay":0.04586, "alpha":-0.00001, "fx":[-164.36854,-164.36854,-164.36854,-164.36854], "fy":[0.78019,0.78001,0.78002,0.78019]}, + {"t":0.44952, "x":2.706, "y":0.6513, "heading":3.14159, "vx":-4.3764, "vy":0.02802, "omega":0.0, "ax":-0.17953, "ay":-0.12526, "alpha":0.0, "fx":[-3.05381,-3.05381,-3.0538,-3.0538], "fy":[-2.13065,-2.13066,-2.13066,-2.13065]}, + {"t":0.49947, "x":2.48719, "y":0.65255, "heading":3.14159, "vx":-4.38537, "vy":0.02176, "omega":0.0, "ax":-0.00029, "ay":-0.05629, "alpha":0.0, "fx":[-0.00487,-0.00487,-0.00487,-0.00487], "fy":[-0.95753,-0.95753,-0.95753,-0.95753]}, + {"t":0.54942, "x":2.26815, "y":0.65356, "heading":3.14159, "vx":-4.38538, "vy":0.01895, "omega":0.0, "ax":-0.0002, "ay":-0.04883, "alpha":0.0, "fx":[-0.00347,-0.00347,-0.00347,-0.00347], "fy":[-0.83063,-0.83063,-0.83063,-0.83063]}, + {"t":0.59936, "x":2.04911, "y":0.65445, "heading":3.14159, "vx":-4.38539, "vy":0.01651, "omega":0.0, "ax":-0.00018, "ay":-0.04864, "alpha":0.0, "fx":[-0.00301,-0.00301,-0.00301,-0.00301], "fy":[-0.82741,-0.82741,-0.82741,-0.82741]}, + {"t":0.64931, "x":1.83007, "y":0.65521, "heading":3.14159, "vx":-4.3854, "vy":0.01408, "omega":0.0, "ax":-0.00015, "ay":-0.05553, "alpha":0.0, "fx":[-0.00255,-0.00255,-0.00255,-0.00255], "fy":[-0.94462,-0.94462,-0.94462,-0.94462]}, + {"t":0.69926, "x":1.61104, "y":0.65585, "heading":3.14159, "vx":-4.38541, "vy":0.01131, "omega":0.0, "ax":0.17841, "ay":-0.1233, "alpha":0.0, "fx":[3.03464,3.03464,3.03462,3.03462], "fy":[-2.09734,-2.09733,-2.09733,-2.09734]}, + {"t":0.74921, "x":1.39222, "y":0.65626, "heading":3.14159, "vx":-4.3765, "vy":0.00515, "omega":0.0, "ax":9.66334, "ay":-0.02688, "alpha":0.00001, "fx":[164.37059,164.37059,164.37059,164.37059], "fy":[-0.4573,-0.45713,-0.45713,-0.4573]}, + {"t":0.79915, "x":1.18568, "y":0.65648, "heading":3.14159, "vx":-3.89384, "vy":0.00381, "omega":0.0, "ax":9.72106, "ay":-0.02038, "alpha":0.0, "fx":[165.35252,165.35252,165.35252,165.35252], "fy":[-0.34672,-0.34669,-0.34669,-0.34672]}, + {"t":0.8491, "x":1.00332, "y":0.65665, "heading":3.14159, "vx":-3.4083, "vy":0.00279, "omega":0.0, "ax":9.73644, "ay":-0.0167, "alpha":0.0, "fx":[165.61401,165.61401,165.61401,165.61401], "fy":[-0.28404,-0.28405,-0.28405,-0.28404]}, + {"t":0.89905, "x":0.84523, "y":0.65677, "heading":3.14159, "vx":-2.922, "vy":0.00196, "omega":0.0, "ax":9.74355, "ay":-0.01358, "alpha":0.0, "fx":[165.73496,165.73496,165.73496,165.73496], "fy":[-0.2309,-0.23093,-0.23093,-0.2309]}, + {"t":0.94899, "x":0.71144, "y":0.65685, "heading":3.14159, "vx":-2.43534, "vy":0.00128, "omega":0.0, "ax":9.74764, "ay":-0.01066, "alpha":0.0, "fx":[165.80465,165.80465,165.80465,165.80465], "fy":[-0.1813,-0.18133,-0.18133,-0.1813]}, + {"t":0.99894, "x":0.60196, "y":0.6569, "heading":3.14159, "vx":-1.94847, "vy":0.00075, "omega":0.0, "ax":9.75031, "ay":-0.00784, "alpha":0.0, "fx":[165.84996,165.84996,165.84996,165.84996], "fy":[-0.13339,-0.13343,-0.13343,-0.13339]}, + {"t":1.04889, "x":0.5168, "y":0.65693, "heading":3.14159, "vx":-1.46147, "vy":0.00035, "omega":0.0, "ax":9.75218, "ay":-0.00508, "alpha":0.0, "fx":[165.88178,165.88178,165.88178,165.88178], "fy":[-0.08644,-0.08648,-0.08648,-0.08644]}, + {"t":1.09884, "x":0.45597, "y":0.65694, "heading":3.14159, "vx":-0.97438, "vy":0.0001, "omega":0.0, "ax":9.75357, "ay":-0.00236, "alpha":0.0, "fx":[165.90535,165.90535,165.90535,165.90535], "fy":[-0.04009,-0.04013,-0.04013,-0.04009]}, + {"t":1.14878, "x":0.41947, "y":0.65694, "heading":3.14159, "vx":-0.48722, "vy":-0.00002, "omega":0.0, "ax":9.75463, "ay":0.00034, "alpha":0.0, "fx":[165.92351,165.92351,165.92351,165.92351], "fy":[0.00586,0.00581,0.00581,0.00586]}, + {"t":1.19873, "x":0.4073, "y":0.65694, "heading":3.14159, "vx":0.0, "vy":0.0, "omega":0.0, "ax":0.0, "ay":0.0, "alpha":0.0, "fx":[0.0,0.0,0.0,0.0], "fy":[0.0,0.0,0.0,0.0]}], + "splits":[0] + }, + "events":[] +} diff --git a/lib/examples/swerve/src/main/deploy/choreo/OUTPOST_2.traj b/lib/examples/swerve/src/main/deploy/choreo/OUTPOST_2.traj new file mode 100644 index 00000000..d82e5f21 --- /dev/null +++ b/lib/examples/swerve/src/main/deploy/choreo/OUTPOST_2.traj @@ -0,0 +1,111 @@ +{ + "name":"OUTPOST_2", + "version":3, + "snapshot":{ + "waypoints":[ + {"x":0.4072994291782379, "y":0.6569390296936035, "heading":3.141592653589793, "intervals":24, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":1.6635223627090454, "y":1.6768428087234497, "heading":0.0, "intervals":27, "split":false, "fixTranslation":true, "fixHeading":false, "overrideIntervals":false}, + {"x":1.93715500831604, "y":3.816153049468994, "heading":1.5707963267948966, "intervals":25, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}], + "constraints":[ + {"from":"first", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"last", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"first", "to":"last", "data":{"type":"KeepInRectangle", "props":{"x":0.0, "y":0.0, "w":16.541, "h":8.0692}}, "enabled":true}], + "targetDt":0.05 + }, + "params":{ + "waypoints":[ + {"x":{"exp":"0.4072994291782379 m", "val":0.4072994291782379}, "y":{"exp":"0.6569390296936035 m", "val":0.6569390296936035}, "heading":{"exp":"3.141592653589793 rad", "val":3.141592653589793}, "intervals":24, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":{"exp":"1.6635223627090454 m", "val":1.6635223627090454}, "y":{"exp":"1.6768428087234497 m", "val":1.6768428087234497}, "heading":{"exp":"0 deg", "val":0.0}, "intervals":27, "split":false, "fixTranslation":true, "fixHeading":false, "overrideIntervals":false}, + {"x":{"exp":"1.93715500831604 m", "val":1.93715500831604}, "y":{"exp":"3.816153049468994 m", "val":3.816153049468994}, "heading":{"exp":"1.5707963267948966 rad", "val":1.5707963267948966}, "intervals":25, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}], + "constraints":[ + {"from":"first", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"last", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"first", "to":"last", "data":{"type":"KeepInRectangle", "props":{"x":{"exp":"0 m", "val":0.0}, "y":{"exp":"0 m", "val":0.0}, "w":{"exp":"16.541 m", "val":16.541}, "h":{"exp":"8.0692 m", "val":8.0692}}}, "enabled":true}], + "targetDt":{ + "exp":"0.05 s", + "val":0.05 + } + }, + "trajectory":{ + "config":{ + "frontLeft":{ + "x":0.225425, + "y":0.333375 + }, + "backLeft":{ + "x":-0.225425, + "y":0.333375 + }, + "mass":68.0388555, + "inertia":7.5, + "gearing":7.03, + "radius":0.0508, + "vmax":607.3745796940267, + "tmax":1.2, + "cof":1.5, + "bumper":{ + "front":0.34925, + "side":0.4445, + "back":0.34925 + }, + "differentialTrackWidth":0.5588 + }, + "sampleType":"Swerve", + "waypoints":[0.0,0.62176,1.36669], + "samples":[ + {"t":0.0, "x":0.4073, "y":0.65694, "heading":3.14159, "vx":0.0, "vy":0.0, "omega":0.0, "ax":7.77717, "ay":4.27371, "alpha":-9.49132, "fx":[60.69069,159.57563,165.55075,143.33265], "fy":[154.2335,42.83598,10.18965,83.51943]}, + {"t":0.02591, "x":0.40991, "y":0.65837, "heading":3.14159, "vx":0.20148, "vy":0.11072, "omega":-0.24589, "ax":7.7829, "ay":4.33302, "alpha":-9.30566, "fx":[62.25925,158.6326,165.45658,143.19121], "fy":[153.5832,46.09709,11.39773,83.73551]}, + {"t":0.05181, "x":0.41774, "y":0.6627, "heading":3.13522, "vx":0.40311, "vy":0.22297, "omega":-0.48697, "ax":7.78577, "ay":4.40975, "alpha":-9.0904, "fx":[64.11681,157.0168,165.36648,143.23464], "fy":[152.79141,51.21555,12.39646,83.6309]}, + {"t":0.07772, "x":0.4308, "y":0.66995, "heading":3.12261, "vx":0.60482, "vy":0.33722, "omega":-0.72247, "ax":7.784, "ay":4.50084, "alpha":-8.86245, "fx":[66.25622,154.63762,165.27768,143.44267], "fy":[151.84787,57.88755,13.25842,83.2379]}, + {"t":0.10363, "x":0.44908, "y":0.6802, "heading":3.10389, "vx":0.80648, "vy":0.45382, "omega":-0.95207, "ax":7.7759, "ay":4.60277, "alpha":-8.64099, "fx":[68.68086,151.41134,165.18306,143.788], "fy":[150.73533,65.75389,14.08072,82.59718]}, + {"t":0.12953, "x":0.47258, "y":0.6935, "heading":3.07922, "vx":1.00793, "vy":0.57306, "omega":-1.17593, "ax":7.76066, "ay":4.71148, "alpha":-8.44347, "fx":[71.40842,147.31314,165.06964,144.23518], "fy":[149.42643,74.37862,14.99682,81.76155]}, + {"t":0.15544, "x":0.5013, "y":0.70993, "heading":3.04876, "vx":1.20898, "vy":0.69512, "omega":-1.39468, "ax":7.73909, "ay":4.82271, "alpha":-8.27952, "fx":[74.47638,142.42952,164.91506,144.73789], "fy":[147.8785,83.25301,16.1974,80.80299]}, + {"t":0.18135, "x":0.53522, "y":0.72955, "heading":3.01263, "vx":1.40948, "vy":0.82006, "omega":-1.60917, "ax":7.71412, "ay":4.93316, "alpha":-8.14257, "fx":[77.95028,136.99801,164.67879,145.2328], "fy":[146.02475,91.82775,17.96812,79.82621]}, + {"t":0.20725, "x":0.57432, "y":0.75246, "heading":2.97094, "vx":1.60932, "vy":0.94787, "omega":-1.82012, "ax":7.69066, "ay":5.04236, "alpha":-7.99924, "fx":[81.93759,131.41981,164.28049,145.6261], "fy":[143.75837,99.56213,20.76018,78.99546]}, + {"t":0.23316, "x":0.61859, "y":0.7787, "heading":2.92379, "vx":1.80857, "vy":1.0785, "omega":-2.02736, "ax":7.67443, "ay":5.15567, "alpha":-7.7745, "fx":[86.6128,126.241,163.5431,145.76245], "fy":[140.90154,105.96525,25.32803,78.59109]}, + {"t":0.25907, "x":0.66802, "y":0.80837, "heading":2.87126, "vx":2.00739, "vy":1.21207, "omega":-2.22877, "ax":7.66855, "ay":5.28941, "alpha":-7.32432, "fx":[92.26812,122.11431,162.03054,145.34656], "fy":[137.13784,110.59971,33.00935,79.13833]}, + {"t":0.28498, "x":0.7226, "y":0.84155, "heading":2.81352, "vx":2.20605, "vy":1.3491, "omega":-2.41852, "ax":7.66388, "ay":5.4807, "alpha":-6.37164, "fx":[99.42833,119.76473,158.53737,143.71106], "fy":[131.84361,113.02239,46.29696,81.73754]}, + {"t":0.31088, "x":0.78232, "y":0.87834, "heading":2.75087, "vx":2.4046, "vy":1.49108, "omega":-2.58359, "ax":7.60468, "ay":5.80652, "alpha":-4.33868, "fx":[109.14167,119.97839,149.35139,138.94221], "fy":[123.58506,112.63713,69.7856,89.06148]}, + {"t":0.33679, "x":0.84717, "y":0.91892, "heading":2.68393, "vx":2.60162, "vy":1.64151, "omega":-2.69599, "ax":7.25268, "ay":6.3889, "alpha":0.08665, "fx":[123.77637,123.60167,122.95623,123.12999], "fy":[108.20511,108.4146,109.14004,108.93398]}, + {"t":0.3627, "x":0.91701, "y":0.96359, "heading":2.61409, "vx":2.78951, "vy":1.80703, "omega":-2.69375, "ax":5.63806, "ay":7.01338, "alpha":10.52124, "fx":[147.9122,131.29068,59.79707,44.60698], "fy":[69.23918,98.58209,152.7597,156.60157]}, + {"t":0.3886, "x":0.99117, "y":1.01276, "heading":2.5443, "vx":2.93557, "vy":1.98873, "omega":-2.42118, "ax":4.11035, "ay":6.95462, "alpha":16.56456, "fx":[152.76319,130.7319,26.94712,-30.77847], "fy":[53.4785,98.64773,161.63738,159.42072]}, + {"t":0.41451, "x":1.0686, "y":1.06661, "heading":2.48158, "vx":3.04206, "vy":2.1689, "omega":-1.99204, "ax":3.0681, "ay":7.29683, "alpha":16.76076, "fx":[141.24708,121.62593,7.83578,-61.95879], "fy":[74.0591,108.78012,163.46856,150.1601]}, + {"t":0.44042, "x":1.14844, "y":1.12525, "heading":2.42997, "vx":3.12155, "vy":2.35794, "omega":-1.55782, "ax":1.23006, "ay":7.72831, "alpha":15.30137, "fx":[99.6387,106.36278,-22.4584,-99.85152], "fy":[112.57909,122.60625,162.0271,128.61297]}, + {"t":0.46632, "x":1.22972, "y":1.18893, "heading":2.38961, "vx":3.15341, "vy":2.55815, "omega":-1.16141, "ax":-2.29365, "ay":8.29783, "alpha":9.738, "fx":[-67.54971,64.59442,-43.54905,-109.5533], "fy":[138.05052,148.04881,157.62962,120.84622]}, + {"t":0.49223, "x":1.31064, "y":1.25799, "heading":2.35952, "vx":3.09399, "vy":2.77312, "omega":-0.90913, "ax":-4.27522, "ay":7.87841, "alpha":7.60081, "fx":[-115.57049,5.34869,-63.69191,-116.96742], "fy":[109.70683,161.37124,150.76111,114.19865]}, + {"t":0.51814, "x":1.38936, "y":1.33248, "heading":2.33597, "vx":2.98323, "vy":2.97723, "omega":-0.71222, "ax":-5.88777, "ay":7.0464, "alpha":6.193, "fx":[-139.59493,-48.75622,-84.18922,-128.05675], "fy":[81.70741,154.53197,140.76161,102.42833]}, + {"t":0.54404, "x":1.46468, "y":1.41197, "heading":2.31752, "vx":2.8307, "vy":3.15978, "omega":-0.55177, "ax":-6.70056, "ay":6.63914, "alpha":4.15368, "fx":[-140.98492,-86.32496,-97.73019,-130.85845], "fy":[82.01704,138.2949,132.05453,99.35312]}, + {"t":0.56995, "x":1.53576, "y":1.49606, "heading":2.30322, "vx":2.65711, "vy":3.33178, "omega":-0.44417, "ax":-7.2751, "ay":6.18766, "alpha":2.99487, "fx":[-142.85827,-108.59245,-109.41701,-134.12203], "fy":[80.2549,122.54395,122.86015,95.34256]}, + {"t":0.59586, "x":1.60216, "y":1.58445, "heading":2.29172, "vx":2.46863, "vy":3.49208, "omega":-0.36658, "ax":-7.71643, "ay":5.72882, "alpha":2.33817, "fx":[-145.44838,-122.57096,-119.18988,-137.80816], "fy":[76.47321,109.28884,113.70177,90.31867]}, + {"t":0.62176, "x":1.66352, "y":1.67684, "heading":2.28222, "vx":2.26872, "vy":3.6405, "omega":-0.306, "ax":-7.99224, "ay":5.34776, "alpha":2.34764, "fx":[-149.33006,-129.18991,-124.00736,-141.25586], "fy":[68.85107,101.52966,108.51767,84.9574]}, + {"t":0.64935, "x":1.72307, "y":1.77932, "heading":2.27378, "vx":2.04822, "vy":3.78804, "omega":-0.24123, "ax":-8.21166, "ay":4.95431, "alpha":2.50031, "fx":[-152.73824,-134.22213,-127.53763,-144.21369], "fy":[59.92593,94.02474,103.86978,79.26533]}, + {"t":0.67694, "x":1.77646, "y":1.88572, "heading":2.26712, "vx":1.82166, "vy":3.92473, "omega":-0.17225, "ax":-8.47224, "ay":4.45653, "alpha":2.34695, "fx":[-154.90895,-140.78016,-133.45917,-147.29332], "fy":[52.59904,82.83765,95.35474,72.4259]}, + {"t":0.70453, "x":1.82349, "y":1.9957, "heading":2.26237, "vx":1.58791, "vy":4.04769, "omega":-0.1075, "ax":-8.77187, "ay":3.79511, "alpha":1.7918, "fx":[-156.01909,-147.94488,-142.10126,-150.76255], "fy":[46.97871,67.67217,80.4535,63.11084]}, + {"t":0.73212, "x":1.86397, "y":2.10882, "heading":2.2594, "vx":1.34589, "vy":4.15239, "omega":-0.05806, "ax":-9.04267, "ay":2.97324, "alpha":0.92574, "fx":[-156.47882,-153.56139,-150.99668,-154.2159], "fy":[41.7564,50.93668,59.09401,50.5089]}, + {"t":0.75971, "x":1.89766, "y":2.22451, "heading":2.2578, "vx":1.09641, "vy":4.23443, "omega":-0.03252, "ax":-9.16617, "ay":2.19665, "alpha":0.31402, "fx":[-156.55778,-155.85699,-155.24971,-155.99093], "fy":[34.47372,37.18522,40.23507,37.56347]}, + {"t":0.7873, "x":1.92442, "y":2.34218, "heading":2.2569, "vx":0.84351, "vy":4.29503, "omega":-0.02385, "ax":-9.12235, "ay":1.55405, "alpha":0.11036, "fx":[-155.31958,-155.13701,-155.01515,-155.20253], "fy":[25.45586,26.30322,27.41073,26.56584]}, + {"t":0.81489, "x":1.94422, "y":2.46127, "heading":2.25625, "vx":0.59183, "vy":4.33791, "omega":-0.02081, "ax":-8.88343, "ay":0.93286, "alpha":-0.05759, "fx":[-151.06209,-151.13811,-151.14645,-151.07153], "fy":[16.35079,15.96315,15.38437,15.77215]}, + {"t":0.84248, "x":1.95717, "y":2.58131, "heading":2.25567, "vx":0.34673, "vy":4.36364, "omega":-0.0224, "ax":-8.05529, "ay":-1.50505, "alpha":-4.86941, "fx":[-141.84265,-145.49045,-131.19864,-129.54129], "fy":[9.37723,-12.62806,-56.89348,-42.25761]}, + {"t":0.87007, "x":1.96367, "y":2.70113, "heading":2.25505, "vx":0.12449, "vy":4.32212, "omega":-0.15675, "ax":-3.09027, "ay":-6.58686, "alpha":-16.57932, "fx":[-124.68132,-128.67674,-17.85233,60.95195], "fy":[-55.92677,-89.42926,-158.28971,-144.51694]}, + {"t":0.89766, "x":1.96593, "y":2.81787, "heading":2.25073, "vx":0.03923, "vy":4.14039, "omega":-0.61417, "ax":-2.24404, "ay":-6.8673, "alpha":-17.05268, "fx":[-112.98053,-126.80477,0.59621,86.50708], "fy":[-69.31018,-98.63212,-162.65069,-136.65058]}, + {"t":0.92525, "x":1.96615, "y":2.92949, "heading":2.23378, "vx":-0.02269, "vy":3.95092, "omega":-1.08465, "ax":-1.94074, "ay":-6.92253, "alpha":-17.07797, "fx":[-101.17933,-128.1769,4.5931,92.7175], "fy":[-73.37593,-99.5066,-163.73689,-134.38149]}, + {"t":0.95284, "x":1.96479, "y":3.03586, "heading":2.20386, "vx":-0.07623, "vy":3.75993, "omega":-1.55583, "ax":-1.4001, "ay":-6.93119, "alpha":-16.93117, "fx":[-62.89761,-130.74453,4.28005,94.10089], "fy":[-75.41779,-97.52727,-164.31766,-134.32749]}, + {"t":0.98043, "x":1.96215, "y":3.13695, "heading":2.16093, "vx":-0.11486, "vy":3.5687, "omega":-2.02296, "ax":-0.03003, "ay":-9.16966, "alpha":-7.12956, "fx":[9.19421,-75.65058,2.74228,61.67093], "fy":[-161.67353,-145.22742,-164.60588,-152.38657]}, + {"t":1.00802, "x":1.95897, "y":3.23193, "heading":2.10512, "vx":-0.11569, "vy":3.3157, "omega":-2.21967, "ax":0.3067, "ay":-9.66915, "alpha":-0.86889, "fx":[6.2934,-3.81708,4.44671,13.94479], "fy":[-164.46639,-164.59411,-164.69429,-164.12331]}, + {"t":1.03561, "x":1.9559, "y":3.31973, "heading":2.04388, "vx":-0.10723, "vy":3.04893, "omega":-2.24364, "ax":0.30834, "ay":-9.6568, "alpha":2.01223, "fx":[2.4602,24.96071,10.15981,-16.60184], "fy":[-165.14749,-163.2164,-164.54891,-164.12495]}, + {"t":1.0632, "x":1.95306, "y":3.40017, "heading":1.98198, "vx":-0.09872, "vy":2.7825, "omega":-2.18812, "ax":0.27341, "ay":-9.57609, "alpha":3.60313, "fx":[-1.66261,37.20008,19.0941,-36.02931], "fy":[-165.40956,-161.12975,-163.85257,-161.15446]}, + {"t":1.09079, "x":1.95044, "y":3.47329, "heading":1.92161, "vx":-0.09118, "vy":2.5183, "omega":-2.08871, "ax":0.26339, "ay":-9.48721, "alpha":4.68297, "fx":[-5.81563,42.68919,30.03949,-48.99242], "fy":[-165.45644,-159.92117,-162.29994,-157.82148]}, + {"t":1.11838, "x":1.94802, "y":3.53916, "heading":1.86398, "vx":-0.08391, "vy":2.25655, "omega":-1.95951, "ax":0.27654, "ay":-9.397, "alpha":5.54878, "fx":[-9.86087,44.97026,41.73205,-58.02604], "fy":[-165.35446,-159.39897,-159.7893,-154.81854]}, + {"t":1.14597, "x":1.94581, "y":3.59784, "heading":1.80992, "vx":-0.07628, "vy":1.99728, "omega":-1.80642, "ax":0.30049, "ay":-9.30672, "alpha":6.30936, "fx":[-13.71151,45.57312,53.0917,-64.50843], "fy":[-165.14197,-159.30078,-156.47611,-152.29961]}, + {"t":1.17356, "x":1.94382, "y":3.64941, "heading":1.76008, "vx":-0.06799, "vy":1.74051, "omega":-1.63234, "ax":0.32482, "ay":-9.21884, "alpha":6.99365, "fx":[-17.30502,45.26708,63.38087,-69.24229], "fy":[-164.84984,-159.44175,-152.68834,-150.25952]}, + {"t":1.20115, "x":1.94207, "y":3.69392, "heading":1.71504, "vx":-0.05903, "vy":1.48616, "omega":-1.43939, "ax":0.3438, "ay":-9.13647, "alpha":7.60217, "fx":[-20.59228,44.48911,72.23601,-72.74082], "fy":[-164.50656,-159.70149,-148.78706,-148.64005]}, + {"t":1.22874, "x":1.94057, "y":3.73145, "heading":1.67533, "vx":-0.04954, "vy":1.23409, "omega":-1.22965, "ax":0.35571, "ay":-9.06194, "alpha":8.13179, "fx":[-23.53336,43.51245,79.59079,-75.36778], "fy":[-164.13938,-160.00239,-145.06118,-147.36107]}, + {"t":1.25633, "x":1.93934, "y":3.76205, "heading":1.6414, "vx":-0.03973, "vy":0.98407, "omega":-1.00529, "ax":0.36124, "ay":-8.99617, "alpha":8.58475, "fx":[-26.09556,42.52071,85.55948,-77.40646], "fy":[-163.77417,-160.29447,-141.68914,-146.33146]}, + {"t":1.28392, "x":1.93838, "y":3.78577, "heading":1.61367, "vx":-0.02976, "vy":0.73586, "omega":-0.76844, "ax":0.36207, "ay":-8.9388, "alpha":8.96948, "fx":[-28.25229,41.64372,90.33551,-79.09244], "fy":[-163.43475,-160.54552,-138.7504,-145.4552]}, + {"t":1.31151, "x":1.9377, "y":3.80267, "heading":1.59247, "vx":-0.01977, "vy":0.48924, "omega":-0.52097, "ax":0.36, "ay":-8.88861, "alpha":9.29824, "fx":[-29.98185,40.97657,94.12526,-80.62628], "fy":[-163.14231,-160.7345,-136.25697,-144.63713]}, + {"t":1.3391, "x":1.93729, "y":3.81279, "heading":1.57809, "vx":-0.00984, "vy":0.24401, "omega":-0.26443, "ax":0.3566, "ay":-8.84401, "alpha":9.58431, "fx":[-31.26606,40.59054,97.11279,-82.17482], "fy":[-162.91488,-160.84708,-134.18526,-143.7889]}, + {"t":1.36669, "x":1.93716, "y":3.81615, "heading":1.5708, "vx":0.0, "vy":0.0, "omega":0.0, "ax":0.0, "ay":0.0, "alpha":0.0, "fx":[0.0,0.0,0.0,0.0], "fy":[0.0,0.0,0.0,0.0]}], + "splits":[0] + }, + "events":[] +} diff --git a/lib/examples/swerve/src/main/deploy/choreo/OUTPOST_DEPOT.traj b/lib/examples/swerve/src/main/deploy/choreo/OUTPOST_DEPOT.traj new file mode 100644 index 00000000..7651266b --- /dev/null +++ b/lib/examples/swerve/src/main/deploy/choreo/OUTPOST_DEPOT.traj @@ -0,0 +1,193 @@ +{ + "name":"OUTPOST_DEPOT", + "version":3, + "snapshot":{ + "waypoints":[ + {"x":0.4072994291782379, "y":0.6569390296936035, "heading":3.141592653589793, "intervals":17, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":1.6635223627090454, "y":1.6768428087234497, "heading":0.0, "intervals":22, "split":false, "fixTranslation":true, "fixHeading":false, "overrideIntervals":false}, + {"x":1.8984053134918213, "y":4.315596580505371, "heading":0.0, "intervals":18, "split":false, "fixTranslation":true, "fixHeading":false, "overrideIntervals":false}, + {"x":1.2030097246170044, "y":5.9760308265686035, "heading":3.141592653589793, "intervals":18, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":0.42246365547180176, "y":5.961839199066162, "heading":-3.118608391365919, "intervals":16, "split":false, "fixTranslation":true, "fixHeading":false, "overrideIntervals":false}, + {"x":1.8416383266448977, "y":5.9760308265686035, "heading":0.0, "intervals":30, "split":false, "fixTranslation":true, "fixHeading":false, "overrideIntervals":false}, + {"x":1.9835559129714968, "y":3.8046934604644775, "heading":1.591626654119535, "intervals":40, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}], + "constraints":[ + {"from":"first", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"last", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"first", "to":"last", "data":{"type":"KeepInRectangle", "props":{"x":0.0, "y":0.0, "w":16.541, "h":8.0692}}, "enabled":true}, + {"from":3, "to":4, "data":{"type":"MaxVelocity", "props":{"max":1.0}}, "enabled":true}, + {"from":3, "to":5, "data":{"type":"MaxAngularVelocity", "props":{"max":0.0}}, "enabled":true}], + "targetDt":0.05 + }, + "params":{ + "waypoints":[ + {"x":{"exp":"0.4072994291782379 m", "val":0.4072994291782379}, "y":{"exp":"0.6569390296936035 m", "val":0.6569390296936035}, "heading":{"exp":"3.141592653589793 rad", "val":3.141592653589793}, "intervals":17, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":{"exp":"1.6635223627090454 m", "val":1.6635223627090454}, "y":{"exp":"1.6768428087234497 m", "val":1.6768428087234497}, "heading":{"exp":"0 deg", "val":0.0}, "intervals":22, "split":false, "fixTranslation":true, "fixHeading":false, "overrideIntervals":false}, + {"x":{"exp":"1.8984053134918213 m", "val":1.8984053134918213}, "y":{"exp":"4.315596580505371 m", "val":4.315596580505371}, "heading":{"exp":"0 deg", "val":0.0}, "intervals":18, "split":false, "fixTranslation":true, "fixHeading":false, "overrideIntervals":false}, + {"x":{"exp":"1.2030097246170044 m", "val":1.2030097246170044}, "y":{"exp":"5.9760308265686035 m", "val":5.9760308265686035}, "heading":{"exp":"3.141592653589793 rad", "val":3.141592653589793}, "intervals":18, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":{"exp":"0.42246365547180176 m", "val":0.42246365547180176}, "y":{"exp":"5.961839199066162 m", "val":5.961839199066162}, "heading":{"exp":"-3.118608391365919 rad", "val":-3.118608391365919}, "intervals":16, "split":false, "fixTranslation":true, "fixHeading":false, "overrideIntervals":false}, + {"x":{"exp":"1.8416383266448975 m", "val":1.8416383266448977}, "y":{"exp":"5.9760308265686035 m", "val":5.9760308265686035}, "heading":{"exp":"0 deg", "val":0.0}, "intervals":30, "split":false, "fixTranslation":true, "fixHeading":false, "overrideIntervals":false}, + {"x":{"exp":"1.9835559129714966 m", "val":1.9835559129714968}, "y":{"exp":"3.8046934604644775 m", "val":3.8046934604644775}, "heading":{"exp":"1.591626654119535 rad", "val":1.591626654119535}, "intervals":40, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}], + "constraints":[ + {"from":"first", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"last", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"first", "to":"last", "data":{"type":"KeepInRectangle", "props":{"x":{"exp":"0 m", "val":0.0}, "y":{"exp":"0 m", "val":0.0}, "w":{"exp":"16.541 m", "val":16.541}, "h":{"exp":"8.0692 m", "val":8.0692}}}, "enabled":true}, + {"from":3, "to":4, "data":{"type":"MaxVelocity", "props":{"max":{"exp":"1 m / s", "val":1.0}}}, "enabled":true}, + {"from":3, "to":5, "data":{"type":"MaxAngularVelocity", "props":{"max":{"exp":"0 rad / s", "val":0.0}}}, "enabled":true}], + "targetDt":{ + "exp":"0.05 s", + "val":0.05 + } + }, + "trajectory":{ + "config":{ + "frontLeft":{ + "x":0.225425, + "y":0.333375 + }, + "backLeft":{ + "x":-0.225425, + "y":0.333375 + }, + "mass":68.0388555, + "inertia":7.5, + "gearing":7.03, + "radius":0.0508, + "vmax":607.3745796940267, + "tmax":1.2, + "cof":1.5, + "bumper":{ + "front":0.34925, + "side":0.4445, + "back":0.34925 + }, + "differentialTrackWidth":0.5588 + }, + "sampleType":"Swerve", + "waypoints":[0.0,0.59907,1.21595,1.76993,2.60807,3.24518,4.1111], + "samples":[ + {"t":0.0, "x":0.4073, "y":0.65694, "heading":3.14159, "vx":0.0, "vy":0.0, "omega":0.0, "ax":8.45162, "ay":4.86406, "alpha":0.0, "fx":[143.75966,143.75966,143.75966,143.75966], "fy":[82.73623,82.73623,82.73623,82.73623]}, + {"t":0.03524, "x":0.41255, "y":0.65996, "heading":3.14159, "vx":0.29783, "vy":0.17141, "omega":0.0, "ax":8.41694, "ay":4.92189, "alpha":0.0, "fx":[143.16972,143.16972,143.16972,143.16972], "fy":[83.72001,83.72001,83.72001,83.72001]}, + {"t":0.07048, "x":0.42827, "y":0.66906, "heading":3.14159, "vx":0.59444, "vy":0.34485, "omega":0.0, "ax":8.37543, "ay":4.98995, "alpha":0.0, "fx":[142.46362,142.46362,142.46362,142.46362], "fy":[84.87754,84.87754,84.87754,84.87754]}, + {"t":0.10572, "x":0.45442, "y":0.68431, "heading":3.14159, "vx":0.88959, "vy":0.5207, "omega":0.0, "ax":8.32478, "ay":5.07131, "alpha":0.0, "fx":[141.60221,141.60221,141.60221,141.60221], "fy":[86.26152,86.26152,86.26152,86.26152]}, + {"t":0.14096, "x":0.49093, "y":0.7058, "heading":3.14159, "vx":1.18295, "vy":0.69941, "omega":0.0, "ax":8.2616, "ay":5.1704, "alpha":0.0, "fx":[140.52744,140.52744,140.52744,140.52744], "fy":[87.94699,87.94699,87.94699,87.94699]}, + {"t":0.1762, "x":0.53775, "y":0.73366, "heading":3.14159, "vx":1.47408, "vy":0.88161, "omega":0.0, "ax":8.1806, "ay":5.2937, "alpha":0.0, "fx":[139.14967,139.14967,139.14967,139.14967], "fy":[90.04435,90.04435,90.04435,90.04435]}, + {"t":0.21144, "x":0.59478, "y":0.76802, "heading":3.14159, "vx":1.76236, "vy":1.06816, "omega":0.0, "ax":8.07321, "ay":5.45118, "alpha":0.0, "fx":[137.32291,137.32291,137.32291,137.32291], "fy":[92.72304,92.72304,92.72304,92.72304]}, + {"t":0.24668, "x":0.66189, "y":0.80904, "heading":3.14159, "vx":2.04686, "vy":1.26025, "omega":0.0, "ax":7.92448, "ay":5.65892, "alpha":0.0, "fx":[134.79316,134.79316,134.79316,134.79316], "fy":[96.25653,96.25653,96.25653,96.25653]}, + {"t":0.28192, "x":0.73894, "y":0.85697, "heading":3.14159, "vx":2.32611, "vy":1.45967, "omega":0.0, "ax":7.70615, "ay":5.94444, "alpha":0.0, "fx":[131.07942,131.07942,131.07942,131.07942], "fy":[101.11326,101.11326,101.11326,101.11326]}, + {"t":0.31716, "x":0.8257, "y":0.9121, "heading":3.14159, "vx":2.59768, "vy":1.66915, "omega":0.0, "ax":7.35835, "ay":6.3583, "alpha":0.0, "fx":[125.16336,125.16336,125.16336,125.16336], "fy":[108.15285,108.15285,108.15285,108.15285]}, + {"t":0.3524, "x":0.92181, "y":0.97486, "heading":-3.14159, "vx":2.85698, "vy":1.89321, "omega":0.0, "ax":6.73347, "ay":6.9996, "alpha":0.0, "fx":[114.53434,114.53434,114.53434,114.53434], "fy":[119.06121,119.06121,119.06121,119.06121]}, + {"t":0.38763, "x":1.02667, "y":1.04593, "heading":-3.14159, "vx":3.09426, "vy":2.13988, "omega":0.0, "ax":5.38512, "ay":8.05623, "alpha":0.0, "fx":[91.59934,91.59934,91.59934,91.59934], "fy":[137.03415,137.03415,137.03415,137.03415]}, + {"t":0.42287, "x":1.13905, "y":1.12634, "heading":-3.14159, "vx":3.28403, "vy":2.42377, "omega":0.0, "ax":1.72721, "ay":9.49302, "alpha":0.0, "fx":[29.37929,29.37929,29.37929,29.37929], "fy":[161.47351,161.47351,161.47351,161.47351]}, + {"t":0.45811, "x":1.25585, "y":1.21764, "heading":-3.14159, "vx":3.3449, "vy":2.7583, "omega":0.0, "ax":-5.36969, "ay":7.99104, "alpha":0.0, "fx":[-91.33685,-91.33685,-91.33685,-91.33685], "fy":[135.92529,135.92529,135.92529,135.92529]}, + {"t":0.49335, "x":1.37039, "y":1.31981, "heading":-3.14159, "vx":3.15567, "vy":3.0399, "omega":0.0, "ax":-6.95017, "ay":6.70502, "alpha":0.0, "fx":[-118.22046,-118.22046,-118.22046,-118.22046], "fy":[114.05052,114.05052,114.05052,114.05052]}, + {"t":0.52859, "x":1.47728, "y":1.43109, "heading":-3.14159, "vx":2.91075, "vy":3.27619, "omega":0.0, "ax":-7.49246, "ay":6.12989, "alpha":0.0, "fx":[-127.4446,-127.4446,-127.4446,-127.4446], "fy":[104.2676,104.2676,104.2676,104.2676]}, + {"t":0.56383, "x":1.5752, "y":1.55035, "heading":-3.14159, "vx":2.64672, "vy":3.4922, "omega":0.0, "ax":-7.97277, "ay":5.52057, "alpha":0.0, "fx":[-135.6146,-135.6146,-135.6146,-135.6146], "fy":[93.90327,93.90327,93.90327,93.90327]}, + {"t":0.59907, "x":1.66352, "y":1.67684, "heading":-3.14159, "vx":2.36577, "vy":3.68674, "omega":0.0, "ax":-8.31322, "ay":4.96267, "alpha":0.0, "fx":[-141.40541,-141.40541,-141.40541,-141.40541], "fy":[84.41356,84.41356,84.41356,84.41356]}, + {"t":0.62711, "x":1.72659, "y":1.78217, "heading":-3.14159, "vx":2.13267, "vy":3.82589, "omega":0.0, "ax":-8.57173, "ay":4.46051, "alpha":0.0, "fx":[-145.80271,-145.80271,-145.80271,-145.80271], "fy":[75.87199,75.87199,75.87199,75.87199]}, + {"t":0.65515, "x":1.78302, "y":1.8912, "heading":-3.14159, "vx":1.89232, "vy":3.95097, "omega":0.0, "ax":-8.80711, "ay":3.91052, "alpha":0.0, "fx":[-149.80637,-149.80637,-149.80637,-149.80637], "fy":[66.51683,66.51683,66.51683,66.51683]}, + {"t":0.68319, "x":1.83262, "y":2.00352, "heading":-3.14159, "vx":1.64537, "vy":4.06062, "omega":0.0, "ax":-8.99611, "ay":3.34374, "alpha":0.0, "fx":[-153.02129,-153.02129,-153.02129,-153.02129], "fy":[56.87604,56.87604,56.87604,56.87604]}, + {"t":0.71123, "x":1.87522, "y":2.11869, "heading":-3.14159, "vx":1.39312, "vy":4.15437, "omega":0.0, "ax":-9.12793, "ay":2.76628, "alpha":0.0, "fx":[-155.2634,-155.2634,-155.2634,-155.2634], "fy":[47.05356,47.05356,47.05356,47.05356]}, + {"t":0.73927, "x":1.91069, "y":2.23627, "heading":-3.14159, "vx":1.13717, "vy":4.23194, "omega":0.0, "ax":-9.18499, "ay":2.18198, "alpha":0.0, "fx":[-156.23411,-156.23411,-156.23411,-156.23411], "fy":[37.11488,37.11488,37.11488,37.11488]}, + {"t":0.76731, "x":1.93897, "y":2.35579, "heading":-3.14159, "vx":0.87963, "vy":4.29312, "omega":0.0, "ax":-9.12966, "ay":1.59664, "alpha":0.0, "fx":[-155.29292,-155.29292,-155.29292,-155.29292], "fy":[27.15832,27.15832,27.15832,27.15832]}, + {"t":0.79535, "x":1.96004, "y":2.4768, "heading":-3.14159, "vx":0.62363, "vy":4.33789, "omega":0.0, "ax":-8.87067, "ay":1.02203, "alpha":0.0, "fx":[-150.88763,-150.88763,-150.88763,-150.88763], "fy":[17.38443,17.38443,17.38443,17.38443]}, + {"t":0.82339, "x":1.97404, "y":2.59883, "heading":-3.14159, "vx":0.3749, "vy":4.36655, "omega":0.0, "ax":-8.16454, "ay":0.48884, "alpha":0.0, "fx":[-138.87649,-138.87649,-138.87649,-138.87649], "fy":[8.31511,8.31511,8.31511,8.31511]}, + {"t":0.85143, "x":1.98134, "y":2.72146, "heading":-3.14159, "vx":0.14597, "vy":4.38026, "omega":0.0, "ax":-6.42487, "ay":0.08318, "alpha":0.0, "fx":[-109.28525,-109.28525,-109.28525,-109.28525], "fy":[1.41483,1.41483,1.41483,1.41483]}, + {"t":0.87947, "x":1.98291, "y":2.84432, "heading":-3.14159, "vx":-0.03418, "vy":4.38259, "omega":0.0, "ax":-3.47135, "ay":-0.06529, "alpha":0.0, "fx":[-59.04674,-59.04674,-59.04674,-59.04674], "fy":[-1.11055,-1.11055,-1.11055,-1.11055]}, + {"t":0.90751, "x":1.98059, "y":2.96718, "heading":3.14159, "vx":-0.13152, "vy":4.38076, "omega":0.0, "ax":-1.30676, "ay":-0.04465, "alpha":0.0, "fx":[-22.22754,-22.22754,-22.22754,-22.22754], "fy":[-0.75944,-0.75944,-0.75944,-0.75944]}, + {"t":0.93555, "x":1.97639, "y":3.08999, "heading":3.14159, "vx":-0.16816, "vy":4.37951, "omega":0.0, "ax":-0.45001, "ay":-0.01793, "alpha":0.0, "fx":[-7.65448,-7.65448,-7.65448,-7.65448], "fy":[-0.30491,-0.30491,-0.30491,-0.30491]}, + {"t":0.96359, "x":1.97149, "y":3.21279, "heading":3.14159, "vx":-0.18078, "vy":4.379, "omega":0.0, "ax":-0.16431, "ay":-0.00687, "alpha":0.0, "fx":[-2.79485,-2.79485,-2.79485,-2.79485], "fy":[-0.11691,-0.11691,-0.11691,-0.11691]}, + {"t":0.99163, "x":1.96636, "y":3.33557, "heading":3.14159, "vx":-0.18539, "vy":4.37881, "omega":0.0, "ax":-0.08064, "ay":-0.00344, "alpha":0.0, "fx":[-1.37159,-1.37159,-1.37159,-1.37159], "fy":[-0.05848,-0.05848,-0.05848,-0.05848]}, + {"t":1.01967, "x":1.96113, "y":3.45835, "heading":3.14159, "vx":-0.18765, "vy":4.37871, "omega":0.0, "ax":-0.08557, "ay":-0.00369, "alpha":0.0, "fx":[-1.45553,-1.45553,-1.45553,-1.45553], "fy":[-0.06282,-0.06282,-0.06282,-0.06282]}, + {"t":1.04771, "x":1.95584, "y":3.58113, "heading":3.14159, "vx":-0.19005, "vy":4.37861, "omega":0.0, "ax":-0.18578, "ay":-0.00818, "alpha":0.0, "fx":[-3.16008,-3.16008,-3.16008,-3.16008], "fy":[-0.13909,-0.13909,-0.13909,-0.13909]}, + {"t":1.07575, "x":1.95043, "y":3.7039, "heading":3.14159, "vx":-0.19525, "vy":4.37838, "omega":0.0, "ax":-0.51666, "ay":-0.02391, "alpha":0.0, "fx":[-8.78817,-8.78817,-8.78817,-8.78817], "fy":[-0.40666,-0.40666,-0.40666,-0.40666]}, + {"t":1.10379, "x":1.94476, "y":3.82666, "heading":3.14159, "vx":-0.20974, "vy":4.37771, "omega":0.0, "ax":-1.49723, "ay":-0.07901, "alpha":0.0, "fx":[-25.46745,-25.46745,-25.46745,-25.46745], "fy":[-1.34397,-1.34397,-1.34397,-1.34397]}, + {"t":1.13183, "x":1.93829, "y":3.94938, "heading":3.14159, "vx":-0.25172, "vy":4.3755, "omega":0.0, "ax":-3.85159, "ay":-0.26978, "alpha":0.0, "fx":[-65.51451,-65.51451,-65.51451,-65.51451], "fy":[-4.5889,-4.5889,-4.5889,-4.5889]}, + {"t":1.15987, "x":1.92971, "y":4.07196, "heading":3.14159, "vx":-0.35972, "vy":4.36793, "omega":0.0, "ax":-6.7014, "ay":-0.69895, "alpha":0.0, "fx":[-113.98888,-113.98888,-113.98888,-113.98888], "fy":[-11.88889,-11.88889,-11.88889,-11.88889]}, + {"t":1.18791, "x":1.91699, "y":4.19416, "heading":3.14159, "vx":-0.54763, "vy":4.34833, "omega":0.0, "ax":-8.22221, "ay":-1.24883, "alpha":0.0, "fx":[-139.85741,-139.85741,-139.85741,-139.85741], "fy":[-21.24224,-21.24224,-21.24224,-21.24224]}, + {"t":1.21595, "x":1.89841, "y":4.3156, "heading":3.14159, "vx":-0.77818, "vy":4.31332, "omega":0.0, "ax":-8.19102, "ay":-1.71636, "alpha":0.0, "fx":[-139.32693,-139.32693,-139.32693,-139.32693], "fy":[-29.19479,-29.19479,-29.19479,-29.19479]}, + {"t":1.24672, "x":1.87058, "y":4.44753, "heading":3.14159, "vx":-1.03027, "vy":4.26049, "omega":0.0, "ax":-6.29351, "ay":-1.67392, "alpha":0.0, "fx":[-107.05084,-107.05084,-107.05084,-107.05084], "fy":[-28.47291,-28.47291,-28.47291,-28.47291]}, + {"t":1.2775, "x":1.83589, "y":4.57787, "heading":3.14159, "vx":-1.22397, "vy":4.20897, "omega":0.0, "ax":-3.15206, "ay":-0.956, "alpha":0.0, "fx":[-53.61567,-53.61567,-53.61567,-53.61567], "fy":[-16.26121,-16.26121,-16.26121,-16.26121]}, + {"t":1.30828, "x":1.79672, "y":4.70695, "heading":3.14159, "vx":-1.32098, "vy":4.17955, "omega":0.0, "ax":-1.54381, "ay":-0.49759, "alpha":0.0, "fx":[-26.25981,-26.25981,-26.25981,-26.25981], "fy":[-8.46383,-8.46383,-8.46383,-8.46383]}, + {"t":1.33906, "x":1.75534, "y":4.83535, "heading":3.14159, "vx":-1.36849, "vy":4.16424, "omega":0.0, "ax":-2.26552, "ay":-0.7658, "alpha":0.0, "fx":[-38.53589,-38.53589,-38.53589,-38.53589], "fy":[-13.0261,-13.0261,-13.0261,-13.0261]}, + {"t":1.36983, "x":1.71215, "y":4.96315, "heading":3.14159, "vx":-1.43822, "vy":4.14067, "omega":0.0, "ax":-5.04703, "ay":-1.88432, "alpha":0.0, "fx":[-85.84847,-85.84847,-85.84847,-85.84847], "fy":[-32.05173,-32.05173,-32.05173,-32.05173]}, + {"t":1.40061, "x":1.66549, "y":5.0897, "heading":3.14159, "vx":-1.59355, "vy":4.08267, "omega":0.0, "ax":-2.66062, "ay":-8.09233, "alpha":0.0, "fx":[-45.25635,-45.25635,-45.25635,-45.25635], "fy":[-137.64814,-137.64814,-137.64814,-137.64814]}, + {"t":1.43139, "x":1.61519, "y":5.21151, "heading":3.14159, "vx":-1.67543, "vy":3.83362, "omega":0.0, "ax":2.25404, "ay":-9.32669, "alpha":0.0, "fx":[38.34059,38.34059,38.34059,38.34059], "fy":[-158.64434,-158.64434,-158.64434,-158.64434]}, + {"t":1.46216, "x":1.56469, "y":5.32508, "heading":3.14159, "vx":-1.60606, "vy":3.54657, "omega":0.0, "ax":2.61258, "ay":-9.31519, "alpha":0.0, "fx":[44.43926,44.43926,44.43926,44.43926], "fy":[-158.44874,-158.44874,-158.44874,-158.44874]}, + {"t":1.49294, "x":1.5165, "y":5.42982, "heading":3.14159, "vx":-1.52565, "vy":3.25988, "omega":0.0, "ax":2.74232, "ay":-9.3075, "alpha":0.0, "fx":[46.64616,46.64616,46.64616,46.64616], "fy":[-158.31789,-158.31789,-158.31789,-158.31789]}, + {"t":1.52372, "x":1.47084, "y":5.52575, "heading":3.14159, "vx":-1.44125, "vy":2.97342, "omega":0.0, "ax":2.80951, "ay":-9.30269, "alpha":0.0, "fx":[47.789,47.789,47.789,47.789], "fy":[-158.23613,-158.23613,-158.23613,-158.23613]}, + {"t":1.55449, "x":1.42782, "y":5.61285, "heading":3.14159, "vx":-1.35479, "vy":2.68711, "omega":0.0, "ax":2.85075, "ay":-9.29942, "alpha":0.0, "fx":[48.49041,48.49041,48.49041,48.49041], "fy":[-158.18051,-158.18051,-158.18051,-158.18051]}, + {"t":1.58527, "x":1.38747, "y":5.69115, "heading":3.14159, "vx":-1.26705, "vy":2.40091, "omega":0.0, "ax":2.87874, "ay":-9.29704, "alpha":0.0, "fx":[48.96654,48.96654,48.96654,48.96654], "fy":[-158.13996,-158.13996,-158.13996,-158.13996]}, + {"t":1.61605, "x":1.34984, "y":5.76064, "heading":3.14159, "vx":-1.17845, "vy":2.11477, "omega":0.0, "ax":2.89906, "ay":-9.29521, "alpha":0.0, "fx":[49.31222,49.31222,49.31222,49.31222], "fy":[-158.10881,-158.10881,-158.10881,-158.10881]}, + {"t":1.64682, "x":1.31494, "y":5.82132, "heading":3.14159, "vx":-1.08923, "vy":1.82869, "omega":0.0, "ax":2.91455, "ay":-9.29374, "alpha":0.0, "fx":[49.57562,49.57562,49.57562,49.57562], "fy":[-158.08388,-158.08388,-158.08388,-158.08388]}, + {"t":1.6776, "x":1.2828, "y":5.8732, "heading":3.14159, "vx":-0.99952, "vy":1.54266, "omega":0.0, "ax":2.92679, "ay":-9.29253, "alpha":0.0, "fx":[49.78381,49.78381,49.78381,49.78381], "fy":[-158.06327,-158.06327,-158.06327,-158.06327]}, + {"t":1.70838, "x":1.25342, "y":5.91628, "heading":3.14159, "vx":-0.90945, "vy":1.25667, "omega":0.0, "ax":2.93674, "ay":-9.2915, "alpha":0.0, "fx":[49.95317,49.95317,49.95317,49.95317], "fy":[-158.04578,-158.04578,-158.04578,-158.04578]}, + {"t":1.73915, "x":1.22682, "y":5.95056, "heading":3.14159, "vx":-0.81906, "vy":0.9707, "omega":0.0, "ax":2.94503, "ay":-9.29061, "alpha":0.0, "fx":[50.09418,50.09418,50.09418,50.09418], "fy":[-158.03062,-158.03062,-158.03062,-158.03062]}, + {"t":1.76993, "x":1.20301, "y":5.97603, "heading":3.14159, "vx":-0.72842, "vy":0.68477, "omega":0.0, "ax":-4.88244, "ay":-8.40909, "alpha":0.0, "fx":[-83.04891,-83.04891,-83.04891,-83.04891], "fy":[-143.03621,-143.03621,-143.03621,-143.03621]}, + {"t":1.8165, "x":1.1638, "y":5.9988, "heading":3.14159, "vx":-0.95577, "vy":0.29321, "omega":0.0, "ax":-0.77597, "ay":-8.98901, "alpha":0.0, "fx":[-13.19897,-13.19897,-13.19897,-13.19897], "fy":[-152.90048,-152.90048,-152.90048,-152.90048]}, + {"t":1.86306, "x":1.11845, "y":6.00271, "heading":3.14159, "vx":-0.9919, "vy":-0.12534, "omega":0.0, "ax":0.06692, "ay":-0.48521, "alpha":0.0, "fx":[1.13832,1.13832,1.13832,1.13832], "fy":[-8.25327,-8.25327,-8.25327,-8.25327]}, + {"t":1.90962, "x":1.07234, "y":5.99635, "heading":3.14159, "vx":-0.98878, "vy":-0.14794, "omega":0.0, "ax":0.00052, "ay":-0.00344, "alpha":0.0, "fx":[0.00876,0.00876,0.00876,0.00876], "fy":[-0.05856,-0.05856,-0.05856,-0.05856]}, + {"t":1.95618, "x":1.0263, "y":5.98945, "heading":3.14159, "vx":-0.98876, "vy":-0.1481, "omega":0.0, "ax":-0.00007, "ay":0.00047, "alpha":0.0, "fx":[-0.00121,-0.00121,-0.00121,-0.00121], "fy":[0.00808,0.00808,0.00808,0.00808]}, + {"t":2.00275, "x":0.98026, "y":5.98256, "heading":3.14159, "vx":-0.98876, "vy":-0.14808, "omega":0.0, "ax":-0.00008, "ay":0.00051, "alpha":0.0, "fx":[-0.00132,-0.00132,-0.00132,-0.00132], "fy":[0.00876,0.00876,0.00876,0.00876]}, + {"t":2.04931, "x":0.93422, "y":5.97566, "heading":3.14159, "vx":-0.98876, "vy":-0.14805, "omega":0.0, "ax":-0.00008, "ay":0.00052, "alpha":0.0, "fx":[-0.00134,-0.00134,-0.00134,-0.00134], "fy":[0.00893,0.00893,0.00893,0.00893]}, + {"t":2.09587, "x":0.88818, "y":5.96877, "heading":3.14159, "vx":-0.98877, "vy":-0.14803, "omega":0.0, "ax":-0.00008, "ay":0.00054, "alpha":0.0, "fx":[-0.00137,-0.00137,-0.00137,-0.00137], "fy":[0.00913,0.00913,0.00913,0.00913]}, + {"t":2.14244, "x":0.84214, "y":5.96188, "heading":3.14159, "vx":-0.98877, "vy":-0.148, "omega":0.0, "ax":-0.00008, "ay":0.00055, "alpha":0.0, "fx":[-0.00141,-0.00141,-0.00141,-0.00141], "fy":[0.00936,0.00936,0.00936,0.00936]}, + {"t":2.189, "x":0.7961, "y":5.95499, "heading":3.14159, "vx":-0.98878, "vy":-0.14798, "omega":0.0, "ax":-0.00009, "ay":0.00057, "alpha":0.0, "fx":[-0.00145,-0.00145,-0.00145,-0.00145], "fy":[0.00965,0.00965,0.00965,0.00965]}, + {"t":2.23556, "x":0.75006, "y":5.9481, "heading":3.14159, "vx":-0.98878, "vy":-0.14795, "omega":0.0, "ax":-0.00009, "ay":0.00059, "alpha":0.0, "fx":[-0.0015,-0.0015,-0.0015,-0.0015], "fy":[0.01001,0.01001,0.01001,0.01001]}, + {"t":2.28213, "x":0.70402, "y":5.94121, "heading":3.14159, "vx":-0.98878, "vy":-0.14792, "omega":0.0, "ax":-0.00009, "ay":0.00062, "alpha":0.0, "fx":[-0.00157,-0.00157,-0.00157,-0.00157], "fy":[0.01047,0.01047,0.01047,0.01047]}, + {"t":2.32869, "x":0.65798, "y":5.93432, "heading":3.14159, "vx":-0.98879, "vy":-0.14789, "omega":0.0, "ax":-0.0001, "ay":0.00067, "alpha":0.0, "fx":[-0.00172,-0.00172,-0.00172,-0.00172], "fy":[0.01145,0.01145,0.01145,0.01145]}, + {"t":2.37525, "x":0.61194, "y":5.92744, "heading":3.14159, "vx":-0.98879, "vy":-0.14786, "omega":0.0, "ax":-0.00053, "ay":0.00356, "alpha":0.0, "fx":[-0.00906,-0.00906,-0.00906,-0.00906], "fy":[0.06055,0.06055,0.06055,0.06055]}, + {"t":2.42182, "x":0.56589, "y":5.92056, "heading":3.14159, "vx":-0.98882, "vy":-0.1477, "omega":0.0, "ax":-0.04983, "ay":0.35385, "alpha":0.0, "fx":[-0.84761,-0.84761,-0.84761,-0.84761], "fy":[6.01893,6.01893,6.01893,6.01893]}, + {"t":2.46838, "x":0.5198, "y":5.91406, "heading":3.14159, "vx":-0.99114, "vy":-0.13122, "omega":0.0, "ax":0.65084, "ay":8.73589, "alpha":0.0, "fx":[11.07052,11.07052,11.07052,11.07052], "fy":[148.595,148.595,148.595,148.595]}, + {"t":2.51494, "x":0.47435, "y":5.91742, "heading":3.14159, "vx":-0.96083, "vy":0.27555, "omega":0.0, "ax":8.44163, "ay":4.84358, "alpha":0.0, "fx":[143.58977,143.58977,143.58977,143.58977], "fy":[82.38793,82.38793,82.38793,82.38793]}, + {"t":2.56151, "x":0.43877, "y":5.9355, "heading":3.14159, "vx":-0.56776, "vy":0.50108, "omega":0.0, "ax":9.3492, "ay":2.7711, "alpha":0.0, "fx":[159.02729,159.02729,159.02729,159.02729], "fy":[47.13566,47.13566,47.13566,47.13566]}, + {"t":2.60807, "x":0.42246, "y":5.96184, "heading":3.14159, "vx":-0.13244, "vy":0.63011, "omega":0.0, "ax":9.47314, "ay":2.31528, "alpha":0.0, "fx":[161.13544,161.13544,161.13544,161.13544], "fy":[39.38233,39.38233,39.38233,39.38233]}, + {"t":2.64789, "x":0.4247, "y":5.98877, "heading":3.14159, "vx":0.24478, "vy":0.72231, "omega":0.0, "ax":9.5479, "ay":1.97827, "alpha":0.0, "fx":[162.40699,162.40699,162.40699,162.40699], "fy":[33.64986,33.64986,33.64986,33.64986]}, + {"t":2.68771, "x":0.44202, "y":6.0191, "heading":3.14159, "vx":0.62498, "vy":0.80108, "omega":0.0, "ax":9.62544, "ay":1.54776, "alpha":0.0, "fx":[163.72597,163.72597,163.72597,163.72597], "fy":[26.32689,26.32689,26.32689,26.32689]}, + {"t":2.72753, "x":0.47453, "y":6.05222, "heading":3.14159, "vx":1.00826, "vy":0.86271, "omega":0.0, "ax":9.69732, "ay":0.98323, "alpha":0.0, "fx":[164.94872,164.94872,164.94872,164.94872], "fy":[16.72448,16.72448,16.72448,16.72448]}, + {"t":2.76735, "x":0.52237, "y":6.08735, "heading":3.14159, "vx":1.3944, "vy":0.90186, "omega":0.0, "ax":9.74189, "ay":0.22086, "alpha":0.0, "fx":[165.70678,165.70678,165.70678,165.70678], "fy":[3.75675,3.75675,3.75675,3.75675]}, + {"t":2.80717, "x":0.58562, "y":6.12344, "heading":3.14159, "vx":1.78232, "vy":0.91066, "omega":0.0, "ax":9.70463, "ay":-0.83986, "alpha":0.0, "fx":[165.07301,165.07301,165.07301,165.07301], "fy":[-14.28583,-14.28583,-14.28583,-14.28583]}, + {"t":2.84699, "x":0.66428, "y":6.15904, "heading":3.14159, "vx":2.16875, "vy":0.87722, "omega":0.0, "ax":9.4498, "ay":-2.34477, "alpha":0.0, "fx":[160.73839,160.73839,160.73839,160.73839], "fy":[-39.8839,-39.8839,-39.8839,-39.8839]}, + {"t":2.88681, "x":0.75813, "y":6.19211, "heading":3.14159, "vx":2.54504, "vy":0.78385, "omega":0.0, "ax":8.66341, "ay":-4.43124, "alpha":0.0, "fx":[147.36215,147.36215,147.36215,147.36215], "fy":[-75.37411,-75.37411,-75.37411,-75.37411]}, + {"t":2.92663, "x":0.86635, "y":6.21981, "heading":3.14159, "vx":2.89001, "vy":0.6074, "omega":0.0, "ax":6.82056, "ay":-6.93383, "alpha":0.0, "fx":[116.01573,116.01573,116.01573,116.01573], "fy":[-117.94252,-117.94252,-117.94252,-117.94252]}, + {"t":2.96645, "x":0.98683, "y":6.2385, "heading":3.14159, "vx":3.16161, "vy":0.3313, "omega":0.0, "ax":3.77071, "ay":-8.96463, "alpha":0.0, "fx":[64.13872,64.13872,64.13872,64.13872], "fy":[-152.48583,-152.48583,-152.48583,-152.48583]}, + {"t":3.00626, "x":1.11571, "y":6.24458, "heading":3.14159, "vx":3.31175, "vy":-0.02567, "omega":0.0, "ax":0.51801, "ay":-9.7155, "alpha":0.0, "fx":[8.81126,8.81126,8.81126,8.81126], "fy":[-165.25782,-165.25782,-165.25782,-165.25782]}, + {"t":3.04608, "x":1.248, "y":6.23586, "heading":3.14159, "vx":3.33238, "vy":-0.41254, "omega":0.0, "ax":-1.9561, "ay":-9.53625, "alpha":0.0, "fx":[-33.27275,-33.27275,-33.27275,-33.27275], "fy":[-162.20882,-162.20882,-162.20882,-162.20882]}, + {"t":3.0859, "x":1.37914, "y":6.21187, "heading":3.14159, "vx":3.25449, "vy":-0.79227, "omega":0.0, "ax":-3.59234, "ay":-9.05296, "alpha":0.0, "fx":[-61.10467,-61.10467,-61.10467,-61.10467], "fy":[-153.9883,-153.9883,-153.9883,-153.9883]}, + {"t":3.12572, "x":1.50589, "y":6.17314, "heading":3.14159, "vx":3.11144, "vy":-1.15275, "omega":0.0, "ax":-4.66024, "ay":-8.55669, "alpha":0.0, "fx":[-79.26927,-79.26927,-79.26927,-79.26927], "fy":[-145.54679,-145.54679,-145.54679,-145.54679]}, + {"t":3.16554, "x":1.62609, "y":6.12046, "heading":3.14159, "vx":2.92587, "vy":-1.49348, "omega":0.0, "ax":-5.38038, "ay":-8.12663, "alpha":0.0, "fx":[-91.51872,-91.51872,-91.51872,-91.51872], "fy":[-138.23165,-138.23165,-138.23165,-138.23165]}, + {"t":3.20536, "x":1.73833, "y":6.05455, "heading":3.14159, "vx":2.71163, "vy":-1.81708, "omega":0.0, "ax":-5.88681, "ay":-7.77039, "alpha":0.0, "fx":[-100.13296,-100.13296,-100.13296,-100.13296], "fy":[-132.17207,-132.17207,-132.17207,-132.17207]}, + {"t":3.24518, "x":1.84164, "y":5.97603, "heading":3.14159, "vx":2.47722, "vy":-2.12649, "omega":0.0, "ax":-5.23919, "ay":-7.35511, "alpha":-9.96289, "fx":[-152.26373,-117.57601,-27.23444,-59.39415], "fy":[-65.5778,-116.9683,-163.5104,-154.37663]}, + {"t":3.27405, "x":1.91096, "y":5.91159, "heading":3.14159, "vx":2.326, "vy":-2.33879, "omega":-0.28757, "ax":-5.51629, "ay":-7.23196, "alpha":-9.37966, "fx":[-152.59374,-119.25674,-33.36758,-70.1037], "fy":[-64.73809,-115.22055,-162.33379,-149.76191]}, + {"t":3.30291, "x":1.9758, "y":5.84107, "heading":3.13329, "vx":2.16678, "vy":-2.54753, "omega":-0.5583, "ax":-5.83074, "ay":-7.06397, "alpha":-8.80217, "fx":[-153.17534,-121.63811,-41.04599,-80.85716], "fy":[-63.26337,-112.66119,-160.52053,-144.17938]}, + {"t":3.33177, "x":2.03591, "y":5.76459, "heading":3.11718, "vx":1.99848, "vy":-2.75142, "omega":-0.81237, "ax":-6.19521, "ay":-6.8368, "alpha":-8.18471, "fx":[-153.99457,-124.77007,-50.67825,-92.07242], "fy":[-61.13034,-109.12709,-157.68979,-137.22056]}, + {"t":3.36064, "x":2.09101, "y":5.68233, "heading":3.09373, "vx":1.81966, "vy":-2.94876, "omega":-1.04861, "ax":-6.62572, "ay":-6.5258, "alpha":-7.47021, "fx":[-155.0362,-128.7378,-62.8783,-104.15417], "fy":[-58.28907,-104.34177,-153.16296,-128.21394]}, + {"t":3.3895, "x":2.14078, "y":5.5945, "heading":3.06346, "vx":1.62842, "vy":-3.13712, "omega":-1.26423, "ax":-7.14032, "ay":-6.0895, "alpha":-6.58306, "fx":[-156.28504,-133.67842,-78.51857,-117.33709], "fy":[-54.64677,-97.82473,-145.66826,-116.18281]}, + {"t":3.41837, "x":2.1848, "y":5.50141, "heading":3.02697, "vx":1.42232, "vy":-3.31289, "omega":-1.45424, "ax":-7.75384, "ay":-5.45812, "alpha":-5.41824, "fx":[-157.72746,-139.79819,-98.63199,-131.40453], "fy":[-50.0337,-88.69383,-132.73771,-99.89914]}, + {"t":3.44723, "x":2.22263, "y":5.40352, "heading":2.985, "vx":1.19851, "vy":-3.47043, "omega":-1.61063, "ax":-8.45834, "ay":-4.51715, "alpha":-3.83927, "fx":[-159.3526,-147.33975,-123.51732,-145.28577], "fy":[-44.12574,-75.21704,-109.74684,-78.25236]}, + {"t":3.47609, "x":2.2537, "y":5.30146, "heading":2.93851, "vx":0.95437, "vy":-3.60081, "omega":-1.72145, "ax":-9.1601, "ay":-3.12124, "alpha":-1.79436, "fx":[-161.15152,-156.13877,-149.1739,-156.77828], "fy":[-36.23186,-54.0358,-70.69145,-51.40624]}, + {"t":3.50496, "x":2.27743, "y":5.19623, "heading":2.88882, "vx":0.68998, "vy":-3.6909, "omega":-1.77324, "ax":-9.55142, "ay":-1.66222, "alpha":-0.4037, "fx":[-163.17302,-162.38935,-161.69574,-162.60938], "fy":[-24.17844,-29.06049,-32.44891,-27.4074]}, + {"t":3.53382, "x":2.29337, "y":5.08901, "heading":2.83764, "vx":0.41429, "vy":-3.73888, "omega":-1.78489, "ax":-9.64906, "ay":-0.74458, "alpha":-0.67148, "fx":[-164.63187,-164.13768,-163.46711,-164.27466], "fy":[-5.84365,-14.32241,-19.76324,-10.73118]}, + {"t":3.56269, "x":2.3013, "y":4.98078, "heading":2.78612, "vx":0.13578, "vy":-3.76037, "omega":-1.80427, "ax":-9.63684, "ay":0.25439, "alpha":-1.4703, "fx":[-163.43255,-164.54481,-163.85049,-163.8517], "fy":[18.63293,0.08014,-11.5835,10.17881]}, + {"t":3.59155, "x":2.30121, "y":4.87234, "heading":2.73404, "vx":-0.14238, "vy":-3.75303, "omega":-1.84671, "ax":-9.43478, "ay":1.45377, "alpha":-2.87821, "fx":[-156.716,-163.60312,-163.48471,-158.12812], "fy":[49.15977,15.38084,-6.75859,41.13058]}, + {"t":3.62041, "x":2.29317, "y":4.76462, "heading":2.68074, "vx":-0.4147, "vy":-3.71107, "omega":-1.92979, "ax":-8.79621, "ay":2.98612, "alpha":-5.38101, "fx":[-141.439,-160.81009,-162.46953,-133.76559], "fy":[83.10557,32.74388,-4.24365,91.56607]}, + {"t":3.64928, "x":2.27753, "y":4.65875, "heading":2.62504, "vx":-0.6686, "vy":-3.62488, "omega":-2.08511, "ax":-7.22374, "ay":4.87173, "alpha":-9.35282, "fx":[-116.23308,-153.95943,-160.07168,-61.23094], "fy":[115.608,56.18282,10.64212,149.03417]}, + {"t":3.67814, "x":2.25523, "y":4.55615, "heading":2.56485, "vx":-0.8771, "vy":-3.48426, "omega":-2.35506, "ax":-5.05046, "ay":6.30992, "alpha":-12.54923, "fx":[-84.39127,-141.82363,-147.4446,30.03231], "fy":[140.57365,81.81128,48.09584,158.83926]}, + {"t":3.707, "x":2.22781, "y":4.45821, "heading":2.49688, "vx":-1.02288, "vy":-3.30213, "omega":-2.71728, "ax":-1.78963, "ay":7.20981, "alpha":-14.91305, "fx":[-53.31823,-129.05342,-32.19106,92.79818], "fy":[155.19988,100.71114,101.13473,133.50172]}, + {"t":3.73587, "x":2.19754, "y":4.3659, "heading":2.41844, "vx":-1.07453, "vy":-3.09403, "omega":-3.14773, "ax":0.6823, "ay":8.80232, "alpha":-10.18299, "fx":[-14.56522,-76.17763,50.96836,86.19755], "fy":[163.56412,144.87491,151.62703,138.83373]}, + {"t":3.76473, "x":2.16681, "y":4.28026, "heading":2.32759, "vx":-1.05484, "vy":-2.83996, "omega":-3.44165, "ax":1.84452, "ay":9.46254, "alpha":1.9236, "fx":[37.0798,46.65387,26.24109,15.52403], "fy":[160.01463,157.66248,162.44866,163.69479]}, + {"t":3.7936, "x":2.13713, "y":4.20223, "heading":2.22825, "vx":-1.0016, "vy":-2.56683, "omega":-3.38613, "ax":2.67008, "ay":8.96882, "alpha":6.93388, "fx":[69.98179,95.71282,29.04617,-13.0719], "fy":[148.71386,134.37242,162.64731,164.49485]}, + {"t":3.82246, "x":2.10933, "y":4.13188, "heading":2.13051, "vx":-0.92453, "vy":-2.30796, "omega":-3.18599, "ax":3.17126, "ay":8.54228, "alpha":9.04046, "fx":[86.79727,117.57777,34.82344,-23.42957], "fy":[139.65475,116.11937,161.77763,163.65518]}, + {"t":3.85132, "x":2.08397, "y":4.06882, "heading":2.03855, "vx":-0.833, "vy":-2.0614, "omega":-2.92505, "ax":3.48878, "ay":8.25752, "alpha":9.89636, "fx":[92.61038,130.11597,41.12849,-26.48255], "fy":[135.8988,102.12528,160.43485,163.37315]}, + {"t":3.88019, "x":2.06138, "y":4.01276, "heading":1.95412, "vx":-0.7323, "vy":-1.82305, "omega":-2.6394, "ax":3.66426, "ay":8.09388, "alpha":10.14612, "fx":[89.92737,138.44843,47.26372,-26.32746], "fy":[137.66875,90.68774,158.82568,163.51587]}, + {"t":3.90905, "x":2.04176, "y":3.96351, "heading":1.87794, "vx":-0.62653, "vy":-1.58943, "omega":-2.34654, "ax":3.70256, "ay":8.02354, "alpha":10.15008, "fx":[79.2075,144.44861,52.9657,-24.70404], "fy":[144.05885,80.9266,157.07777,163.84911]}, + {"t":3.93792, "x":2.02522, "y":3.92098, "heading":1.81021, "vx":-0.51966, "vy":-1.35784, "omega":-2.05357, "ax":3.59945, "ay":8.00816, "alpha":10.19996, "fx":[60.26358,148.94802,58.12626,-22.43535], "fy":[152.92232,72.42429,155.28695,164.23239]}, + {"t":3.96678, "x":2.01172, "y":3.88512, "heading":1.75094, "vx":-0.41577, "vy":-1.12669, "omega":-1.75916, "ax":3.37279, "ay":7.99385, "alpha":10.54411, "fx":[34.35649,152.37186,62.69827,-19.94617], "fy":[160.75597,65.00856,153.532,164.59569]}, + {"t":3.99564, "x":2.00113, "y":3.85593, "heading":1.70016, "vx":-0.31842, "vy":-0.89596, "omega":-1.45482, "ax":3.08557, "ay":7.93362, "alpha":11.26732, "fx":[5.80367,154.96961,66.65259,-17.4869], "fy":[164.37081,58.63258,151.88379,164.90746]}, + {"t":4.02451, "x":1.99322, "y":3.83337, "heading":1.65817, "vx":-0.22935, "vy":-0.66697, "omega":-1.1296, "ax":2.81984, "ay":7.82331, "alpha":12.21022, "fx":[-19.76488,156.90998,69.95532,-15.24178], "fy":[163.41901,53.30183,150.41263,165.15532]}, + {"t":4.05337, "x":1.98778, "y":3.81738, "heading":1.62556, "vx":-0.14796, "vy":-0.44115, "omega":-0.77717, "ax":2.6239, "ay":7.69625, "alpha":13.11584, "fx":[-38.97793,158.31825,72.56007,-13.37335], "fy":[160.07848,49.03714,149.19113,165.33711]}, + {"t":4.08223, "x":1.9846, "y":3.80785, "heading":1.60313, "vx":-0.07223, "vy":-0.21901, "omega":-0.39859, "ax":2.50235, "ay":7.58773, "alpha":13.80941, "fx":[-51.42039,159.28993,74.41472,-12.02725], "fy":[156.65654,45.85797,148.28945,165.45655]}, + {"t":4.1111, "x":1.98356, "y":3.80469, "heading":1.59163, "vx":0.0, "vy":0.0, "omega":0.0, "ax":0.0, "ay":0.0, "alpha":0.0, "fx":[0.0,0.0,0.0,0.0], "fy":[0.0,0.0,0.0,0.0]}], + "splits":[0] + }, + "events":[] +} diff --git a/lib/examples/swerve/src/main/deploy/choreo/RIGHT_TRENCH.traj b/lib/examples/swerve/src/main/deploy/choreo/RIGHT_TRENCH.traj new file mode 100644 index 00000000..bee355fd --- /dev/null +++ b/lib/examples/swerve/src/main/deploy/choreo/RIGHT_TRENCH.traj @@ -0,0 +1,213 @@ +{ + "name":"RIGHT_TRENCH", + "version":3, + "snapshot":{ + "waypoints":[ + {"x":4.441253185272217, "y":0.6037423729896545, "heading":1.5707963267948966, "intervals":30, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":7.795207023620605, "y":1.3253390531539917, "heading":1.5707963267948966, "intervals":24, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":7.795207023620605, "y":3.931103515625, "heading":-1.5707963267948966, "intervals":33, "split":false, "fixTranslation":true, "fixHeading":false, "overrideIntervals":false}, + {"x":5.714679718017578, "y":0.62682590484619, "heading":1.5707963267948966, "intervals":23, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":3.2933757305145264, "y":0.62682590484619, "heading":1.5707963267948966, "intervals":31, "split":false, "fixTranslation":true, "fixHeading":false, "overrideIntervals":false}, + {"x":1.9037671089172363, "y":4.050841808319092, "heading":1.570796602033749, "intervals":40, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}], + "constraints":[ + {"from":"first", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"last", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"first", "to":"last", "data":{"type":"KeepInRectangle", "props":{"x":0.0, "y":0.0, "w":16.541, "h":8.0692}}, "enabled":true}, + {"from":1, "to":2, "data":{"type":"KeepInLane", "props":{"tolerance":0.01}}, "enabled":true}, + {"from":"first", "to":"last", "data":{"type":"MaxVelocity", "props":{"max":3.0}}, "enabled":true}, + {"from":1, "to":2, "data":{"type":"MaxAngularVelocity", "props":{"max":0.0}}, "enabled":true}], + "targetDt":0.05 + }, + "params":{ + "waypoints":[ + {"x":{"exp":"4.441253185272217 m", "val":4.441253185272217}, "y":{"exp":"0.6037423729896545 m", "val":0.6037423729896545}, "heading":{"exp":"90 deg", "val":1.5707963267948966}, "intervals":30, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":{"exp":"7.7952070236206055 m", "val":7.795207023620605}, "y":{"exp":"1.3253390531539917 m", "val":1.3253390531539917}, "heading":{"exp":"90 deg", "val":1.5707963267948966}, "intervals":24, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":{"exp":"7.7952070236206055 m", "val":7.795207023620605}, "y":{"exp":"3.931103515625 m", "val":3.931103515625}, "heading":{"exp":"-1.5707963267948966 rad", "val":-1.5707963267948966}, "intervals":33, "split":false, "fixTranslation":true, "fixHeading":false, "overrideIntervals":false}, + {"x":{"exp":"5.714679718017578 m", "val":5.714679718017578}, "y":{"exp":"0.62682590484619 m", "val":0.62682590484619}, "heading":{"exp":"90 deg", "val":1.5707963267948966}, "intervals":23, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}, + {"x":{"exp":"3.2933757305145264 m", "val":3.2933757305145264}, "y":{"exp":"0.62682590484619 m", "val":0.62682590484619}, "heading":{"exp":"90 deg", "val":1.5707963267948966}, "intervals":31, "split":false, "fixTranslation":true, "fixHeading":false, "overrideIntervals":false}, + {"x":{"exp":"1.9037671089172363 m", "val":1.9037671089172363}, "y":{"exp":"4.050841808319092 m", "val":4.050841808319092}, "heading":{"exp":"1.570796602033749 rad", "val":1.570796602033749}, "intervals":40, "split":false, "fixTranslation":true, "fixHeading":true, "overrideIntervals":false}], + "constraints":[ + {"from":"first", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"last", "to":null, "data":{"type":"StopPoint", "props":{}}, "enabled":true}, + {"from":"first", "to":"last", "data":{"type":"KeepInRectangle", "props":{"x":{"exp":"0 m", "val":0.0}, "y":{"exp":"0 m", "val":0.0}, "w":{"exp":"16.541 m", "val":16.541}, "h":{"exp":"8.0692 m", "val":8.0692}}}, "enabled":true}, + {"from":1, "to":2, "data":{"type":"KeepInLane", "props":{"tolerance":{"exp":"0.01 m", "val":0.01}}}, "enabled":true}, + {"from":"first", "to":"last", "data":{"type":"MaxVelocity", "props":{"max":{"exp":"3 m / s", "val":3.0}}}, "enabled":true}, + {"from":1, "to":2, "data":{"type":"MaxAngularVelocity", "props":{"max":{"exp":"0 rad / s", "val":0.0}}}, "enabled":true}], + "targetDt":{ + "exp":"0.05 s", + "val":0.05 + } + }, + "trajectory":{ + "config":{ + "frontLeft":{ + "x":0.225425, + "y":0.333375 + }, + "backLeft":{ + "x":-0.225425, + "y":0.333375 + }, + "mass":68.0388555, + "inertia":7.5, + "gearing":7.03, + "radius":0.0508, + "vmax":607.3745796940267, + "tmax":1.2, + "cof":1.5, + "bumper":{ + "front":0.34925, + "side":0.4445, + "back":0.34925 + }, + "differentialTrackWidth":0.5588 + }, + "sampleType":"Swerve", + "waypoints":[0.0,1.40394,2.41372,3.87973,4.70708,6.10492], + "samples":[ + {"t":0.0, "x":4.44125, "y":0.60374, "heading":1.5708, "vx":0.0, "vy":0.0, "omega":0.0, "ax":9.75357, "ay":-0.05824, "alpha":0.0, "fx":[165.90536,165.90536,165.90536,165.90536], "fy":[-0.99062,-0.99062,-0.99062,-0.99062]}, + {"t":0.0468, "x":4.45193, "y":0.60368, "heading":1.5708, "vx":0.45645, "vy":-0.00273, "omega":0.0, "ax":9.75174, "ay":-0.05985, "alpha":0.0, "fx":[165.8743,165.8743,165.8743,165.8743], "fy":[-1.01805,-1.01805,-1.01805,-1.01805]}, + {"t":0.0936, "x":4.48397, "y":0.60349, "heading":1.5708, "vx":0.91281, "vy":-0.00553, "omega":0.0, "ax":9.74901, "ay":-0.06147, "alpha":0.0, "fx":[165.82782,165.82782,165.82782,165.82782], "fy":[-1.04558,-1.04558,-1.04558,-1.04558]}, + {"t":0.14039, "x":4.53737, "y":0.60316, "heading":1.5708, "vx":1.36904, "vy":-0.0084, "omega":0.0, "ax":9.74446, "ay":-0.0631, "alpha":0.0, "fx":[165.75056,165.75056,165.75056,165.75056], "fy":[-1.07332,-1.07332,-1.07332,-1.07332]}, + {"t":0.18719, "x":4.6121, "y":0.6027, "heading":1.5708, "vx":1.82506, "vy":-0.01136, "omega":0.0, "ax":9.73541, "ay":-0.06476, "alpha":0.0, "fx":[165.59661,165.59661,165.59661,165.59661], "fy":[-1.10152,-1.10152,-1.10152,-1.10152]}, + {"t":0.23399, "x":4.70817, "y":0.60209, "heading":1.5708, "vx":2.28066, "vy":-0.01439, "omega":0.0, "ax":9.70849, "ay":-0.06652, "alpha":0.0, "fx":[165.13856,165.13856,165.13856,165.13856], "fy":[-1.13142,-1.13142,-1.13142,-1.13142]}, + {"t":0.28079, "x":4.82553, "y":0.60135, "heading":1.5708, "vx":2.73499, "vy":-0.0175, "omega":0.0, "ax":5.64742, "ay":-0.07918, "alpha":0.0, "fx":[96.06098,96.06098,96.06098,96.06098], "fy":[-1.34684,-1.34684,-1.34684,-1.34684]}, + {"t":0.32758, "x":4.95971, "y":0.60044, "heading":1.5708, "vx":2.99928, "vy":-0.0212, "omega":0.0, "ax":-0.00004, "ay":-0.02368, "alpha":0.0, "fx":[-0.00064,-0.00064,-0.00064,-0.00064], "fy":[-0.40272,-0.40272,-0.40272,-0.40272]}, + {"t":0.37438, "x":5.10007, "y":0.59942, "heading":1.5708, "vx":2.99928, "vy":-0.02231, "omega":0.0, "ax":-0.00016, "ay":-0.02147, "alpha":0.0, "fx":[-0.00278,-0.00278,-0.00278,-0.00278], "fy":[-0.36526,-0.36526,-0.36526,-0.36526]}, + {"t":0.42118, "x":5.24043, "y":0.59836, "heading":1.5708, "vx":2.99927, "vy":-0.02332, "omega":0.0, "ax":-0.00017, "ay":-0.02141, "alpha":0.0, "fx":[-0.00289,-0.00289,-0.00289,-0.00289], "fy":[-0.3641,-0.3641,-0.3641,-0.3641]}, + {"t":0.46798, "x":5.38079, "y":0.59724, "heading":1.5708, "vx":2.99926, "vy":-0.02432, "omega":0.0, "ax":-0.00018, "ay":-0.02148, "alpha":0.0, "fx":[-0.00303,-0.00303,-0.00303,-0.00303], "fy":[-0.36533,-0.36533,-0.36533,-0.36533]}, + {"t":0.51478, "x":5.52115, "y":0.59608, "heading":1.5708, "vx":2.99925, "vy":-0.02532, "omega":0.0, "ax":-0.00019, "ay":-0.02156, "alpha":0.0, "fx":[-0.00316,-0.00316,-0.00316,-0.00316], "fy":[-0.36679,-0.36679,-0.36679,-0.36679]}, + {"t":0.56157, "x":5.66151, "y":0.59487, "heading":1.5708, "vx":2.99924, "vy":-0.02633, "omega":0.0, "ax":-0.00019, "ay":-0.02165, "alpha":0.0, "fx":[-0.0033,-0.0033,-0.0033,-0.0033], "fy":[-0.36833,-0.36833,-0.36833,-0.36833]}, + {"t":0.60837, "x":5.80186, "y":0.59362, "heading":1.5708, "vx":2.99924, "vy":-0.02735, "omega":0.0, "ax":-0.0002, "ay":-0.02175, "alpha":0.0, "fx":[-0.00344,-0.00344,-0.00344,-0.00344], "fy":[-0.36994,-0.36994,-0.36994,-0.36994]}, + {"t":0.65517, "x":5.94222, "y":0.59231, "heading":1.5708, "vx":2.99923, "vy":-0.02836, "omega":0.0, "ax":-0.00021, "ay":-0.02185, "alpha":0.0, "fx":[-0.00358,-0.00358,-0.00358,-0.00358], "fy":[-0.37162,-0.37162,-0.37162,-0.37162]}, + {"t":0.70197, "x":6.08258, "y":0.59096, "heading":1.5708, "vx":2.99922, "vy":-0.02939, "omega":0.0, "ax":-0.00022, "ay":-0.02195, "alpha":0.0, "fx":[-0.00372,-0.00372,-0.00372,-0.00372], "fy":[-0.37332,-0.37332,-0.37332,-0.37332]}, + {"t":0.74877, "x":6.22293, "y":0.58956, "heading":1.5708, "vx":2.99921, "vy":-0.03041, "omega":0.0, "ax":-0.00023, "ay":-0.022, "alpha":0.0, "fx":[-0.00386,-0.00386,-0.00386,-0.00386], "fy":[-0.37419,-0.37419,-0.37419,-0.37419]}, + {"t":0.79556, "x":6.36329, "y":0.58811, "heading":1.5708, "vx":2.9992, "vy":-0.03144, "omega":0.0, "ax":-0.00023, "ay":-0.02122, "alpha":0.0, "fx":[-0.00384,-0.00384,-0.00384,-0.00384], "fy":[-0.36088,-0.36088,-0.36088,-0.36088]}, + {"t":0.84236, "x":6.50365, "y":0.58662, "heading":1.5708, "vx":2.99918, "vy":-0.03244, "omega":0.0, "ax":-0.00008, "ay":-0.00741, "alpha":0.0, "fx":[-0.00137,-0.00137,-0.00137,-0.00137], "fy":[-0.12601,-0.12601,-0.12601,-0.12601]}, + {"t":0.88916, "x":6.644, "y":0.58509, "heading":1.5708, "vx":2.99918, "vy":-0.03278, "omega":0.0, "ax":0.00194, "ay":0.20892, "alpha":0.0, "fx":[0.03303,0.03303,0.03303,0.03303], "fy":[3.55373,3.55373,3.55373,3.55373]}, + {"t":0.93596, "x":6.78436, "y":0.58379, "heading":1.5708, "vx":2.99927, "vy":-0.02301, "omega":0.0, "ax":-0.05514, "ay":3.19297, "alpha":0.0, "fx":[-0.93799,-0.93799,-0.93799,-0.93799], "fy":[54.31147,54.31147,54.31147,54.31147]}, + {"t":0.98275, "x":6.92466, "y":0.58621, "heading":1.5708, "vx":2.99669, "vy":0.12642, "omega":0.0, "ax":-0.98673, "ay":8.81506, "alpha":0.0, "fx":[-16.78395,-16.78395,-16.78395,-16.78395], "fy":[149.94157,149.94157,149.94157,149.94157]}, + {"t":1.02955, "x":7.06382, "y":0.60178, "heading":1.5708, "vx":2.95051, "vy":0.53894, "omega":0.0, "ax":-2.41154, "ay":9.23765, "alpha":0.0, "fx":[-41.01952,-41.01952,-41.01952,-41.01952], "fy":[157.12981,157.12981,157.12981,157.12981]}, + {"t":1.07635, "x":7.19925, "y":0.63711, "heading":1.5708, "vx":2.83766, "vy":0.97125, "omega":0.0, "ax":-3.81516, "ay":8.88585, "alpha":0.0, "fx":[-64.89478,-64.89478,-64.89478,-64.89478], "fy":[151.14583,151.14583,151.14583,151.14583]}, + {"t":1.12315, "x":7.32787, "y":0.6923, "heading":1.5708, "vx":2.65912, "vy":1.38708, "omega":0.0, "ax":-5.13673, "ay":8.24187, "alpha":0.0, "fx":[-87.37424,-87.37424,-87.37424,-87.37424], "fy":[140.19186,140.19186,140.19186,140.19186]}, + {"t":1.16995, "x":7.44669, "y":0.76623, "heading":1.5708, "vx":2.41873, "vy":1.77279, "omega":0.0, "ax":-6.51104, "ay":7.23067, "alpha":0.0, "fx":[-110.751,-110.751,-110.751,-110.751], "fy":[122.99159,122.99159,122.99159,122.99159]}, + {"t":1.21674, "x":7.55275, "y":0.85711, "heading":1.5708, "vx":2.11403, "vy":2.11117, "omega":0.0, "ax":-8.27465, "ay":5.13891, "alpha":0.0, "fx":[-140.7494,-140.7494,-140.7494,-140.7494], "fy":[87.41144,87.41144,87.41144,87.41144]}, + {"t":1.26354, "x":7.64262, "y":0.96154, "heading":1.5708, "vx":1.72679, "vy":2.35166, "omega":0.0, "ax":-8.96407, "ay":3.8262, "alpha":0.0, "fx":[-152.47628,-152.47628,-152.47628,-152.47628], "fy":[65.08257,65.08257,65.08257,65.08257]}, + {"t":1.31034, "x":7.71362, "y":1.07578, "heading":1.5708, "vx":1.30729, "vy":2.53071, "omega":0.0, "ax":-9.26781, "ay":3.02852, "alpha":0.0, "fx":[-157.64283,-157.64283,-157.64283,-157.64283], "fy":[51.5142,51.5142,51.5142,51.5142]}, + {"t":1.35714, "x":7.76465, "y":1.19753, "heading":1.5708, "vx":0.87358, "vy":2.67244, "omega":0.0, "ax":-9.42477, "ay":2.5067, "alpha":0.0, "fx":[-160.31267,-160.31267,-160.31267,-160.31267], "fy":[42.63818,42.63818,42.63818,42.63818]}, + {"t":1.40394, "x":7.79521, "y":1.32534, "heading":1.5708, "vx":0.43252, "vy":2.78975, "omega":0.0, "ax":-9.26749, "ay":3.004, "alpha":0.0, "fx":[-157.63742,-157.63742,-157.63742,-157.63742], "fy":[51.09726,51.09726,51.09726,51.09726]}, + {"t":1.44601, "x":7.8052, "y":1.44538, "heading":1.5708, "vx":0.04259, "vy":2.91614, "omega":0.0, "ax":-3.01713, "ay":1.94817, "alpha":0.0, "fx":[-51.32047,-51.32047,-51.32047,-51.32047], "fy":[33.13778,33.13778,33.13778,33.13778]}, + {"t":1.48808, "x":7.80432, "y":1.5698, "heading":1.5708, "vx":-0.08435, "vy":2.99811, "omega":0.0, "ax":0.87649, "ay":0.01931, "alpha":0.0, "fx":[14.90881,14.90881,14.90881,14.90881], "fy":[0.3285,0.3285,0.3285,0.3285]}, + {"t":1.53016, "x":7.80155, "y":1.69596, "heading":1.5708, "vx":-0.04747, "vy":2.99892, "omega":0.0, "ax":0.38228, "ay":0.00502, "alpha":0.0, "fx":[6.5025,6.5025,6.5025,6.5025], "fy":[0.08546,0.08546,0.08546,0.08546]}, + {"t":1.57223, "x":7.79989, "y":1.82214, "heading":1.5708, "vx":-0.03139, "vy":2.99913, "omega":0.0, "ax":0.2206, "ay":0.00197, "alpha":0.0, "fx":[3.75241,3.75241,3.75241,3.75241], "fy":[0.03344,0.03344,0.03344,0.03344]}, + {"t":1.61431, "x":7.79877, "y":1.94833, "heading":1.5708, "vx":-0.02211, "vy":2.99922, "omega":0.0, "ax":0.15127, "ay":0.00095, "alpha":0.0, "fx":[2.57305,2.57305,2.57305,2.57305], "fy":[0.01622,0.01622,0.01622,0.01622]}, + {"t":1.65638, "x":7.79797, "y":2.07452, "heading":1.5708, "vx":-0.01574, "vy":2.99926, "omega":0.0, "ax":0.11267, "ay":0.0005, "alpha":0.0, "fx":[1.91641,1.91641,1.91641,1.91641], "fy":[0.00853,0.00853,0.00853,0.00853]}, + {"t":1.69846, "x":7.79741, "y":2.20071, "heading":1.5708, "vx":-0.011, "vy":2.99928, "omega":0.0, "ax":0.08874, "ay":0.00027, "alpha":0.0, "fx":[1.50943,1.50943,1.50943,1.50943], "fy":[0.00458,0.00458,0.00458,0.00458]}, + {"t":1.74053, "x":7.79702, "y":2.32691, "heading":1.5708, "vx":-0.00727, "vy":2.99929, "omega":0.0, "ax":0.07388, "ay":0.00014, "alpha":0.0, "fx":[1.25663,1.25663,1.25663,1.25663], "fy":[0.00238,0.00238,0.00238,0.00238]}, + {"t":1.78261, "x":7.79678, "y":2.4531, "heading":1.5708, "vx":-0.00416, "vy":2.9993, "omega":0.0, "ax":0.06563, "ay":0.00006, "alpha":0.0, "fx":[1.11643,1.11643,1.11643,1.11643], "fy":[0.00102,0.00102,0.00102,0.00102]}, + {"t":1.82468, "x":7.79667, "y":2.57929, "heading":1.5708, "vx":-0.0014, "vy":2.9993, "omega":0.0, "ax":0.0629, "ay":0.0, "alpha":0.0, "fx":[1.06999,1.06999,1.06999,1.06999], "fy":[0.00002,0.00002,0.00002,0.00002]}, + {"t":1.86676, "x":7.79666, "y":2.70549, "heading":1.5708, "vx":0.00125, "vy":2.9993, "omega":0.0, "ax":0.06535, "ay":-0.00006, "alpha":0.0, "fx":[1.11163,1.11163,1.11163,1.11163], "fy":[-0.00098,-0.00098,-0.00098,-0.00098]}, + {"t":1.90883, "x":7.79677, "y":2.83168, "heading":1.5708, "vx":0.004, "vy":2.9993, "omega":0.0, "ax":0.07328, "ay":-0.00014, "alpha":0.0, "fx":[1.24653,1.24653,1.24653,1.24653], "fy":[-0.00231,-0.00231,-0.00231,-0.00231]}, + {"t":1.9509, "x":7.79701, "y":2.95788, "heading":1.5708, "vx":0.00708, "vy":2.99929, "omega":0.0, "ax":0.08781, "ay":-0.00026, "alpha":0.0, "fx":[1.49358,1.49358,1.49358,1.49358], "fy":[-0.00445,-0.00445,-0.00445,-0.00445]}, + {"t":1.99298, "x":7.79738, "y":3.08407, "heading":1.5708, "vx":0.01078, "vy":2.99928, "omega":0.0, "ax":0.11187, "ay":-0.00049, "alpha":0.0, "fx":[1.90292,1.90292,1.90292,1.90292], "fy":[-0.00833,-0.00833,-0.00833,-0.00833]}, + {"t":2.03505, "x":7.79793, "y":3.21026, "heading":1.5708, "vx":0.01548, "vy":2.99926, "omega":0.0, "ax":0.15781, "ay":-0.00099, "alpha":0.0, "fx":[2.68438,2.68438,2.68438,2.68438], "fy":[-0.01683,-0.01683,-0.01683,-0.01683]}, + {"t":2.07713, "x":7.79873, "y":3.33645, "heading":1.5708, "vx":0.02212, "vy":2.99922, "omega":0.0, "ax":0.32125, "ay":-0.00545, "alpha":0.0, "fx":[5.46437,5.46437,5.46437,5.46437], "fy":[-0.09263,-0.09263,-0.09263,-0.09263]}, + {"t":2.1192, "x":7.79994, "y":3.46264, "heading":1.5708, "vx":0.03564, "vy":2.99899, "omega":0.0, "ax":0.05531, "ay":-9.23843, "alpha":0.0, "fx":[0.94075,0.94075,0.94075,0.94075], "fy":[-157.14308,-157.14308,-157.14308,-157.14308]}, + {"t":2.16128, "x":7.80149, "y":3.58064, "heading":1.5708, "vx":0.03797, "vy":2.61029, "omega":0.0, "ax":-0.12294, "ay":-9.70283, "alpha":0.0, "fx":[-2.09121,-2.09121,-2.09121,-2.09121], "fy":[-165.04235,-165.04235,-165.04235,-165.04235]}, + {"t":2.20335, "x":7.80298, "y":3.68188, "heading":1.5708, "vx":0.03279, "vy":2.20204, "omega":0.0, "ax":-0.17444, "ay":-9.72991, "alpha":0.0, "fx":[-2.96726,-2.96726,-2.96726,-2.96726], "fy":[-165.50292,-165.50292,-165.50292,-165.50292]}, + {"t":2.24543, "x":7.8042, "y":3.76592, "heading":1.5708, "vx":0.02545, "vy":1.79266, "omega":0.0, "ax":-0.25446, "ay":-9.73819, "alpha":0.0, "fx":[-4.3283,-4.3283,-4.3283,-4.3283], "fy":[-165.64377,-165.64377,-165.64377,-165.64377]}, + {"t":2.2875, "x":7.80505, "y":3.83273, "heading":1.5708, "vx":0.01475, "vy":1.38293, "omega":0.0, "ax":-0.5469, "ay":-9.73135, "alpha":0.0, "fx":[-9.30265,-9.30265,-9.30265,-9.30265], "fy":[-165.5274,-165.5274,-165.5274,-165.5274]}, + {"t":2.32958, "x":7.80519, "y":3.8823, "heading":1.5708, "vx":-0.00826, "vy":0.97349, "omega":0.0, "ax":-2.15269, "ay":-9.50954, "alpha":0.0, "fx":[-36.61662,-36.61662,-36.61662,-36.61662], "fy":[-161.75456,-161.75456,-161.75456,-161.75456]}, + {"t":2.37165, "x":7.80293, "y":3.91484, "heading":1.5708, "vx":-0.09884, "vy":0.57338, "omega":0.0, "ax":-4.02989, "ay":-8.88141, "alpha":0.0, "fx":[-68.54735,-68.54735,-68.54735,-68.54735], "fy":[-151.0703,-151.0703,-151.0703,-151.0703]}, + {"t":2.41372, "x":7.79521, "y":3.9311, "heading":1.5708, "vx":-0.26839, "vy":0.1997, "omega":0.0, "ax":-4.64597, "ay":-8.57627, "alpha":0.0, "fx":[-79.02665,-79.02665,-79.02665,-79.02665], "fy":[-145.87993,-145.87993,-145.87993,-145.87993]}, + {"t":2.45815, "x":7.7787, "y":3.93151, "heading":1.5708, "vx":-0.47479, "vy":-0.1813, "omega":0.0, "ax":-4.63899, "ay":-8.5783, "alpha":0.0, "fx":[-78.90788,-78.90788,-78.90788,-78.90788], "fy":[-145.91439,-145.91439,-145.91439,-145.91439]}, + {"t":2.50257, "x":7.75303, "y":3.91499, "heading":1.5708, "vx":-0.68087, "vy":-0.56238, "omega":0.0, "ax":-4.62912, "ay":-8.58115, "alpha":0.0, "fx":[-78.74003,-78.74003,-78.74003,-78.74003], "fy":[-145.96289,-145.96289,-145.96289,-145.96289]}, + {"t":2.547, "x":7.71821, "y":3.88154, "heading":1.5708, "vx":-0.88652, "vy":-0.9436, "omega":0.0, "ax":-4.61412, "ay":-8.58546, "alpha":0.0, "fx":[-78.48489,-78.48489,-78.48489,-78.48489], "fy":[-146.03619,-146.03619,-146.03619,-146.03619]}, + {"t":2.59142, "x":7.67428, "y":3.83115, "heading":1.5708, "vx":-1.0915, "vy":-1.325, "omega":0.0, "ax":-4.58858, "ay":-8.59272, "alpha":0.0, "fx":[-78.05037,-78.05037,-78.05037,-78.05037], "fy":[-146.15978,-146.15978,-146.15978,-146.15978]}, + {"t":2.63585, "x":7.62126, "y":3.76381, "heading":1.5708, "vx":-1.29534, "vy":-1.70673, "omega":0.0, "ax":-4.5353, "ay":-8.6076, "alpha":0.0, "fx":[-77.14417,-77.14417,-77.14417,-77.14417], "fy":[-146.41281,-146.41281,-146.41281,-146.41281]}, + {"t":2.68027, "x":7.55924, "y":3.6795, "heading":1.5708, "vx":-1.49682, "vy":-2.08912, "omega":0.0, "ax":-4.35514, "ay":-8.65532, "alpha":0.0, "fx":[-74.07975,-74.07975,-74.07975,-74.07975], "fy":[-147.22451,-147.22451,-147.22451,-147.22451]}, + {"t":2.7247, "x":7.48845, "y":3.57815, "heading":1.5708, "vx":-1.6903, "vy":-2.47362, "omega":0.0, "ax":4.97585, "ay":-3.17804, "alpha":0.0, "fx":[84.63787,84.63787,84.63787,84.63787], "fy":[-54.05759,-54.05759,-54.05759,-54.05759]}, + {"t":2.76912, "x":7.41827, "y":3.46512, "heading":1.5708, "vx":-1.46925, "vy":-2.61481, "omega":0.0, "ax":0.60481, "ay":-0.33579, "alpha":0.0, "fx":[10.28766,10.28766,10.28766,10.28766], "fy":[-5.71166,-5.71166,-5.71166,-5.71166]}, + {"t":2.81354, "x":7.35359, "y":3.34863, "heading":1.5708, "vx":-1.44238, "vy":-2.62972, "omega":0.0, "ax":0.04559, "ay":-0.02498, "alpha":0.0, "fx":[0.77543,0.77543,0.77543,0.77543], "fy":[-0.42493,-0.42493,-0.42493,-0.42493]}, + {"t":2.85797, "x":7.28956, "y":3.23178, "heading":1.5708, "vx":-1.44035, "vy":-2.63083, "omega":0.0, "ax":0.00363, "ay":-0.00199, "alpha":0.0, "fx":[0.06175,0.06175,0.06175,0.06175], "fy":[-0.03381,-0.03381,-0.03381,-0.03381]}, + {"t":2.90239, "x":7.22558, "y":3.1149, "heading":1.5708, "vx":-1.44019, "vy":-2.63092, "omega":0.0, "ax":0.00056, "ay":-0.00031, "alpha":0.0, "fx":[0.00952,0.00952,0.00952,0.00952], "fy":[-0.00522,-0.00522,-0.00522,-0.00522]}, + {"t":2.94682, "x":7.1616, "y":2.99803, "heading":1.5708, "vx":-1.44017, "vy":-2.63094, "omega":0.0, "ax":0.00039, "ay":-0.00021, "alpha":0.0, "fx":[0.00661,0.00661,0.00661,0.00661], "fy":[-0.00363,-0.00363,-0.00363,-0.00363]}, + {"t":2.99124, "x":7.09762, "y":2.88115, "heading":1.5708, "vx":-1.44015, "vy":-2.63095, "omega":0.0, "ax":0.00044, "ay":-0.00024, "alpha":0.0, "fx":[0.00744,0.00744,0.00744,0.00744], "fy":[-0.00408,-0.00408,-0.00408,-0.00408]}, + {"t":3.03567, "x":7.03364, "y":2.76427, "heading":1.5708, "vx":-1.44013, "vy":-2.63096, "omega":0.0, "ax":0.00051, "ay":-0.00028, "alpha":0.0, "fx":[0.00863,0.00863,0.00863,0.00863], "fy":[-0.00473,-0.00473,-0.00473,-0.00473]}, + {"t":3.08009, "x":6.96967, "y":2.64739, "heading":1.5708, "vx":-1.44011, "vy":-2.63097, "omega":0.0, "ax":0.00058, "ay":-0.00032, "alpha":0.0, "fx":[0.00992,0.00992,0.00992,0.00992], "fy":[-0.00544,-0.00544,-0.00544,-0.00544]}, + {"t":3.12452, "x":6.90569, "y":2.53051, "heading":1.5708, "vx":-1.44008, "vy":-2.63098, "omega":0.0, "ax":0.00067, "ay":-0.00037, "alpha":0.0, "fx":[0.01133,0.01133,0.01133,0.01133], "fy":[-0.00621,-0.00621,-0.00621,-0.00621]}, + {"t":3.16894, "x":6.84172, "y":2.41363, "heading":1.5708, "vx":-1.44005, "vy":-2.631, "omega":0.0, "ax":0.00076, "ay":-0.00042, "alpha":0.0, "fx":[0.01288,0.01288,0.01288,0.01288], "fy":[-0.00706,-0.00706,-0.00706,-0.00706]}, + {"t":3.21337, "x":6.77774, "y":2.29675, "heading":1.5708, "vx":-1.44002, "vy":-2.63102, "omega":0.0, "ax":0.00086, "ay":-0.00047, "alpha":0.0, "fx":[0.01458,0.01458,0.01458,0.01458], "fy":[-0.00799,-0.00799,-0.00799,-0.00799]}, + {"t":3.25779, "x":6.71377, "y":2.17987, "heading":1.5708, "vx":-1.43998, "vy":-2.63104, "omega":0.0, "ax":0.00097, "ay":-0.00053, "alpha":0.0, "fx":[0.01647,0.01647,0.01647,0.01647], "fy":[-0.00903,-0.00903,-0.00903,-0.00903]}, + {"t":3.30221, "x":6.6498, "y":2.06298, "heading":1.5708, "vx":-1.43994, "vy":-2.63106, "omega":0.0, "ax":0.00109, "ay":-0.0006, "alpha":0.0, "fx":[0.0186,0.0186,0.0186,0.0186], "fy":[-0.01019,-0.01019,-0.01019,-0.01019]}, + {"t":3.34664, "x":6.58584, "y":1.9461, "heading":1.5708, "vx":-1.43989, "vy":-2.63109, "omega":0.0, "ax":0.00123, "ay":-0.00068, "alpha":0.0, "fx":[0.021,0.021,0.021,0.021], "fy":[-0.01151,-0.01151,-0.01151,-0.01151]}, + {"t":3.39106, "x":6.52187, "y":1.82922, "heading":1.5708, "vx":-1.43983, "vy":-2.63112, "omega":0.0, "ax":0.0014, "ay":-0.00076, "alpha":0.0, "fx":[0.02374,0.02374,0.02374,0.02374], "fy":[-0.01301,-0.01301,-0.01301,-0.01301]}, + {"t":3.43549, "x":6.45791, "y":1.71233, "heading":1.5708, "vx":-1.43977, "vy":-2.63115, "omega":0.0, "ax":0.00158, "ay":-0.00087, "alpha":0.0, "fx":[0.02689,0.02689,0.02689,0.02689], "fy":[-0.01473,-0.01473,-0.01473,-0.01473]}, + {"t":3.47991, "x":6.39395, "y":1.59544, "heading":1.5708, "vx":-1.4397, "vy":-2.63119, "omega":0.0, "ax":0.00178, "ay":-0.00098, "alpha":0.0, "fx":[0.03029,0.03029,0.03029,0.03029], "fy":[-0.01659,-0.01659,-0.01659,-0.01659]}, + {"t":3.52434, "x":6.32999, "y":1.47855, "heading":1.5708, "vx":-1.43962, "vy":-2.63123, "omega":0.0, "ax":0.00178, "ay":-0.00097, "alpha":0.0, "fx":[0.03026,0.03026,0.03026,0.03026], "fy":[-0.01658,-0.01658,-0.01658,-0.01658]}, + {"t":3.56876, "x":6.26604, "y":1.36166, "heading":1.5708, "vx":-1.43954, "vy":-2.63128, "omega":0.0, "ax":-0.00143, "ay":0.00078, "alpha":0.0, "fx":[-0.02433,-0.02433,-0.02433,-0.02433], "fy":[0.01328,0.01328,0.01328,0.01328]}, + {"t":3.61319, "x":6.20209, "y":1.24476, "heading":1.5708, "vx":-1.43961, "vy":-2.63124, "omega":0.0, "ax":-0.04841, "ay":0.02651, "alpha":0.0, "fx":[-0.82352,-0.82352,-0.82352,-0.82352], "fy":[0.45097,0.45097,0.45097,0.45097]}, + {"t":3.65761, "x":6.13809, "y":1.1279, "heading":1.5708, "vx":-1.44176, "vy":-2.63007, "omega":0.0, "ax":-0.67803, "ay":0.37678, "alpha":0.0, "fx":[-11.53308,-11.53308,-11.53308,-11.53308], "fy":[6.40885,6.40885,6.40885,6.40885]}, + {"t":3.70203, "x":6.07337, "y":1.01143, "heading":1.5708, "vx":-1.47188, "vy":-2.61333, "omega":0.0, "ax":-5.27758, "ay":3.3021, "alpha":0.0, "fx":[-89.77013,-89.77013,-89.77013,-89.77013], "fy":[56.16769,56.16769,56.16769,56.16769]}, + {"t":3.74646, "x":6.00277, "y":0.89859, "heading":1.5708, "vx":-1.70633, "vy":-2.46663, "omega":0.0, "ax":-7.23475, "ay":5.77767, "alpha":0.0, "fx":[-123.06101,-123.06101,-123.06101,-123.06101], "fy":[98.27657,98.27657,98.27657,98.27657]}, + {"t":3.79088, "x":5.91983, "y":0.79472, "heading":1.5708, "vx":-2.02773, "vy":-2.20996, "omega":0.0, "ax":-6.58885, "ay":6.97295, "alpha":0.0, "fx":[-112.07453,-112.07453,-112.07453,-112.07453], "fy":[118.60791,118.60791,118.60791,118.60791]}, + {"t":3.83531, "x":5.82325, "y":0.70342, "heading":1.5708, "vx":-2.32044, "vy":-1.90019, "omega":0.0, "ax":-5.55856, "ay":7.92471, "alpha":0.0, "fx":[-94.5495,-94.5495,-94.5495,-94.5495], "fy":[134.79711,134.79711,134.79711,134.79711]}, + {"t":3.87973, "x":5.71468, "y":0.62683, "heading":1.5708, "vx":-2.56737, "vy":-1.54814, "omega":0.0, "ax":-4.52692, "ay":8.54097, "alpha":0.0, "fx":[-77.00153,-77.00153,-77.00153,-77.00153], "fy":[145.27941,145.27941,145.27941,145.27941]}, + {"t":3.9157, "x":5.6194, "y":0.57666, "heading":1.5708, "vx":-2.73022, "vy":-1.24091, "omega":0.0, "ax":-3.46436, "ay":8.9513, "alpha":0.0, "fx":[-58.92774,-58.92774,-58.92774,-58.92774], "fy":[152.25908,152.25908,152.25908,152.25908]}, + {"t":3.95168, "x":5.51895, "y":0.53782, "heading":1.5708, "vx":-2.85483, "vy":-0.91892, "omega":0.0, "ax":-2.37724, "ay":9.11896, "alpha":0.0, "fx":[-40.43619,-40.43619,-40.43619,-40.43619], "fy":[155.11082,155.11082,155.11082,155.11082]}, + {"t":3.98765, "x":5.41472, "y":0.51066, "heading":1.5708, "vx":-2.94035, "vy":-0.59089, "omega":0.0, "ax":-1.27436, "ay":8.68125, "alpha":0.0, "fx":[-21.67651,-21.67651,-21.67651,-21.67651], "fy":[147.66565,147.66565,147.66565,147.66565]}, + {"t":4.02362, "x":5.30812, "y":0.49502, "heading":1.5708, "vx":-2.98619, "vy":-0.27861, "omega":0.0, "ax":-0.32057, "ay":5.15843, "alpha":0.0, "fx":[-5.45287,-5.45287,-5.45287,-5.45287], "fy":[87.74335,87.74335,87.74335,87.74335]}, + {"t":4.05959, "x":5.2005, "y":0.48834, "heading":1.5708, "vx":-2.99772, "vy":-0.09306, "omega":0.0, "ax":-0.02209, "ay":0.85128, "alpha":0.0, "fx":[-0.3757,-0.3757,-0.3757,-0.3757], "fy":[14.48003,14.48003,14.48003,14.48003]}, + {"t":4.09556, "x":5.09265, "y":0.48554, "heading":1.5708, "vx":-2.99851, "vy":-0.06244, "omega":0.0, "ax":-0.00135, "ay":0.06597, "alpha":0.0, "fx":[-0.02294,-0.02294,-0.02294,-0.02294], "fy":[1.12219,1.12219,1.12219,1.12219]}, + {"t":4.13153, "x":4.98479, "y":0.48334, "heading":1.5708, "vx":-2.99856, "vy":-0.06006, "omega":0.0, "ax":0.00067, "ay":-0.03315, "alpha":0.0, "fx":[0.0114,0.0114,0.0114,0.0114], "fy":[-0.56391,-0.56391,-0.56391,-0.56391]}, + {"t":4.16751, "x":4.87692, "y":0.48116, "heading":1.5708, "vx":-2.99854, "vy":-0.06125, "omega":0.0, "ax":0.00095, "ay":-0.04608, "alpha":0.0, "fx":[0.01622,0.01622,0.01622,0.01622], "fy":[-0.78382,-0.78382,-0.78382,-0.78382]}, + {"t":4.20348, "x":4.76906, "y":0.47892, "heading":1.5708, "vx":-2.9985, "vy":-0.06291, "omega":0.0, "ax":0.00103, "ay":-0.04832, "alpha":0.0, "fx":[0.01747,0.01747,0.01747,0.01747], "fy":[-0.82186,-0.82186,-0.82186,-0.82186]}, + {"t":4.23945, "x":4.6612, "y":0.47663, "heading":1.5708, "vx":-2.99847, "vy":-0.06465, "omega":0.0, "ax":0.00108, "ay":-0.04926, "alpha":0.0, "fx":[0.0183,0.0183,0.0183,0.0183], "fy":[-0.83791,-0.83791,-0.83791,-0.83791]}, + {"t":4.27542, "x":4.55334, "y":0.47427, "heading":1.5708, "vx":-2.99843, "vy":-0.06642, "omega":0.0, "ax":0.00112, "ay":-0.05007, "alpha":0.0, "fx":[0.01911,0.01911,0.01911,0.01911], "fy":[-0.85167,-0.85167,-0.85167,-0.85167]}, + {"t":4.31139, "x":4.44549, "y":0.47185, "heading":1.5708, "vx":-2.99839, "vy":-0.06822, "omega":0.0, "ax":0.00117, "ay":-0.05077, "alpha":0.0, "fx":[0.0199,0.0199,0.0199,0.0199], "fy":[-0.86357,-0.86357,-0.86356,-0.86356]}, + {"t":4.34736, "x":4.33763, "y":0.46936, "heading":1.5708, "vx":-2.99835, "vy":-0.07005, "omega":0.0, "ax":0.00119, "ay":-0.05038, "alpha":0.0, "fx":[0.02027,0.02027,0.02027,0.02027], "fy":[-0.85701,-0.85701,-0.85701,-0.85701]}, + {"t":4.38334, "x":4.22977, "y":0.46681, "heading":1.5708, "vx":-2.9983, "vy":-0.07186, "omega":0.0, "ax":0.00099, "ay":-0.0409, "alpha":0.0, "fx":[0.01683,0.01683,0.01683,0.01683], "fy":[-0.69564,-0.69564,-0.69563,-0.69563]}, + {"t":4.41931, "x":4.12192, "y":0.4642, "heading":1.5708, "vx":-2.99827, "vy":-0.07333, "omega":0.0, "ax":-0.00102, "ay":0.04217, "alpha":0.0, "fx":[-0.01738,-0.01738,-0.01738,-0.01738], "fy":[0.71738,0.71738,0.71738,0.71738]}, + {"t":4.45528, "x":4.01407, "y":0.46159, "heading":1.5708, "vx":-2.9983, "vy":-0.07182, "omega":0.0, "ax":-0.01402, "ay":0.71333, "alpha":0.0, "fx":[-0.23855,-0.23854,-0.23854,-0.23855], "fy":[12.13346,12.13346,12.13346,12.13346]}, + {"t":4.49125, "x":3.9062, "y":0.45947, "heading":1.5708, "vx":-2.99881, "vy":-0.04616, "omega":0.0, "ax":0.06053, "ay":4.70506, "alpha":0.0, "fx":[1.02952,1.02953,1.02953,1.02952], "fy":[80.0317,80.0317,80.0317,80.0317]}, + {"t":4.52722, "x":3.79837, "y":0.46085, "heading":1.5708, "vx":-2.99663, "vy":0.12309, "omega":0.0, "ax":0.80597, "ay":8.62951, "alpha":0.0, "fx":[13.70933,13.70934,13.70934,13.70933], "fy":[146.78542,146.78542,146.78542,146.78542]}, + {"t":4.56319, "x":3.6911, "y":0.47086, "heading":1.5708, "vx":-2.96764, "vy":0.43351, "omega":0.0, "ax":1.88244, "ay":9.21035, "alpha":0.0, "fx":[32.01969,32.01969,32.01969,32.01969], "fy":[156.6655,156.6655,156.6655,156.6655]}, + {"t":4.59917, "x":3.58557, "y":0.49241, "heading":1.5708, "vx":-2.89993, "vy":0.76482, "omega":0.0, "ax":2.97736, "ay":9.1167, "alpha":0.0, "fx":[50.64408,50.64408,50.64408,50.64408], "fy":[155.07246,155.07246,155.07246,155.07246]}, + {"t":4.63514, "x":3.48318, "y":0.52582, "heading":1.5708, "vx":-2.79282, "vy":1.09276, "omega":0.0, "ax":4.0403, "ay":8.77786, "alpha":0.0, "fx":[68.72441,68.72441,68.72441,68.72441], "fy":[149.30887,149.30887,149.30887,149.30887]}, + {"t":4.67111, "x":3.38533, "y":0.57081, "heading":1.5708, "vx":-2.64749, "vy":1.40852, "omega":0.0, "ax":5.07137, "ay":8.26608, "alpha":0.0, "fx":[86.26256,86.26256,86.26256,86.26256], "fy":[140.60364,140.60364,140.60364,140.60364]}, + {"t":4.70708, "x":3.29338, "y":0.62683, "heading":1.5708, "vx":-2.46506, "vy":1.70586, "omega":0.0, "ax":6.06839, "ay":7.57754, "alpha":0.0, "fx":[103.22162,103.22162,103.22162,103.22162], "fy":[128.89179,128.89179,128.89179,128.89179]}, + {"t":4.75217, "x":3.18839, "y":0.71145, "heading":1.5708, "vx":-2.19143, "vy":2.04755, "omega":0.0, "ax":7.0923, "ay":6.56593, "alpha":0.0, "fx":[120.63791,120.63791,120.63791,120.63791], "fy":[111.68467,111.68467,111.68467,111.68467]}, + {"t":4.79726, "x":3.09679, "y":0.81045, "heading":1.5708, "vx":-1.87163, "vy":2.34361, "omega":0.0, "ax":7.86264, "ay":5.40508, "alpha":0.0, "fx":[133.74123,133.74123,133.74123,133.74123], "fy":[91.93886,91.93886,91.93886,91.93886]}, + {"t":4.84235, "x":3.02039, "y":0.92162, "heading":1.5708, "vx":-1.51709, "vy":2.58734, "omega":0.0, "ax":7.97984, "ay":3.98627, "alpha":0.0, "fx":[135.73473,135.73473,135.73473,135.73473], "fy":[67.80529,67.80529,67.80529,67.80529]}, + {"t":4.88745, "x":2.96009, "y":1.04234, "heading":1.5708, "vx":-1.15726, "vy":2.76709, "omega":0.0, "ax":3.45774, "ay":1.33429, "alpha":0.0, "fx":[58.81516,58.81517,58.81517,58.81516], "fy":[22.6959,22.6959,22.6959,22.6959]}, + {"t":4.93254, "x":2.91142, "y":1.16847, "heading":1.5708, "vx":-1.00135, "vy":2.82725, "omega":0.0, "ax":0.28321, "ay":0.09959, "alpha":0.0, "fx":[4.8174,4.81741,4.81741,4.8174], "fy":[1.69401,1.69401,1.69401,1.69401]}, + {"t":4.97763, "x":2.86656, "y":1.29606, "heading":1.5708, "vx":-0.98858, "vy":2.83174, "omega":0.0, "ax":0.01586, "ay":0.00553, "alpha":0.0, "fx":[0.26981,0.26981,0.26981,0.26981], "fy":[0.09413,0.09413,0.09414,0.09414]}, + {"t":5.02272, "x":2.822, "y":1.42375, "heading":1.5708, "vx":-0.98786, "vy":2.83199, "omega":0.0, "ax":-0.00284, "ay":-0.00099, "alpha":0.0, "fx":[-0.04828,-0.04828,-0.04828,-0.04828], "fy":[-0.01686,-0.01686,-0.01686,-0.01686]}, + {"t":5.06781, "x":2.77745, "y":1.55145, "heading":1.5708, "vx":-0.98799, "vy":2.83195, "omega":0.0, "ax":-0.00403, "ay":-0.00141, "alpha":0.0, "fx":[-0.06853,-0.06852,-0.06852,-0.06853], "fy":[-0.02392,-0.02392,-0.02392,-0.02392]}, + {"t":5.1129, "x":2.7329, "y":1.67915, "heading":1.5708, "vx":-0.98817, "vy":2.83188, "omega":0.0, "ax":-0.00401, "ay":-0.0014, "alpha":0.0, "fx":[-0.06829,-0.06829,-0.06829,-0.06829], "fy":[-0.02384,-0.02384,-0.02384,-0.02384]}, + {"t":5.158, "x":2.68833, "y":1.80684, "heading":1.5708, "vx":-0.98835, "vy":2.83182, "omega":0.0, "ax":-0.00394, "ay":-0.00138, "alpha":0.0, "fx":[-0.067,-0.067,-0.067,-0.067], "fy":[-0.0234,-0.0234,-0.0234,-0.0234]}, + {"t":5.20309, "x":2.64376, "y":1.93453, "heading":1.5708, "vx":-0.98853, "vy":2.83176, "omega":0.0, "ax":-0.00388, "ay":-0.00135, "alpha":0.0, "fx":[-0.06594,-0.06594,-0.06594,-0.06594], "fy":[-0.02303,-0.02303,-0.02303,-0.02303]}, + {"t":5.24818, "x":2.59918, "y":2.06222, "heading":1.5708, "vx":-0.98871, "vy":2.8317, "omega":0.0, "ax":-0.00383, "ay":-0.00134, "alpha":0.0, "fx":[-0.06516,-0.06516,-0.06516,-0.06516], "fy":[-0.02276,-0.02276,-0.02276,-0.02276]}, + {"t":5.29327, "x":2.5546, "y":2.1899, "heading":1.5708, "vx":-0.98888, "vy":2.83164, "omega":0.0, "ax":-0.0038, "ay":-0.00133, "alpha":0.0, "fx":[-0.06461,-0.06461,-0.06461,-0.06461], "fy":[-0.02257,-0.02257,-0.02257,-0.02257]}, + {"t":5.33836, "x":2.51, "y":2.31758, "heading":1.5708, "vx":-0.98905, "vy":2.83158, "omega":0.0, "ax":-0.00378, "ay":-0.00132, "alpha":0.0, "fx":[-0.06427,-0.06427,-0.06427,-0.06427], "fy":[-0.02246,-0.02246,-0.02246,-0.02246]}, + {"t":5.38345, "x":2.4654, "y":2.44526, "heading":1.5708, "vx":-0.98922, "vy":2.83152, "omega":0.0, "ax":-0.00377, "ay":-0.00132, "alpha":0.0, "fx":[-0.0641,-0.0641,-0.0641,-0.0641], "fy":[-0.0224,-0.0224,-0.0224,-0.0224]}, + {"t":5.42855, "x":2.42079, "y":2.57294, "heading":1.5708, "vx":-0.98939, "vy":2.83146, "omega":0.0, "ax":-0.00377, "ay":-0.00132, "alpha":0.0, "fx":[-0.06409,-0.06409,-0.06409,-0.06409], "fy":[-0.0224,-0.0224,-0.0224,-0.0224]}, + {"t":5.47364, "x":2.37618, "y":2.70061, "heading":1.5708, "vx":-0.98956, "vy":2.8314, "omega":0.0, "ax":-0.00378, "ay":-0.00132, "alpha":0.0, "fx":[-0.06422,-0.06422,-0.06422,-0.06422], "fy":[-0.02245,-0.02245,-0.02245,-0.02245]}, + {"t":5.51873, "x":2.33155, "y":2.82828, "heading":1.5708, "vx":-0.98973, "vy":2.83134, "omega":0.0, "ax":-0.00379, "ay":-0.00133, "alpha":0.0, "fx":[-0.06448,-0.06449,-0.06449,-0.06448], "fy":[-0.02254,-0.02254,-0.02255,-0.02255]}, + {"t":5.56382, "x":2.28692, "y":2.95595, "heading":1.5708, "vx":-0.9899, "vy":2.83128, "omega":0.0, "ax":-0.00381, "ay":-0.00133, "alpha":0.0, "fx":[-0.06487,-0.06487,-0.06487,-0.06487], "fy":[-0.02268,-0.02268,-0.02268,-0.02268]}, + {"t":5.60891, "x":2.24228, "y":3.08361, "heading":1.5708, "vx":-0.99007, "vy":2.83122, "omega":0.0, "ax":-0.00384, "ay":-0.00134, "alpha":0.0, "fx":[-0.06537,-0.06537,-0.06537,-0.06537], "fy":[-0.02286,-0.02286,-0.02286,-0.02286]}, + {"t":5.654, "x":2.19763, "y":3.21128, "heading":1.5708, "vx":-0.99025, "vy":2.83116, "omega":0.0, "ax":-0.00388, "ay":-0.00136, "alpha":0.0, "fx":[-0.06606,-0.06607,-0.06607,-0.06606], "fy":[-0.0231,-0.0231,-0.02311,-0.02311]}, + {"t":5.6991, "x":2.15298, "y":3.33894, "heading":1.5708, "vx":-0.99042, "vy":2.8311, "omega":0.0, "ax":-0.004, "ay":-0.0014, "alpha":0.0, "fx":[-0.06804,-0.06804,-0.06804,-0.06804], "fy":[-0.0238,-0.0238,-0.02381,-0.02381]}, + {"t":5.74419, "x":2.10831, "y":3.46659, "heading":1.5708, "vx":-0.9906, "vy":2.83103, "omega":0.0, "ax":-0.00494, "ay":-0.00223, "alpha":0.0, "fx":[-0.08409,-0.08409,-0.08409,-0.08409], "fy":[-0.03798,-0.03798,-0.038,-0.038]}, + {"t":5.78928, "x":2.06364, "y":3.59425, "heading":1.5708, "vx":-0.99083, "vy":2.83093, "omega":0.0, "ax":2.66279, "ay":-7.62828, "alpha":0.0, "fx":[45.29323,45.29323,45.29323,45.29323], "fy":[-129.75479,-129.75479,-129.75479,-129.75479]}, + {"t":5.83437, "x":2.02167, "y":3.71414, "heading":1.5708, "vx":-0.87076, "vy":2.48696, "omega":0.0, "ax":3.20661, "ay":-9.16123, "alpha":0.0, "fx":[54.54347,54.54347,54.54347,54.54347], "fy":[-155.82984,-155.82984,-155.82984,-155.82984]}, + {"t":5.87946, "x":1.98566, "y":3.81697, "heading":1.5708, "vx":-0.72617, "vy":2.07387, "omega":0.0, "ax":3.21622, "ay":-9.18737, "alpha":0.0, "fx":[54.70705,54.70705,54.70705,54.70705], "fy":[-156.27453,-156.27453,-156.27453,-156.27453]}, + {"t":5.92455, "x":1.95619, "y":3.90115, "heading":1.5708, "vx":-0.58114, "vy":1.65959, "omega":0.0, "ax":3.2197, "ay":-9.19621, "alpha":0.0, "fx":[54.76619,54.76619,54.76619,54.76619], "fy":[-156.42487,-156.42487,-156.42487,-156.42487]}, + {"t":5.96964, "x":1.93326, "y":3.96663, "heading":1.5708, "vx":-0.43596, "vy":1.24492, "omega":0.0, "ax":3.2216, "ay":-9.20061, "alpha":0.0, "fx":[54.79857,54.79857,54.79857,54.79857], "fy":[-156.49975,-156.49975,-156.49975,-156.49975]}, + {"t":6.01474, "x":1.91688, "y":4.01341, "heading":1.5708, "vx":-0.29069, "vy":0.83005, "omega":0.0, "ax":3.22287, "ay":-9.20322, "alpha":0.0, "fx":[54.82017,54.82017,54.82017,54.82017], "fy":[-156.54415,-156.54415,-156.54415,-156.54415]}, + {"t":6.05983, "x":1.90704, "y":4.04148, "heading":1.5708, "vx":-0.14537, "vy":0.41506, "omega":0.0, "ax":3.22383, "ay":-9.20493, "alpha":0.0, "fx":[54.83635,54.83635,54.83635,54.83635], "fy":[-156.57322,-156.57322,-156.57322,-156.57322]}, + {"t":6.10492, "x":1.90377, "y":4.05084, "heading":1.5708, "vx":0.0, "vy":0.0, "omega":0.0, "ax":0.0, "ay":0.0, "alpha":0.0, "fx":[0.0,0.0,0.0,0.0], "fy":[0.0,0.0,0.0,0.0]}], + "splits":[0] + }, + "events":[] +} diff --git a/lib/examples/swerve/src/main/deploy/choreo/V1_DoomSpiralAutos.chor b/lib/examples/swerve/src/main/deploy/choreo/V1_DoomSpiralAutos.chor new file mode 100644 index 00000000..83e3d259 --- /dev/null +++ b/lib/examples/swerve/src/main/deploy/choreo/V1_DoomSpiralAutos.chor @@ -0,0 +1,84 @@ +{ + "name":"V1_DoomSpiralAutos", + "version":2, + "type":"Swerve", + "variables":{ + "expressions":{}, + "poses":{} + }, + "config":{ + "frontLeft":{ + "x":{ + "exp":"8.875 in", + "val":0.225425 + }, + "y":{ + "exp":"13.125 in", + "val":0.333375 + } + }, + "backLeft":{ + "x":{ + "exp":"-8.875 in", + "val":-0.225425 + }, + "y":{ + "exp":"13.125 in", + "val":0.333375 + } + }, + "mass":{ + "exp":"150 lbs", + "val":68.0388555 + }, + "inertia":{ + "exp":"7.5 kg m ^ 2", + "val":7.5 + }, + "gearing":{ + "exp":"7.03", + "val":7.03 + }, + "radius":{ + "exp":"2 in", + "val":0.0508 + }, + "vmax":{ + "exp":"5800 RPM", + "val":607.3745796940267 + }, + "tmax":{ + "exp":"1.2 N * m", + "val":1.2 + }, + "cof":{ + "exp":"1.5", + "val":1.5 + }, + "bumper":{ + "front":{ + "exp":"13.75 in", + "val":0.34925 + }, + "side":{ + "exp":"17.5 in", + "val":0.4445 + }, + "back":{ + "exp":"13.75 in", + "val":0.34925 + } + }, + "differentialTrackWidth":{ + "exp":"22 in", + "val":0.5588 + } + }, + "generationFeatures":[], + "codegen":{ + "root":"../../java/frc/robot", + "genVars":false, + "genTrajData":false, + "useChoreoLib":false + } +} diff --git a/lib/examples/swerve/src/main/java/frc/robot/Constants.java b/lib/examples/swerve/src/main/java/frc/robot/Constants.java new file mode 100644 index 00000000..191dd3d4 --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/Constants.java @@ -0,0 +1,29 @@ +package frc.robot; + +import edu.wpi.first.wpilibj.RobotBase; +import edu.wpi.team190.gompeilib.core.robot.RobotMode; + +public final class Constants { + public static final boolean TUNING_MODE = true; + public static final double LOOP_PERIOD_SECONDS = 0.02; + + public static RobotMode getMode() { + switch (RobotConfig.ROBOT) { + case V0_FUNKY, V1_DOOMSPIRAL: + return RobotBase.isReal() ? RobotMode.REAL : RobotMode.REPLAY; + + case V0_FUNKY_SIM, V1_DOOMSPIRAL_SIM: + return RobotMode.SIM; + + default: + return RobotMode.REAL; + } + } + + public static void main(String... args) { + if (getMode().equals(RobotMode.SIM)) { + System.err.println("Cannot deploy, invalid mode selected: " + RobotConfig.ROBOT); + System.exit(1); + } + } +} diff --git a/lib/examples/swerve/src/main/java/frc/robot/FieldConstants.java b/lib/examples/swerve/src/main/java/frc/robot/FieldConstants.java new file mode 100644 index 00000000..de1b67a8 --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/FieldConstants.java @@ -0,0 +1,324 @@ +// Copyright (c) 2025-2026 Littleton Robotics +// http://github.com/Mechanical-Advantage +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file at +// the root directory of this project. + +package frc.robot; + +import edu.wpi.first.apriltag.AprilTagFieldLayout; +import edu.wpi.first.apriltag.AprilTagFields; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.math.util.Units; + +/** + * Contains information for location of field element and other useful reference points. + * + *

NOTE: All constants are defined relative to the field coordinate system, and from the + * perspective of the blue alliance station + */ +public class FieldConstants { + + // AprilTag related constants + public static final int aprilTagCount = AprilTagLayoutType.ANDYMARK.getLayout().getTags().size(); + public static final double aprilTagWidth = Units.inchesToMeters(6.5); + + // Field dimensions + public static final double fieldLength = AprilTagLayoutType.ANDYMARK.getLayout().getFieldLength(); + public static final double fieldWidth = AprilTagLayoutType.ANDYMARK.getLayout().getFieldWidth(); + + /** + * Officially defined and relevant vertical lines found on the field (defined by X-axis offset) + */ + public static class LinesVertical { + public static final double center = fieldLength / 2.0; + public static final double starting = + AprilTagLayoutType.ANDYMARK.getLayout().getTagPose(26).get().getX(); + public static final double allianceZone = starting; + public static final double hubCenter = + AprilTagLayoutType.ANDYMARK.getLayout().getTagPose(26).get().getX() + Hub.width / 2.0; + public static final double neutralZoneNear = center - Units.inchesToMeters(120); + public static final double neutralZoneFar = center + Units.inchesToMeters(120); + public static final double oppHubCenter = + AprilTagLayoutType.ANDYMARK.getLayout().getTagPose(4).get().getX() + Hub.width / 2.0; + public static final double oppAllianceZone = + AprilTagLayoutType.ANDYMARK.getLayout().getTagPose(10).get().getX(); + } + + /** + * Officially defined and relevant horizontal lines found on the field (defined by Y-axis offset) + * + *

NOTE: The field element start and end are always left to right from the perspective of the + * alliance station + */ + public static class LinesHorizontal { + + public static final double center = fieldWidth / 2.0; + + // Right of hub + public static final double rightBumpStart = Hub.nearRightCorner.getY(); + public static final double rightBumpEnd = rightBumpStart - RightBump.width; + public static final double rightTrenchOpenStart = rightBumpEnd - Units.inchesToMeters(12.0); + public static final double rightTrenchOpenEnd = 0; + + // Left of hub + public static final double leftBumpEnd = Hub.nearLeftCorner.getY(); + public static final double leftBumpStart = leftBumpEnd + LeftBump.width; + public static final double leftTrenchOpenEnd = leftBumpStart + Units.inchesToMeters(12.0); + public static final double leftTrenchOpenStart = fieldWidth; + } + + /** Hub related constants */ + public static class Hub { + + // Dimensions + public static final double width = Units.inchesToMeters(47.0); + public static final double height = + Units.inchesToMeters(72.0); // includes the catcher at the top + public static final double innerWidth = Units.inchesToMeters(41.7); + public static final double innerHeight = Units.inchesToMeters(56.5); + + // Relevant reference points on alliance side + public static final Translation3d topCenterPoint = + new Translation3d( + AprilTagLayoutType.ANDYMARK.getLayout().getTagPose(26).get().getX() + width / 2.0, + fieldWidth / 2.0, + height); + public static final Translation3d innerCenterPoint = + new Translation3d( + AprilTagLayoutType.ANDYMARK.getLayout().getTagPose(26).get().getX() + width / 2.0, + fieldWidth / 2.0, + innerHeight); + + public static final Translation2d nearLeftCorner = + new Translation2d(topCenterPoint.getX() - width / 2.0, fieldWidth / 2.0 + width / 2.0); + public static final Translation2d nearRightCorner = + new Translation2d(topCenterPoint.getX() - width / 2.0, fieldWidth / 2.0 - width / 2.0); + public static final Translation2d farLeftCorner = + new Translation2d(topCenterPoint.getX() + width / 2.0, fieldWidth / 2.0 + width / 2.0); + public static final Translation2d farRightCorner = + new Translation2d(topCenterPoint.getX() + width / 2.0, fieldWidth / 2.0 - width / 2.0); + + // Relevant reference points on the opposite side + public static final Translation3d oppTopCenterPoint = + new Translation3d( + AprilTagLayoutType.ANDYMARK.getLayout().getTagPose(4).get().getX() + width / 2.0, + fieldWidth / 2.0, + height); + public static final Translation2d oppNearLeftCorner = + new Translation2d(oppTopCenterPoint.getX() - width / 2.0, fieldWidth / 2.0 + width / 2.0); + public static final Translation2d oppNearRightCorner = + new Translation2d(oppTopCenterPoint.getX() - width / 2.0, fieldWidth / 2.0 - width / 2.0); + public static final Translation2d oppFarLeftCorner = + new Translation2d(oppTopCenterPoint.getX() + width / 2.0, fieldWidth / 2.0 + width / 2.0); + public static final Translation2d oppFarRightCorner = + new Translation2d(oppTopCenterPoint.getX() + width / 2.0, fieldWidth / 2.0 - width / 2.0); + + // Hub faces + public static final Pose2d nearFace = + AprilTagLayoutType.ANDYMARK.getLayout().getTagPose(26).get().toPose2d(); + public static final Pose2d farFace = + AprilTagLayoutType.ANDYMARK.getLayout().getTagPose(20).get().toPose2d(); + public static final Pose2d rightFace = + AprilTagLayoutType.ANDYMARK.getLayout().getTagPose(18).get().toPose2d(); + public static final Pose2d leftFace = + AprilTagLayoutType.ANDYMARK.getLayout().getTagPose(21).get().toPose2d(); + } + + /** Left Bump related constants */ + public static class LeftBump { + + // Dimensions + public static final double width = Units.inchesToMeters(73.0); + public static final double height = Units.inchesToMeters(6.513); + public static final double depth = Units.inchesToMeters(44.4); + + // Relevant reference points on alliance side + public static final Translation2d nearLeftCorner = + new Translation2d(LinesVertical.hubCenter - width / 2, Units.inchesToMeters(255)); + public static final Translation2d nearRightCorner = Hub.nearLeftCorner; + public static final Translation2d farLeftCorner = + new Translation2d(LinesVertical.hubCenter + width / 2, Units.inchesToMeters(255)); + public static final Translation2d farRightCorner = Hub.farLeftCorner; + + // Relevant reference points on opposing side + public static final Translation2d oppNearLeftCorner = + new Translation2d(LinesVertical.hubCenter - width / 2, Units.inchesToMeters(255)); + public static final Translation2d oppNearRightCorner = Hub.oppNearLeftCorner; + public static final Translation2d oppFarLeftCorner = + new Translation2d(LinesVertical.hubCenter + width / 2, Units.inchesToMeters(255)); + public static final Translation2d oppFarRightCorner = Hub.oppFarLeftCorner; + } + + /** Right Bump related constants */ + public static class RightBump { + // Dimensions + public static final double width = Units.inchesToMeters(73.0); + public static final double height = Units.inchesToMeters(6.513); + public static final double depth = Units.inchesToMeters(44.4); + + // Relevant reference points on alliance side + public static final Translation2d nearLeftCorner = + new Translation2d(LinesVertical.hubCenter + width / 2, Units.inchesToMeters(255)); + public static final Translation2d nearRightCorner = Hub.nearLeftCorner; + public static final Translation2d farLeftCorner = + new Translation2d(LinesVertical.hubCenter - width / 2, Units.inchesToMeters(255)); + public static final Translation2d farRightCorner = Hub.farLeftCorner; + + // Relevant reference points on opposing side + public static final Translation2d oppNearLeftCorner = + new Translation2d(LinesVertical.hubCenter + width / 2, Units.inchesToMeters(255)); + public static final Translation2d oppNearRightCorner = Hub.oppNearLeftCorner; + public static final Translation2d oppFarLeftCorner = + new Translation2d(LinesVertical.hubCenter - width / 2, Units.inchesToMeters(255)); + public static final Translation2d oppFarRightCorner = Hub.oppFarLeftCorner; + } + + /** Left Trench related constants */ + public static class LeftTrench { + // Dimensions + public static final double width = Units.inchesToMeters(65.65); + public static final double depth = Units.inchesToMeters(47.0); + public static final double height = Units.inchesToMeters(40.25); + public static final double openingWidth = Units.inchesToMeters(50.34); + public static final double openingHeight = Units.inchesToMeters(22.25); + + // Relevant reference points on alliance side + public static final Translation3d openingTopLeft = + new Translation3d(LinesVertical.hubCenter, fieldWidth, openingHeight); + public static final Translation3d openingTopRight = + new Translation3d(LinesVertical.hubCenter, fieldWidth - openingWidth, openingHeight); + + // Relevant reference points on opposing side + public static final Translation3d oppOpeningTopLeft = + new Translation3d(LinesVertical.oppHubCenter, fieldWidth, openingHeight); + public static final Translation3d oppOpeningTopRight = + new Translation3d(LinesVertical.oppHubCenter, fieldWidth - openingWidth, openingHeight); + } + + public static class RightTrench { + + // Dimensions + public static final double width = Units.inchesToMeters(65.65); + public static final double depth = Units.inchesToMeters(47.0); + public static final double height = Units.inchesToMeters(40.25); + public static final double openingWidth = Units.inchesToMeters(50.34); + public static final double openingHeight = Units.inchesToMeters(22.25); + + // Relevant reference points on alliance side + public static final Translation3d openingTopLeft = + new Translation3d(LinesVertical.hubCenter, openingWidth, openingHeight); + public static final Translation3d openingTopRight = + new Translation3d(LinesVertical.hubCenter, 0, openingHeight); + + // Relevant reference points on opposing side + public static final Translation3d oppOpeningTopLeft = + new Translation3d(LinesVertical.oppHubCenter, openingWidth, openingHeight); + public static final Translation3d oppOpeningTopRight = + new Translation3d(LinesVertical.oppHubCenter, 0, openingHeight); + } + + /** Tower related constants */ + public static class Tower { + // Dimensions + public static final double width = Units.inchesToMeters(49.25); + public static final double depth = Units.inchesToMeters(45.0); + public static final double height = Units.inchesToMeters(78.25); + public static final double innerOpeningWidth = Units.inchesToMeters(32.250); + public static final double frontFaceX = Units.inchesToMeters(43.51); + + public static final double uprightHeight = Units.inchesToMeters(72.1); + + // Rung heights from the floor + public static final double lowRungHeight = Units.inchesToMeters(27.0); + public static final double midRungHeight = Units.inchesToMeters(45.0); + public static final double highRungHeight = Units.inchesToMeters(63.0); + + // Relevant reference points on alliance side + public static final Translation2d centerPoint = + new Translation2d( + frontFaceX, AprilTagLayoutType.ANDYMARK.getLayout().getTagPose(31).get().getY()); + public static final Translation2d leftUpright = + new Translation2d( + frontFaceX, + (AprilTagLayoutType.ANDYMARK.getLayout().getTagPose(31).get().getY()) + + innerOpeningWidth / 2 + + Units.inchesToMeters(0.75)); + public static final Translation2d rightUpright = + new Translation2d( + frontFaceX, + (AprilTagLayoutType.ANDYMARK.getLayout().getTagPose(31).get().getY()) + - innerOpeningWidth / 2 + - Units.inchesToMeters(0.75)); + + // Relevant reference points on opposing side + public static final Translation2d oppCenterPoint = + new Translation2d( + fieldLength - frontFaceX, + AprilTagLayoutType.ANDYMARK.getLayout().getTagPose(15).get().getY()); + public static final Translation2d oppLeftUpright = + new Translation2d( + fieldLength - frontFaceX, + (AprilTagLayoutType.ANDYMARK.getLayout().getTagPose(15).get().getY()) + + innerOpeningWidth / 2 + + Units.inchesToMeters(0.75)); + public static final Translation2d oppRightUpright = + new Translation2d( + fieldLength - frontFaceX, + (AprilTagLayoutType.ANDYMARK.getLayout().getTagPose(15).get().getY()) + - innerOpeningWidth / 2 + - Units.inchesToMeters(0.75)); + } + + public static class Depot { + // Dimensions + public static final double width = Units.inchesToMeters(42.0); + public static final double depth = Units.inchesToMeters(27.0); + public static final double height = Units.inchesToMeters(1.125); + public static final double distanceFromCenterY = Units.inchesToMeters(75.93); + + // Relevant reference points on alliance side + public static final Translation3d depotCenter = + new Translation3d(depth, (fieldWidth / 2) + distanceFromCenterY, height); + public static final Translation3d leftCorner = + new Translation3d(depth, (fieldWidth / 2) + distanceFromCenterY + (width / 2), height); + public static final Translation3d rightCorner = + new Translation3d(depth, (fieldWidth / 2) + distanceFromCenterY - (width / 2), height); + } + + public static class Outpost { + // Dimensions + public static final double width = Units.inchesToMeters(31.8); + public static final double openingDistanceFromFloor = Units.inchesToMeters(28.1); + public static final double height = Units.inchesToMeters(7.0); + + // Relevant reference points on alliance side + public static final Translation2d centerPoint = + new Translation2d(0, AprilTagLayoutType.ANDYMARK.getLayout().getTagPose(29).get().getY()); + } + + public enum AprilTagLayoutType { + ANDYMARK(AprilTagFields.k2026RebuiltAndymark), + WELDED(AprilTagFields.k2026RebuiltWelded); + + private final AprilTagFields name; + private volatile AprilTagFieldLayout layout; + + AprilTagLayoutType(AprilTagFields name) { + this.name = name; + } + + public AprilTagFieldLayout getLayout() { + if (layout != null) { + return layout; + } + layout = AprilTagFieldLayout.loadField(name); + return layout; + } + } + + public static final Translation2d FEED_TRANSLATION = new Translation2d(0, 0); +} diff --git a/lib/examples/swerve/src/main/java/frc/robot/Main.java b/lib/examples/swerve/src/main/java/frc/robot/Main.java new file mode 100644 index 00000000..8e389ea6 --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/Main.java @@ -0,0 +1,11 @@ +package frc.robot; + +import edu.wpi.first.wpilibj.RobotBase; + +public final class Main { + private Main() {} + + public static void main(String... args) { + RobotBase.startRobot(Robot::new); + } +} diff --git a/lib/examples/swerve/src/main/java/frc/robot/Robot.java b/lib/examples/swerve/src/main/java/frc/robot/Robot.java new file mode 100644 index 00000000..1ae16042 --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/Robot.java @@ -0,0 +1,285 @@ +package frc.robot; + +import com.ctre.phoenix6.SignalLogger; +import edu.wpi.first.math.MathShared; +import edu.wpi.first.math.MathSharedStore; +import edu.wpi.first.math.MathUsageId; +import edu.wpi.first.net.WebServer; +import edu.wpi.first.wpilibj.*; +import edu.wpi.first.wpilibj.livewindow.LiveWindow; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.CommandScheduler; +import edu.wpi.team190.gompeilib.core.GompeiLib; +import edu.wpi.team190.gompeilib.core.robot.RobotContainer; +import edu.wpi.team190.gompeilib.core.utility.VirtualSubsystem; +import edu.wpi.team190.gompeilib.core.utility.phoenix.PhoenixUtil; +import frc.robot.subsystems.v0_Funky.V0_FunkyRobotContainer; +import frc.robot.util.*; +import frc.robot.util.Alert; +import frc.robot.util.Alert.AlertType; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; +import org.littletonrobotics.junction.LogFileUtil; +import org.littletonrobotics.junction.LoggedRobot; +import org.littletonrobotics.junction.Logger; +import org.littletonrobotics.junction.networktables.NT4Publisher; +import org.littletonrobotics.junction.wpilog.WPILOGReader; +import org.littletonrobotics.junction.wpilog.WPILOGWriter; + +/** + * The VM is configured to automatically run this class, and to call the functions corresponding to + * each mode, as described in the TimedRobot documentation. If you change the name of this class or + * the package after creating this project, you must also update the build.gradle file in the + * project. + */ +public class Robot extends LoggedRobot { + private static final double LOW_BATTERY_VOLTAGE = 10.0; + private static final double LOW_BATTERY_DISABLED_TIME = 1.5; + + private static final double LOOP_OVERRUN_WARNING_TIMEOUT = 1; + private static double startupTimestamp = Double.NEGATIVE_INFINITY; + private final Timer disabledTimer = new Timer(); + private final Alert logReceiverQueueAlert = + new Alert("Logging queue exceeded capacity, data will NOT be logged.", AlertType.WARNING); + private final Elastic.Notification lowBatteryAlert = + new Elastic.Notification( + Elastic.NotificationLevel.WARNING, + "Low Battery! Replace battery soon!", + "Battery voltage is very low, consider turning off the robot or replacing the battery."); + private Command autonomousCommand; + private RobotContainer robotContainer; + + public Robot() { + super(Constants.LOOP_PERIOD_SECONDS); + GompeiLib.init(Constants.getMode(), Constants.TUNING_MODE, Constants.LOOP_PERIOD_SECONDS); + } + + public static boolean isJitting() { + return Timer.getFPGATimestamp() - startupTimestamp <= 45.0; + } + + /** + * This function is run when the robot is first started up and should be used for any + * initialization code. + */ + @Override + public void robotInit() { + SignalLogger.enableAutoLogging(false); + LiveWindow.disableAllTelemetry(); + // Record metadata + Logger.recordMetadata("ProjectName", BuildConstants.MAVEN_NAME); + Logger.recordMetadata("BuildDate", BuildConstants.BUILD_DATE); + Logger.recordMetadata("GitSHA", BuildConstants.GIT_SHA); + Logger.recordMetadata("GitDate", BuildConstants.GIT_DATE); + Logger.recordMetadata("GitBranch", BuildConstants.GIT_BRANCH); + Logger.recordMetadata("GompeiLib_Version", BuildConstants.VERSION); + switch (BuildConstants.DIRTY) { + case 0: + Logger.recordMetadata("GitDirty", "All changes committed"); + break; + case 1: + Logger.recordMetadata("GitDirty", "Uncomitted changes"); + break; + default: + Logger.recordMetadata("GitDirty", "Unknown"); + break; + } + + // Set up data receivers & replay source + switch (Constants.getMode()) { + case REAL: + // Running on a real robot, log to a USB stick ("/U/logs") + Logger.addDataReceiver(new WPILOGWriter()); + Logger.addDataReceiver(new NT4Publisher()); + break; + + case SIM: + // Running a physics simulator, log to NT + // setUseTiming(false); + Logger.addDataReceiver(new NT4Publisher()); + // setting up maple sim field + 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(); + + // Start timers + disabledTimer.restart(); + + RobotController.setBrownoutVoltage(6); + // Instantiate our RobotContainer. This will perform all our button bindings, + // and put our autonomous chooser on the dashboard. + robotContainer = + switch (RobotConfig.ROBOT) { + case V0_FUNKY, V0_FUNKY_SIM -> new V0_FunkyRobotContainer(); + default -> new RobotContainer() {}; + }; + + Elastic.selectTab("Autonomous"); + DriverStation.silenceJoystickConnectionWarning(true); + + try { + Field watchdogField = IterativeRobotBase.class.getDeclaredField("m_watchdog"); + watchdogField.setAccessible(true); + Watchdog watchdog = (Watchdog) watchdogField.get(this); + watchdog.setTimeout(LOOP_OVERRUN_WARNING_TIMEOUT); + } catch (Exception e) { + DriverStation.reportWarning("Failed to disable loop overrun warnings.", false); + } + CommandScheduler.getInstance().setPeriod(LOOP_OVERRUN_WARNING_TIMEOUT); + + // Silence Rotation2d warnings + var mathShared = MathSharedStore.getMathShared(); + MathSharedStore.setMathShared( + new MathShared() { + @Override + public void reportError(String error, StackTraceElement[] stackTrace) { + if (error.startsWith("x and y components of Rotation2d are zero")) { + return; + } + mathShared.reportError(error, stackTrace); + } + + @Override + public void reportUsage(MathUsageId id, int count) { + mathShared.reportUsage(id, count); + } + + @Override + public double getTimestamp() { + return mathShared.getTimestamp(); + } + }); + + String DEPLOY_DIR = + Filesystem.getDeployDirectory().getPath() + + RobotConfig.ROBOT.name().toLowerCase().replaceFirst("_sim", ""); + // try { + // var m = Choreo.class.getDeclaredMethod("setChoreoDir", File.class); + // m.setAccessible(true); + // m.invoke(null, new File(DEPLOY_DIR + "/choreo")); + // } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + // throw new RuntimeException(e); + // } + // 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.recordOutput( + "CommandsUnique/" + name + "_" + Integer.toHexString(command.hashCode()), active); + Logger.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)); + + WebServer.start(5800, DEPLOY_DIR); + + startupTimestamp = Timer.getFPGATimestamp(); + } + + /** This function is called periodically during all modes. */ + @Override + public void robotPeriodic() { + // Runs the Scheduler. This is responsible for polling buttons, adding + // newly-scheduled commands, running already-scheduled commands, removing + // finished or interrupted commands, and running subsystem periodic() methods. + // This must be called from the robot's periodic block in order for anything in + // the Command-based framework to work. + + PhoenixUtil.refreshAll(); + VirtualSubsystem.periodicAll(); + robotContainer.robotPeriodic(); + CommandScheduler.getInstance().run(); + + // Check logging fault + logReceiverQueueAlert.set(Logger.getReceiverQueueFault()); + + // Update low battery alert + if (RobotState.isEnabled()) { + disabledTimer.reset(); + } + if (RobotController.getBatteryVoltage() < LOW_BATTERY_VOLTAGE + && disabledTimer.hasElapsed(LOW_BATTERY_DISABLED_TIME)) { + Elastic.sendNotification(lowBatteryAlert); + } + } + + /** This function is called once when the robot is disabled. */ + @Override + public void disabledInit() {} + + /** This function is called periodically when disabled. */ + @Override + public void disabledPeriodic() {} + + /** This autonomous runs the autonomous command selected by your {@link RobotContainer} class. */ + @Override + public void autonomousInit() { + Elastic.selectTab("Autonomous"); + + autonomousCommand = robotContainer.getAutonomousCommand(); + + if (autonomousCommand != null) { + CommandScheduler.getInstance().cancelAll(); + CommandScheduler.getInstance().schedule(autonomousCommand); + } + } + + /** This function is called periodically during autonomous. */ + @Override + public void autonomousPeriodic() {} + + /** This function is called once when teleop is enabled. */ + @Override + public void teleopInit() { + // This makes sure that the autonomous stops running when + // teleop starts running. If you want the autonomous to + // continue until interrupted by another command, remove + // this line or comment it out. + if (autonomousCommand != null) { + CommandScheduler.getInstance().cancel(autonomousCommand); + } + Elastic.selectTab("Teleoperated"); + } + + /** This function is called periodically during operator control. */ + @Override + public void teleopPeriodic() {} + + /** This function is called once when test mode is enabled. */ + @Override + public void testInit() { + // Cancels all running commands at the start of test mode. + CommandScheduler.getInstance().cancelAll(); + } + + /** This function is called periodically during test mode. */ + @Override + public void testPeriodic() {} + + /** This function is called once when the robot is first started up. */ + @Override + public void simulationInit() {} + + /** This function is called periodically whilst in simulation. */ + @Override + public void simulationPeriodic() {} +} diff --git a/lib/examples/swerve/src/main/java/frc/robot/RobotConfig.java b/lib/examples/swerve/src/main/java/frc/robot/RobotConfig.java new file mode 100644 index 00000000..ebea24c8 --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/RobotConfig.java @@ -0,0 +1,12 @@ +package frc.robot; + +public final class RobotConfig { + public static final RobotType ROBOT = RobotType.V1_DOOMSPIRAL; + + public enum RobotType { + V0_FUNKY, + V0_FUNKY_SIM, + V1_DOOMSPIRAL, + V1_DOOMSPIRAL_SIM, + } +} diff --git a/lib/examples/swerve/src/main/java/frc/robot/commands/shared/AutoAlignCommand.java b/lib/examples/swerve/src/main/java/frc/robot/commands/shared/AutoAlignCommand.java new file mode 100644 index 00000000..6d5c6e92 --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/commands/shared/AutoAlignCommand.java @@ -0,0 +1,168 @@ +package frc.robot.commands.shared; + +import static edu.wpi.first.units.Units.*; + +import edu.wpi.first.math.controller.ProfiledPIDController; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.kinematics.ChassisSpeeds; +import edu.wpi.first.math.trajectory.TrapezoidProfile; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive.SwerveDrive; +import edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive.SwerveDriveConstants.AutoAlignConstants; +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; +import org.littletonrobotics.junction.Logger; + +public class AutoAlignCommand extends Command { + private final SwerveDrive drive; + private final Pose2d targetPose; + private final BooleanSupplier valid; + private final Supplier robotPose; + + private ChassisSpeeds speeds; + + private final ProfiledPIDController alignXController; + private final ProfiledPIDController alignYController; + private final ProfiledPIDController alignHeadingController; + + /** + * Creates a new AutoAlignCommand. + * + * @param drive The swerve drive subsystem on which this command will run + * @param targetPose The pose to which the robot will attempt to align + * @param valid A boolean supplier that returns true when auto aligning is possible (e.g. when + * tags are visible) + * @param robotPose A supplier that returns the robot's current pose + * @param constants The swerve drive constants + */ + public AutoAlignCommand( + SwerveDrive drive, + Pose2d targetPose, + BooleanSupplier valid, + Supplier robotPose, + AutoAlignConstants constants, + double maxAccelerationMetersPerSecond) { + this.addRequirements(drive); + + this.drive = drive; + this.targetPose = targetPose; + this.valid = valid; + this.robotPose = robotPose; + + alignXController = + new ProfiledPIDController( + constants.xGains().kP().get(), + 0.0, + constants.xGains().kD().get(), + new TrapezoidProfile.Constraints( + constants.xConstraints().maxVelocity().get().in(MetersPerSecond), + maxAccelerationMetersPerSecond)); + alignYController = + new ProfiledPIDController( + constants.yGains().kP().get(), + 0.0, + constants.yGains().kD().get(), + new TrapezoidProfile.Constraints( + constants.yConstraints().maxVelocity().get().in(MetersPerSecond), + maxAccelerationMetersPerSecond)); + alignHeadingController = + new ProfiledPIDController( + constants.rotationGains().kP().get(), + 0.0, + constants.rotationGains().kD().get(), + new TrapezoidProfile.Constraints( + constants.rotationConstraints().maxVelocity().get().in(RadiansPerSecond), + Double.POSITIVE_INFINITY)); + + alignXController.setTolerance(constants.xConstraints().goalTolerance().get().in(Meters), 0); + alignYController.setTolerance(constants.yConstraints().goalTolerance().get().in(Meters), 0); + + alignHeadingController.enableContinuousInput(-Math.PI, Math.PI); + alignHeadingController.setTolerance( + constants.rotationConstraints().goalTolerance().get().in(Radians), 0); + speeds = new ChassisSpeeds(); + } + + @Override + public void initialize() { + alignHeadingController.reset( + robotPose.get().getRotation().getRadians(), + drive.getMeasuredChassisSpeeds().omegaRadiansPerSecond); + alignXController.reset( + robotPose.get().getX(), drive.getMeasuredChassisSpeeds().vxMetersPerSecond); + alignYController.reset( + robotPose.get().getY(), drive.getMeasuredChassisSpeeds().vyMetersPerSecond); + } + + @Override + public void execute() { + + if (valid.getAsBoolean()) { + ChassisSpeeds measuredSpeeds = drive.getMeasuredChassisSpeeds(); + + double adjustedXSpeed = + calculate( + alignXController, + targetPose.getX(), + robotPose.get().getX(), + measuredSpeeds.vxMetersPerSecond); + double adjustedYSpeed = + calculate( + alignYController, + targetPose.getY(), + robotPose.get().getY(), + measuredSpeeds.vyMetersPerSecond); + double adjustedThetaSpeed = + calculate( + alignHeadingController, + targetPose.getRotation().getRadians(), + robotPose.get().getRotation().getRadians(), + measuredSpeeds.omegaRadiansPerSecond); + speeds = + ChassisSpeeds.fromFieldRelativeSpeeds( + adjustedXSpeed, adjustedYSpeed, adjustedThetaSpeed, robotPose.get().getRotation()); + + } else { + speeds = new ChassisSpeeds(); + } + Logger.recordOutput("Drive/Auto Align/speeds", speeds); + drive.runVelocity(speeds); + } + + @Override + public void end(boolean interrupted) { + drive.runVelocity(new ChassisSpeeds()); + alignHeadingController.reset(robotPose.get().getRotation().getRadians()); + alignXController.reset(robotPose.get().getX()); + alignYController.reset(robotPose.get().getY()); + } + + @Override + public boolean isFinished() { + // Return true when the command should end + return alignXController.atGoal() + && alignYController.atGoal() + && alignHeadingController.atGoal(); + } + + /** + * Calculates the PID output for the given setpoint, measurement, and speed. If the controller is + * not at the setpoint, calculate the PID output. Otherwise, reset the controller with the given + * measurement and speed. + * + * @param controller The profiled PID controller to use. + * @param setpoint The setpoint of the controller. + * @param measurement The current measurement. + * @param speed The current speed. + * @return The calculated PID output. + */ + public static double calculate( + ProfiledPIDController controller, double setpoint, double measurement, double speed) { + double pidOutput = 0.0; + + if (!controller.atSetpoint()) pidOutput = controller.calculate(measurement, setpoint); + else controller.reset(measurement, speed); + + return pidOutput; + } +} diff --git a/lib/examples/swerve/src/main/java/frc/robot/commands/shared/DriveCommands.java b/lib/examples/swerve/src/main/java/frc/robot/commands/shared/DriveCommands.java new file mode 100644 index 00000000..3fcf265c --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/commands/shared/DriveCommands.java @@ -0,0 +1,399 @@ +package frc.robot.commands.shared; + +import static edu.wpi.first.units.Units.Radians; +import static edu.wpi.first.units.Units.RadiansPerSecond; + +import edu.wpi.first.math.MathUtil; +import edu.wpi.first.math.Pair; +import edu.wpi.first.math.controller.ProfiledPIDController; +import edu.wpi.first.math.filter.SlewRateLimiter; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.kinematics.ChassisSpeeds; +import edu.wpi.first.math.trajectory.TrapezoidProfile; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; +import edu.wpi.team190.gompeilib.core.logging.Trace; +import edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive.SwerveDrive; +import edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive.SwerveDriveConstants; +import edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive.SwerveDriveConstants.AutoAlignConstants; +import frc.robot.FieldConstants; +import frc.robot.subsystems.v0_Funky.V0_FunkyRobotState; +import frc.robot.util.AllianceFlipUtil; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.List; +import java.util.function.BooleanSupplier; +import java.util.function.DoubleSupplier; +import java.util.function.Supplier; +import org.littletonrobotics.junction.Logger; + +public final class DriveCommands { + + /** + * A command that drives a SwerveDrive using joystick input. + * + * @param drive The SwerveDrive to control. + * @param driveConstants The constants for the SwerveDrive. + * @param xSupplier The supplier of the x-axis joystick input. + * @param ySupplier The supplier of the y-axis joystick input. + * @param omegaSupplier The supplier of the omega joystick input. + * @param rotationSupplier The supplier of the rotation of the robot. + * @param hijackXSuppliers A list of pairs of boolean suppliers and double suppliers for hijacking + * the x velocity. If the boolean supplier is true, the x velocity will be set to the value of + * the double supplier. If multiple boolean suppliers are true, the first one in the list will + * take precedence. + * @param hijackYSuppliers A list of pairs of boolean suppliers and double suppliers for hijacking + * the y velocity. If the boolean supplier is true, the y velocity will be set to the value of + * the double supplier. If multiple boolean suppliers are true, the first one in the list will + * take precedence. + * @param hijackOmegaSuppliers A list of pairs of boolean suppliers and double suppliers for + * hijacking the omega velocity. If the boolean supplier is true, the omega velocity will be + * set to the value of the double supplier. If multiple boolean suppliers are true, the first + * one in the list will take precedence. + * @return A command that drives the SwerveDrive. + */ + @Trace + public static Command joystickDrive( + SwerveDrive drive, + SwerveDriveConstants driveConstants, + DoubleSupplier xSupplier, + DoubleSupplier ySupplier, + DoubleSupplier omegaSupplier, + Supplier rotationSupplier, + List> hijackXSuppliers, + List> hijackYSuppliers, + List> hijackOmegaSuppliers, + BooleanSupplier slowMode) { + return Commands.run( + () -> { + // Apply deadband + double linearMagnitude = + MathUtil.applyDeadband( + Math.hypot(xSupplier.getAsDouble(), ySupplier.getAsDouble()), + driveConstants.driverDeadband); + Rotation2d linearDirection = + new Rotation2d(xSupplier.getAsDouble(), ySupplier.getAsDouble()); + + double omega = + MathUtil.applyDeadband(omegaSupplier.getAsDouble(), driveConstants.driverDeadband); + linearMagnitude *= linearMagnitude; + + // Calculate new linear velocities + + double fieldRelativeXVel = + linearMagnitude * linearDirection.getCos() * drive.getMaxLinearSpeedMetersPerSec(); + double fieldRelativeYVel = + linearMagnitude * linearDirection.getSin() * drive.getMaxLinearSpeedMetersPerSec(); + + double angular = omega * drive.getMaxAngularSpeedRadPerSec(); + + fieldRelativeXVel = + hijackXSuppliers.stream() + .filter(pair -> pair.getFirst().getAsBoolean()) + .map(pair -> pair.getSecond().getAsDouble()) + .findFirst() + .orElse(slowMode.getAsBoolean() ? (fieldRelativeXVel * 0.1) : fieldRelativeXVel); + + fieldRelativeYVel = + hijackYSuppliers.stream() + .filter(pair -> pair.getFirst().getAsBoolean()) + .map(pair -> pair.getSecond().getAsDouble()) + .findFirst() + .orElse(slowMode.getAsBoolean() ? (fieldRelativeYVel * 0.1) : fieldRelativeYVel); + + angular = + hijackOmegaSuppliers.stream() + .filter(pair -> pair.getFirst().getAsBoolean()) + .map(pair -> pair.getSecond().getAsDouble()) + .findFirst() + .orElse(slowMode.getAsBoolean() ? (angular * 0.1) : angular); + + ChassisSpeeds chassisSpeeds = + ChassisSpeeds.fromFieldRelativeSpeeds( + fieldRelativeXVel, + fieldRelativeYVel, + angular, + AllianceFlipUtil.apply(rotationSupplier.get())); + + Logger.recordOutput("Drive/JoystickDrive/chassisSpeeds", chassisSpeeds); + + drive.runVelocity(chassisSpeeds); + }, + drive); + } + + public static Command joystickDrive( + SwerveDrive drive, + SwerveDriveConstants driveConstants, + DoubleSupplier xSupplier, + DoubleSupplier ySupplier, + DoubleSupplier omegaSupplier, + Supplier rotationSupplier) { + return joystickDrive( + drive, + driveConstants, + xSupplier, + ySupplier, + omegaSupplier, + rotationSupplier, + List.of(), + List.of(), + List.of(), + () -> false); + } + + public static Command joystickDriveRotationLock( + SwerveDrive drive, + SwerveDriveConstants driveConstants, + DoubleSupplier xSupplier, + DoubleSupplier ySupplier, + DoubleSupplier omegaSupplier, + Supplier rotationSupplier, + BooleanSupplier pointAtHub, + DoubleSupplier hubSetpoint, + BooleanSupplier cardinalDirectionAlign, + DoubleSupplier cardinalDirectionSetpoint, + BooleanSupplier slowMode) { + ProfiledPIDController omegaController = + new ProfiledPIDController( + driveConstants.autoAlignConstants.rotationGains().kP().get(), + 0.0, + driveConstants.autoAlignConstants.rotationGains().kD().get(), + new TrapezoidProfile.Constraints( + driveConstants + .autoAlignConstants + .rotationConstraints() + .maxVelocity() + .get() + .in(RadiansPerSecond), + Double.POSITIVE_INFINITY)); + omegaController.enableContinuousInput(-Math.PI, Math.PI); + omegaController.setTolerance( + driveConstants.autoAlignConstants.rotationConstraints().goalTolerance().get().in(Radians), + 0); + return joystickDrive( + drive, + driveConstants, + xSupplier, + ySupplier, + omegaSupplier, + rotationSupplier, + List.of(), + List.of(), + List.of( + Pair.of( + pointAtHub, + () -> + AutoAlignCommand.calculate( + omegaController, + hubSetpoint.getAsDouble(), + rotationSupplier.get().getRadians(), + drive.getMeasuredChassisSpeeds().omegaRadiansPerSecond)), + Pair.of( + cardinalDirectionAlign, + () -> + AutoAlignCommand.calculate( + omegaController, + cardinalDirectionSetpoint.getAsDouble(), + rotationSupplier.get().getRadians(), + drive.getMeasuredChassisSpeeds().omegaRadiansPerSecond)), + Pair.of( + slowMode, + () -> + AutoAlignCommand.calculate( + omegaController, + AllianceFlipUtil.apply(Rotation2d.kCCW_90deg).getRadians(), + rotationSupplier.get().getRadians(), + drive.getMeasuredChassisSpeeds().omegaRadiansPerSecond))), + slowMode); + } + + public static Command rotateToAngle( + SwerveDrive drive, + SwerveDriveConstants driveConstants, + Supplier currentRotation, + Rotation2d targetRotation) { + ProfiledPIDController omegaController = + new ProfiledPIDController( + driveConstants.autoAlignConstants.rotationGains().kP().get(), + 0.0, + driveConstants.autoAlignConstants.rotationGains().kD().get(), + new TrapezoidProfile.Constraints( + driveConstants + .autoAlignConstants + .rotationConstraints() + .maxVelocity() + .get() + .in(RadiansPerSecond), + Double.POSITIVE_INFINITY)); + omegaController.enableContinuousInput(-Math.PI, Math.PI); + + omegaController.setTolerance( + driveConstants.autoAlignConstants.rotationConstraints().goalTolerance().get().in(Radians), + 0); + return Commands.run( + () -> + drive.runVelocity( + ChassisSpeeds.fromFieldRelativeSpeeds( + 0.0, + 0.0, + AutoAlignCommand.calculate( + omegaController, + targetRotation.getRadians(), + currentRotation.get().getRadians(), + drive.getMeasuredChassisSpeeds().omegaRadiansPerSecond), + AllianceFlipUtil.apply(currentRotation.get())))); + } + + public static Command inchMovement(SwerveDrive drive, double velocity, double time) { + return Commands.run(() -> drive.runVelocity(new ChassisSpeeds(0.0, velocity, 0.0))) + .withTimeout(time); + } + + public static Command stop(SwerveDrive drive) { + return Commands.run(drive::stopWithX); + } + + public static Command feedforwardCharacterization(SwerveDrive drive) { + return new KSCharacterization( + drive, drive::runCharacterization, drive::getFFCharacterizationVelocity); + } + + public static Command autoAlignPoseCommand( + SwerveDrive drive, + Supplier robotPoseSupplier, + Pose2d targetPose, + AutoAlignConstants constants) { + return new AutoAlignCommand( + drive, targetPose, () -> true, robotPoseSupplier, constants, Double.POSITIVE_INFINITY); + } + + public static Command autoAlignTowerCommand( + SwerveDrive drive, Supplier robotPoseSupplier, AutoAlignConstants constants) { + return autoAlignPoseCommand( + drive, + robotPoseSupplier, + new Pose2d(FieldConstants.Tower.centerPoint, new Rotation2d()), + constants); + } + + public static Command aimAtHub(SwerveDrive drive, SwerveDriveConstants driveConstants) { + ProfiledPIDController omegaController = + new ProfiledPIDController( + driveConstants.autoAlignConstants.rotationGains().kP().get(), + 0.0, + driveConstants.autoAlignConstants.rotationGains().kD().get(), + new TrapezoidProfile.Constraints( + driveConstants + .autoAlignConstants + .rotationConstraints() + .maxVelocity() + .get() + .in(RadiansPerSecond), + Double.POSITIVE_INFINITY)); + omegaController.enableContinuousInput(-Math.PI, Math.PI); + omegaController.setTolerance( + driveConstants.autoAlignConstants.rotationConstraints().goalTolerance().get().in(Radians), + 0); + return Commands.run( + () -> { + drive.runVelocity( + ChassisSpeeds.fromFieldRelativeSpeeds( + 0.0, + 0.0, + AutoAlignCommand.calculate( + omegaController, + (AllianceFlipUtil.shouldFlip() + ? FieldConstants.Hub.oppTopCenterPoint.toTranslation2d() + : FieldConstants.Hub.topCenterPoint.toTranslation2d()) + .minus(V0_FunkyRobotState.getGlobalPose().getTranslation()) + .getAngle() + .minus(Rotation2d.kCW_Pi_2) + .getRadians(), + V0_FunkyRobotState.getHeading().getRadians(), + drive.getMeasuredChassisSpeeds().omegaRadiansPerSecond), + V0_FunkyRobotState.getHeading())); + }); + } + + public static Command wheelRadiusCharacterization( + SwerveDrive drive, SwerveDriveConstants driveConstants) { + double WHEEL_RADIUS_MAX_VELOCITY = 0.25; // Rad/Sec + double WHEEL_RADIUS_RAMP_RATE = 0.05; // Rad/Sec^2 + SlewRateLimiter limiter = new SlewRateLimiter(WHEEL_RADIUS_RAMP_RATE); + WheelRadiusCharacterizationState state = new WheelRadiusCharacterizationState(); + + return Commands.parallel( + // SwerveDrive control sequence + Commands.sequence( + // Reset acceleration limiter + Commands.runOnce( + () -> { + limiter.reset(0.0); + }), + + // Turn in place, accelerating up to full speed + Commands.run( + () -> { + double speed = limiter.calculate(WHEEL_RADIUS_MAX_VELOCITY); + drive.runVelocity(new ChassisSpeeds(0.0, 0.0, speed)); + }, + drive)), + + // Measurement sequence + Commands.sequence( + // Wait for modules to fully orient before starting measurement + Commands.waitSeconds(1.0), + + // Record starting measurement + Commands.runOnce( + () -> { + state.positions = drive.getWheelRadiusCharacterizationPositions(); + state.lastAngle = drive.getRawGyroRotation(); + state.gyroDelta = 0.0; + }), + + // Update gyro delta + Commands.run( + () -> { + var rotation = drive.getRawGyroRotation(); + state.gyroDelta += Math.abs(rotation.minus(state.lastAngle).getRadians()); + state.lastAngle = rotation; + }) + + // When cancelled, calculate and print results + .finallyDo( + () -> { + double[] positions = drive.getWheelRadiusCharacterizationPositions(); + double wheelDelta = 0.0; + for (int i = 0; i < 4; i++) { + wheelDelta += Math.abs(positions[i] - state.positions[i]) / 4.0; + } + double wheelRadius = + (state.gyroDelta * driveConstants.driveConfig.driveBaseRadius()) + / wheelDelta; + + NumberFormat formatter = new DecimalFormat("#0.000"); + System.out.println( + "********** Wheel Radius Characterization Results **********"); + System.out.println( + "\tWheel Delta: " + formatter.format(wheelDelta) + " radians"); + System.out.println( + "\tGyro Delta: " + formatter.format(state.gyroDelta) + " radians"); + System.out.println( + "\tWheel Radius: " + + formatter.format(wheelRadius) + + " meters, " + + formatter.format(Units.metersToInches(wheelRadius)) + + " inches"); + }))); + } + + private static class WheelRadiusCharacterizationState { + double[] positions = new double[4]; + Rotation2d lastAngle = new Rotation2d(); + double gyroDelta = 0.0; + } +} diff --git a/lib/examples/swerve/src/main/java/frc/robot/commands/shared/KSCharacterization.java b/lib/examples/swerve/src/main/java/frc/robot/commands/shared/KSCharacterization.java new file mode 100644 index 00000000..a92b593a --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/commands/shared/KSCharacterization.java @@ -0,0 +1,55 @@ +package frc.robot.commands.shared; + +import edu.wpi.first.wpilibj.Timer; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Subsystem; +import edu.wpi.team190.gompeilib.core.utility.tunable.LoggedTunableNumber; +import java.util.function.DoubleConsumer; +import java.util.function.DoubleSupplier; +import org.littletonrobotics.junction.Logger; + +public class KSCharacterization extends Command { + private static final LoggedTunableNumber currentRampFactor = + new LoggedTunableNumber("StaticCharacterization/CurrentRampPerSec", 1.0); + private static final LoggedTunableNumber minVelocity = + new LoggedTunableNumber("StaticCharacterization/MinStaticVelocity", 0.1); + + private final DoubleConsumer inputConsumer; + private final DoubleSupplier velocitySupplier; + private final Timer timer = new Timer(); + private double currentInput = 0.0; + + public KSCharacterization( + Subsystem subsystem, + DoubleConsumer characterizationInputConsumer, + DoubleSupplier velocitySupplier) { + inputConsumer = characterizationInputConsumer; + this.velocitySupplier = velocitySupplier; + addRequirements(subsystem); + } + + @Override + public void initialize() { + timer.restart(); + } + + @Override + public void execute() { + currentInput = timer.get() * currentRampFactor.get(); + inputConsumer.accept(currentInput); + } + + @Override + public boolean isFinished() { + return velocitySupplier.getAsDouble() >= minVelocity.get(); + } + + @Override + public void end(boolean interrupted) { + System.out.println("********** FF Characterization Results **********\n\n\n\n\n\n\n\n\n"); + System.out.println( + "Static Characterization output: " + currentInput + " amps\n\n\n\n\n\n\n\n\n\n"); + Logger.recordOutput("KS output", currentInput); + inputConsumer.accept(0); + } +} diff --git a/lib/examples/swerve/src/main/java/frc/robot/commands/shared/SharedCompositeCommands.java b/lib/examples/swerve/src/main/java/frc/robot/commands/shared/SharedCompositeCommands.java new file mode 100644 index 00000000..d89a8280 --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/commands/shared/SharedCompositeCommands.java @@ -0,0 +1,35 @@ +package frc.robot.commands.shared; + +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; +import edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive.SwerveDrive; +import frc.robot.util.AllianceFlipUtil; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * A class that holds composite commands, which are sequences of commands for complex robot actions. + */ +public class SharedCompositeCommands { + /** + * Creates a command to reset the robot's heading to the alliance-specific zero. + * + * @param drive The drive subsystem. + * @return A command to reset the heading. + */ + public static Command resetHeading( + SwerveDrive drive, + Consumer resetHeadingConsumer, + Supplier currentRobotTranslation) { + return Commands.runOnce( + () -> { + resetHeadingConsumer.accept( + new Pose2d( + currentRobotTranslation.get(), AllianceFlipUtil.apply(new Rotation2d()))); + }) + .ignoringDisable(true); + } +} diff --git a/lib/examples/swerve/src/main/java/frc/robot/commands/v0_Funky/V0_FunkyCompositeCommands.java b/lib/examples/swerve/src/main/java/frc/robot/commands/v0_Funky/V0_FunkyCompositeCommands.java new file mode 100644 index 00000000..2fa8ec61 --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/commands/v0_Funky/V0_FunkyCompositeCommands.java @@ -0,0 +1,3 @@ +package frc.robot.commands.v0_Funky; + +public class V0_FunkyCompositeCommands {} diff --git a/lib/examples/swerve/src/main/java/frc/robot/commands/v0_Funky/autonomous/V0_FunkyAutonomousTest.java b/lib/examples/swerve/src/main/java/frc/robot/commands/v0_Funky/autonomous/V0_FunkyAutonomousTest.java new file mode 100644 index 00000000..e65de1b5 --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/commands/v0_Funky/autonomous/V0_FunkyAutonomousTest.java @@ -0,0 +1,3 @@ +package frc.robot.commands.v0_Funky.autonomous; + +public class V0_FunkyAutonomousTest {} diff --git a/lib/examples/swerve/src/main/java/frc/robot/subsystems/v0_Funky/V0_FunkyConstants.java b/lib/examples/swerve/src/main/java/frc/robot/subsystems/v0_Funky/V0_FunkyConstants.java new file mode 100644 index 00000000..69134d4f --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/subsystems/v0_Funky/V0_FunkyConstants.java @@ -0,0 +1,162 @@ +package frc.robot.subsystems.v0_Funky; + +import static edu.wpi.first.units.Units.*; + +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Transform3d; +import edu.wpi.first.math.system.plant.DCMotor; +import edu.wpi.first.math.util.Units; +import edu.wpi.team190.gompeilib.core.utility.control.Gains; +import edu.wpi.team190.gompeilib.core.utility.control.constraints.AngularPositionConstraints; +import edu.wpi.team190.gompeilib.core.utility.control.constraints.LinearConstraints; +import edu.wpi.team190.gompeilib.core.utility.tunable.LoggedTunableMeasure; +import edu.wpi.team190.gompeilib.core.utility.tunable.LoggedTunableNumber; +import edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive.SwerveDriveConstants; +import edu.wpi.team190.gompeilib.subsystems.vision.VisionConstants.StaticLimelightConfig; +import edu.wpi.team190.gompeilib.subsystems.vision.camera.CameraType; + +public class V0_FunkyConstants { + public static final SwerveDriveConstants.DriveConfig DRIVE_CONFIG = + SwerveDriveConstants.DriveConfig.builder() + .withCanBus(V0_FunkyTunerConstants.kCANBus) + .withPigeon2Id(V0_FunkyTunerConstants.DrivetrainConstants.Pigeon2Id) + .withMaxLinearVelocityMetersPerSecond( + V0_FunkyTunerConstants.kSpeedAt12Volts.in(MetersPerSecond)) + .withWheelRadiusMeters(V0_FunkyTunerConstants.kWheelRadius.in(Meters)) + .withDriveModel(DCMotor.getKrakenX60Foc(1)) + .withTurnModel(DCMotor.getKrakenX44Foc(1)) + .withFrontLeft(V0_FunkyTunerConstants.FrontLeft) + .withFrontRight(V0_FunkyTunerConstants.FrontRight) + .withBackLeft(V0_FunkyTunerConstants.BackLeft) + .withBackRight(V0_FunkyTunerConstants.BackRight) + .withDriveClosedLoopOutputType(V0_FunkyTunerConstants.kDriveClosedLoopOutput) + .withSteerClosedLoopOutputType(V0_FunkyTunerConstants.kSteerClosedLoopOutput) + .withBumperLength(Units.inchesToMeters(34.5)) + .withBumperWidth(Units.inchesToMeters(34.5)) + .build(); + + public static final Gains DRIVE_GAINS = + Gains.builder() + .withKP( + new LoggedTunableNumber( + "Drive/Teleoperated/Drive Kp", V0_FunkyTunerConstants.driveGains.kP)) + .withKD( + new LoggedTunableNumber( + "Drive/Teleoperated/Drive Kd", V0_FunkyTunerConstants.driveGains.kD)) + .withKS( + new LoggedTunableNumber( + "Drive/Teleoperated/Drive Ks", V0_FunkyTunerConstants.driveGains.kS)) + .withKV( + new LoggedTunableNumber( + "Drive/Teleoperated/Drive Kv", V0_FunkyTunerConstants.driveGains.kV)) + .build(); + + public static final Gains TRANSLATION_AUTO_GAINS = + Gains.builder() + .withKP(new LoggedTunableNumber("Drive/Auto/Translation Kp", 0.0)) + .withKD(new LoggedTunableNumber("Drive/Auto/Translation Kd", 0.0)) + .build(); + + public static final Gains ROTATION_AUTO_GAINS = + Gains.builder() + .withKP(new LoggedTunableNumber("Drive/Auto/Rotation Kp", 0.0)) + .withKD(new LoggedTunableNumber("Drive/Auto/Rotation Kd", 0.0)) + .build(); + + public static final Gains AUTO_ALIGN_X_GAINS = + Gains.builder() + .withKP(new LoggedTunableNumber("Drive/Auto Align/X/Kp", 0.0)) + .withKD(new LoggedTunableNumber("Drive/Auto Align/X/Kd", 0.0)) + .build(); + + public static final LinearConstraints AUTO_ALIGN_X_CONSTRAINTS = + LinearConstraints.builder() + .withMaxVelocity( + new LoggedTunableMeasure<>( + "Drive/Auto Align/X/Max Velocity", MetersPerSecond.of(0.0))) + .withMaxAcceleration( + new LoggedTunableMeasure<>( + "Drive/Auto Align/X/Max Acceleration", MetersPerSecondPerSecond.of(0.0))) + .withGoalTolerance( + new LoggedTunableMeasure<>("Drive/Auto Align/X/Max Velocity", Meters.of(0.0))) + .build(); + + public static final Gains AUTO_ALIGN_Y_GAINS = + Gains.builder() + .withKP(new LoggedTunableNumber("Drive/Auto Align/Y/Kp", 0.0)) + .withKD(new LoggedTunableNumber("Drive/Auto Align/Y/Kd", 0.0)) + .build(); + + public static final LinearConstraints AUTO_ALIGN_Y_CONSTRAINTS = + LinearConstraints.builder() + .withMaxVelocity( + new LoggedTunableMeasure<>( + "Drive/Auto Align/Y/Max Velocity", MetersPerSecond.of(0.0))) + .withMaxAcceleration( + new LoggedTunableMeasure<>( + "Drive/Auto Align/Y/Max Acceleration", MetersPerSecondPerSecond.of(0.0))) + .withGoalTolerance( + new LoggedTunableMeasure<>("Drive/Auto Align/Y/Max Velocity", Meters.of(0.0))) + .build(); + + public static final Gains AUTO_ALIGN_THETA_GAINS = + Gains.builder() + .withKP(new LoggedTunableNumber("Drive/Auto Align/Theta/Kp", 0.0)) + .withKD(new LoggedTunableNumber("Drive/Auto Align/Theta/Kd", 0.0)) + .build(); + + public static final AngularPositionConstraints AUTO_ALIGN_THETA_CONSTRAINTS = + AngularPositionConstraints.builder() + .withMaxVelocity( + new LoggedTunableMeasure<>( + "Drive/Auto Align/Theta/Max Velocity", RadiansPerSecond.of(0.0))) + .withMaxAcceleration( + new LoggedTunableMeasure<>( + "Drive/Auto Align/Theta/Max Acceleration", RadiansPerSecondPerSecond.of(0.0))) + .withGoalTolerance( + new LoggedTunableMeasure<>("Drive/Auto Align/Theta/Max Velocity", Radians.of(0.0))) + .build(); + + public static final SwerveDriveConstants.AutoAlignConstants AUTO_ALIGN_NEAR_CONSTANTS = + SwerveDriveConstants.AutoAlignConstants.builder() + .withXGains(AUTO_ALIGN_X_GAINS) + .withXConstraints(AUTO_ALIGN_X_CONSTRAINTS) + .withYGains(AUTO_ALIGN_Y_GAINS) + .withYConstraints(AUTO_ALIGN_Y_CONSTRAINTS) + .withRotationGains(AUTO_ALIGN_THETA_GAINS) + .withRotationConstraints(AUTO_ALIGN_THETA_CONSTRAINTS) + .withLinearThreshold( + new LoggedTunableMeasure<>( + "Drive/Auto Align/Position Threshold Meters", Inches.of(0.25))) + .withAngularThreshold( + new LoggedTunableMeasure<>( + "Drive/Auto Align/Angular Threshold Meters", Radians.of(0.25))) + .build(); + + public static final double ODOMETRY_FREQUENCY = 250.0; + public static final double DRIVER_DEADBAND = 0.1; + public static final double OPERATOR_DEADBAND = 0.1; + + public static final SwerveDriveConstants DRIVE_CONSTANTS = + SwerveDriveConstants.builder() + .withDriveConfig(DRIVE_CONFIG) + .withAutoTranslationGains(TRANSLATION_AUTO_GAINS) + .withAutoRotationGains(ROTATION_AUTO_GAINS) + .withAutoAlignConstants(AUTO_ALIGN_NEAR_CONSTANTS) + .withOdometryFrequency(ODOMETRY_FREQUENCY) + .withDriverDeadband(DRIVER_DEADBAND) + .withOperatorDeadband(OPERATOR_DEADBAND) + .build(); + + public static final StaticLimelightConfig LIMELIGHT_CONFIG = + StaticLimelightConfig.builder() + .key("limelight") + .cameraType(CameraType.LIMELIGHT_4) + .horizontalFOV(82.0) + .verticalFOV(56.2) + .megatagXYStdev(0.1) + .metatagThetaStdev(0.0015) + .megatag2XYStdev(0.001) + .robotToCameraTransform(new Transform3d(0.0, 0.0, 0.0, new Rotation3d(0.0, 0.0, 0.0))) + .build(); +} diff --git a/lib/examples/swerve/src/main/java/frc/robot/subsystems/v0_Funky/V0_FunkyRobotContainer.java b/lib/examples/swerve/src/main/java/frc/robot/subsystems/v0_Funky/V0_FunkyRobotContainer.java new file mode 100644 index 00000000..41b53588 --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/subsystems/v0_Funky/V0_FunkyRobotContainer.java @@ -0,0 +1,167 @@ +package frc.robot.subsystems.v0_Funky; + +import choreo.auto.AutoChooser; +import edu.wpi.first.apriltag.AprilTagFieldLayout; +import edu.wpi.first.apriltag.AprilTagFields; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.button.CommandXboxController; +import edu.wpi.team190.gompeilib.core.io.components.inertial.GyroIO; +import edu.wpi.team190.gompeilib.core.io.components.inertial.GyroIOPigeon2; +import edu.wpi.team190.gompeilib.core.robot.RobotContainer; +import edu.wpi.team190.gompeilib.core.robot.RobotMode; +import edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive.SwerveDrive; +import edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive.SwerveModuleIO; +import edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive.SwerveModuleIOSim; +import edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive.SwerveModuleIOTalonFX; +import edu.wpi.team190.gompeilib.subsystems.vision.Vision; +import edu.wpi.team190.gompeilib.subsystems.vision.camera.CameraStaticLimelight; +import edu.wpi.team190.gompeilib.subsystems.vision.io.CameraIOLimelight; +import frc.robot.Constants; +import frc.robot.RobotConfig; +import frc.robot.commands.shared.DriveCommands; +import frc.robot.commands.shared.SharedCompositeCommands; +import frc.robot.util.input.XKeysInput; +import java.util.List; + +public class V0_FunkyRobotContainer implements RobotContainer { + private SwerveDrive drive; + private Vision vision; + + private final CommandXboxController driver = new CommandXboxController(0); + + private final AutoChooser autoChooser = new AutoChooser(); + + private final XKeysInput xkeys = new XKeysInput(1); + + public V0_FunkyRobotContainer() { + if (Constants.getMode() != RobotMode.REPLAY) { + switch (RobotConfig.ROBOT) { + case V0_FUNKY: + drive = + new SwerveDrive( + V0_FunkyConstants.DRIVE_CONSTANTS, + new GyroIOPigeon2( + V0_FunkyConstants.DRIVE_CONSTANTS, V0_FunkyRobotState::setGyroTimestamp), + new SwerveModuleIOTalonFX( + V0_FunkyConstants.DRIVE_CONSTANTS, + V0_FunkyConstants.DRIVE_CONSTANTS.driveConfig.frontLeft()), + new SwerveModuleIOTalonFX( + V0_FunkyConstants.DRIVE_CONSTANTS, + V0_FunkyConstants.DRIVE_CONSTANTS.driveConfig.frontRight()), + new SwerveModuleIOTalonFX( + V0_FunkyConstants.DRIVE_CONSTANTS, + V0_FunkyConstants.DRIVE_CONSTANTS.driveConfig.backLeft()), + new SwerveModuleIOTalonFX( + V0_FunkyConstants.DRIVE_CONSTANTS, + V0_FunkyConstants.DRIVE_CONSTANTS.driveConfig.backRight()), + V0_FunkyRobotState::getGlobalPose, + V0_FunkyRobotState::resetPose); + vision = + new Vision( + () -> AprilTagFieldLayout.loadField(AprilTagFields.k2025ReefscapeAndyMark), + new CameraStaticLimelight( + new CameraIOLimelight(V0_FunkyConstants.LIMELIGHT_CONFIG), + V0_FunkyConstants.LIMELIGHT_CONFIG, + V0_FunkyRobotState::getHeading, + drive::getMeasuredChassisSpeeds, + V0_FunkyRobotState::getGyroTimestamp, + List.of(), + List.of())); + break; + + case V0_FUNKY_SIM: + drive = + new SwerveDrive( + V0_FunkyConstants.DRIVE_CONSTANTS, + new GyroIO() {}, + new SwerveModuleIOSim( + V0_FunkyConstants.DRIVE_CONSTANTS, + V0_FunkyConstants.DRIVE_CONSTANTS.driveConfig.frontLeft()), + new SwerveModuleIOSim( + V0_FunkyConstants.DRIVE_CONSTANTS, + V0_FunkyConstants.DRIVE_CONSTANTS.driveConfig.frontRight()), + new SwerveModuleIOSim( + V0_FunkyConstants.DRIVE_CONSTANTS, + V0_FunkyConstants.DRIVE_CONSTANTS.driveConfig.backLeft()), + new SwerveModuleIOSim( + V0_FunkyConstants.DRIVE_CONSTANTS, + V0_FunkyConstants.DRIVE_CONSTANTS.driveConfig.backRight()), + () -> Pose2d.kZero, + V0_FunkyRobotState::resetPose); + vision = + new Vision( + () -> AprilTagFieldLayout.loadField(AprilTagFields.k2025ReefscapeAndyMark)); + + break; + + default: + break; + } + } + if (drive == null) { + drive = + new SwerveDrive( + V0_FunkyConstants.DRIVE_CONSTANTS, + new GyroIOPigeon2( + V0_FunkyConstants.DRIVE_CONSTANTS, V0_FunkyRobotState::setGyroTimestamp), + new SwerveModuleIO() {}, + new SwerveModuleIO() {}, + new SwerveModuleIO() {}, + new SwerveModuleIO() {}, + V0_FunkyRobotState::getGlobalPose, + V0_FunkyRobotState::resetPose); + } + + if (vision == null) { + new Vision(() -> AprilTagFieldLayout.loadField(AprilTagFields.k2025ReefscapeAndyMark)); + } + + configureButtonBindings(); + configureAutos(); + } + + private void configureButtonBindings() { + drive.setDefaultCommand( + DriveCommands.joystickDrive( + drive, + V0_FunkyConstants.DRIVE_CONSTANTS, + driver::getLeftY, + driver::getLeftX, + driver::getRightX, + V0_FunkyRobotState::getHeading)); + + driver + .povDown() + .onTrue( + SharedCompositeCommands.resetHeading( + drive, + V0_FunkyRobotState::resetPose, + () -> V0_FunkyRobotState.getGlobalPose().getTranslation())); + + xkeys + .a1() + .or(xkeys.a2()) + .or(xkeys.a3()) + .or(xkeys.a4()) + .onTrue( + SharedCompositeCommands.resetHeading( + drive, + V0_FunkyRobotState::resetPose, + () -> V0_FunkyRobotState.getGlobalPose().getTranslation())); + } + + private void configureAutos() { + // Autos here + } + + @Override + public void robotPeriodic() { + V0_FunkyRobotState.periodic(drive.getRawGyroRotation(), drive.getModulePositions()); + } + + @Override + public Command getAutonomousCommand() { + return autoChooser.selectedCommand(); + } +} diff --git a/lib/examples/swerve/src/main/java/frc/robot/subsystems/v0_Funky/V0_FunkyRobotState.java b/lib/examples/swerve/src/main/java/frc/robot/subsystems/v0_Funky/V0_FunkyRobotState.java new file mode 100644 index 00000000..c49be094 --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/subsystems/v0_Funky/V0_FunkyRobotState.java @@ -0,0 +1,67 @@ +package frc.robot.subsystems.v0_Funky; + +import edu.wpi.first.apriltag.AprilTagFieldLayout; +import edu.wpi.first.apriltag.AprilTagFields; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.kinematics.SwerveModulePosition; +import edu.wpi.first.networktables.NetworkTablesJNI; +import edu.wpi.first.wpilibj.Timer; +import edu.wpi.team190.gompeilib.core.state.localization.FieldZone; +import edu.wpi.team190.gompeilib.core.state.localization.Localization; +import frc.robot.util.NTPrefixes; +import java.util.HashSet; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import org.littletonrobotics.junction.AutoLogOutput; +import org.littletonrobotics.junction.Logger; + +public class V0_FunkyRobotState { + private static final AprilTagFieldLayout fieldLayout; + private static final Localization localization; + + @AutoLogOutput(key = NTPrefixes.ROBOT_STATE + "Hood/Score Angle") + @Getter + private static final Rotation2d scoreAngle; + + @AutoLogOutput(key = NTPrefixes.ROBOT_STATE + "Hood/Feed Angle") + @Getter + private static final Rotation2d feedAngle; + + private static final FieldZone globalZone; + + @Getter @Setter private static long gyroTimestamp; + + static { + fieldLayout = AprilTagFieldLayout.loadField(AprilTagFields.k2026RebuiltAndymark); + + globalZone = new FieldZone(new HashSet<>(fieldLayout.getTags())); + + localization = + new Localization( + List.of(globalZone), V0_FunkyConstants.DRIVE_CONSTANTS.driveConfig.kinematics(), 2); + scoreAngle = Rotation2d.kZero; + feedAngle = Rotation2d.kZero; + + gyroTimestamp = NetworkTablesJNI.now(); + } + + public static void periodic(Rotation2d heading, SwerveModulePosition[] modulePositions) { + + localization.addOdometryObservation(Timer.getTimestamp(), heading, modulePositions); + Logger.recordOutput(NTPrefixes.ROBOT_STATE + "/Global Pose", getGlobalPose()); + } + + public static void resetPose(Pose2d pose) { + localization.resetPose(pose); + } + + public static Rotation2d getHeading() { + return localization.getHeading(); + } + + public static Pose2d getGlobalPose() { + return localization.getEstimatedPose(globalZone); + } +} diff --git a/lib/examples/swerve/src/main/java/frc/robot/subsystems/v0_Funky/V0_FunkyTunerConstants.java b/lib/examples/swerve/src/main/java/frc/robot/subsystems/v0_Funky/V0_FunkyTunerConstants.java new file mode 100644 index 00000000..cebaaaf4 --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/subsystems/v0_Funky/V0_FunkyTunerConstants.java @@ -0,0 +1,304 @@ +package frc.robot.subsystems.v0_Funky; + +import static edu.wpi.first.units.Units.*; + +import com.ctre.phoenix6.CANBus; +import com.ctre.phoenix6.configs.*; +import com.ctre.phoenix6.hardware.*; +import com.ctre.phoenix6.signals.*; +import com.ctre.phoenix6.swerve.*; +import com.ctre.phoenix6.swerve.SwerveModuleConstants.*; +import edu.wpi.first.math.Matrix; +import edu.wpi.first.math.numbers.N1; +import edu.wpi.first.math.numbers.N3; +import edu.wpi.first.units.measure.*; + +// Generated by the 2026 Tuner X Swerve Project Generator +// https://v6.docs.ctr-electronics.com/en/stable/docs/tuner/tuner-swerve/index.html +public class V0_FunkyTunerConstants { + // Both sets of gains need to be tuned to your individual robot. + + // The steer motor uses any SwerveModule.SteerRequestType control request with the + // output type specified by SwerveModuleConstants.SteerMotorClosedLoopOutput + public static final Slot0Configs steerGains = + new Slot0Configs() + .withKP(100) + .withKI(0) + .withKD(0.5) + .withKS(0.1) + .withKV(2.66) + .withKA(0) + .withStaticFeedforwardSign(StaticFeedforwardSignValue.UseClosedLoopSign); + // When using closed-loop control, the drive motor uses the control + // output type specified by SwerveModuleConstants.DriveMotorClosedLoopOutput + public static final Slot0Configs driveGains = + new Slot0Configs().withKP(60.0).withKI(0).withKD(0.0).withKS(2.361118000000002).withKV(0.0); + + // The closed-loop output type to use for the steer motors; + // This affects the PID/FF gains for the steer motors + public static final ClosedLoopOutputType kSteerClosedLoopOutput = ClosedLoopOutputType.Voltage; + // The closed-loop output type to use for the drive motors; + // This affects the PID/FF gains for the drive motors + public static final ClosedLoopOutputType kDriveClosedLoopOutput = + ClosedLoopOutputType.TorqueCurrentFOC; + + // The type of motor used for the drive motor + private static final DriveMotorArrangement kDriveMotorType = + DriveMotorArrangement.TalonFX_Integrated; + // The type of motor used for the drive motor + private static final SteerMotorArrangement kSteerMotorType = + SteerMotorArrangement.TalonFX_Integrated; + + // The remote sensor feedback type to use for the steer motors; + // When not Pro-licensed, Fused*/Sync* automatically fall back to Remote* + private static final SteerFeedbackType kSteerFeedbackType = SteerFeedbackType.FusedCANcoder; + + // The stator current at which the wheels start to slip; + // This needs to be tuned to your individual robot + private static final Current kSlipCurrent = Amps.of(120); + + // Initial configs for the drive and steer motors and the azimuth encoder; these cannot be null. + // Some configs will be overwritten; check the `with*InitialConfigs()` API documentation. + private static final TalonFXConfiguration driveInitialConfigs = new TalonFXConfiguration(); + private static final TalonFXConfiguration steerInitialConfigs = + new TalonFXConfiguration() + .withCurrentLimits( + new CurrentLimitsConfigs() + // Swerve azimuth does not require much torque output, so we can set a relatively + // low + // stator current limit to help avoid brownouts without impacting performance. + .withStatorCurrentLimit(Amps.of(60)) + .withStatorCurrentLimitEnable(true)); + private static final CANcoderConfiguration encoderInitialConfigs = new CANcoderConfiguration(); + // Configs for the Pigeon 2; leave this null to skip applying Pigeon 2 configs + private static final Pigeon2Configuration pigeonConfigs = null; + + // CAN bus that the devices are located on; + // All swerve devices must share the same CAN bus + public static final CANBus kCANBus = new CANBus("Drive", "./logs/example.hoot"); + + // Theoretical free speed (m/s) at 12 V applied output; + // This needs to be tuned to your individual robot + public static final LinearVelocity kSpeedAt12Volts = MetersPerSecond.of(5.04); + + // Every 1 rotation of the azimuth results in kCoupleRatio drive motor turns; + // This may need to be tuned to your individual robot + private static final double kCoupleRatio = 3.5714285714285716; + + private static final double kDriveGearRatio = 6.122448979591837; + private static final double kSteerGearRatio = 21.428571428571427; + public static final Distance kWheelRadius = Inches.of(2); + + private static final boolean kInvertRightSide = false; + + private static final int kPigeonId = 50; + + // These are only used for simulation + private static final MomentOfInertia kSteerInertia = KilogramSquareMeters.of(0.01); + private static final MomentOfInertia kDriveInertia = KilogramSquareMeters.of(0.01); + // Simulated voltage necessary to overcome friction + private static final Voltage kSteerFrictionVoltage = Volts.of(0.2); + private static final Voltage kDriveFrictionVoltage = Volts.of(0.2); + + public static final SwerveDrivetrainConstants DrivetrainConstants = + new SwerveDrivetrainConstants() + .withCANBusName(kCANBus.getName()) + .withPigeon2Id(kPigeonId) + .withPigeon2Configs(pigeonConfigs); + + private static final SwerveModuleConstantsFactory< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + ConstantCreator = + new SwerveModuleConstantsFactory< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration>() + .withDriveMotorGearRatio(kDriveGearRatio) + .withSteerMotorGearRatio(kSteerGearRatio) + .withCouplingGearRatio(kCoupleRatio) + .withWheelRadius(kWheelRadius) + .withSteerMotorGains(steerGains) + .withDriveMotorGains(driveGains) + .withSteerMotorClosedLoopOutput(kSteerClosedLoopOutput) + .withDriveMotorClosedLoopOutput(kDriveClosedLoopOutput) + .withSlipCurrent(kSlipCurrent) + .withSpeedAt12Volts(kSpeedAt12Volts) + .withDriveMotorType(kDriveMotorType) + .withSteerMotorType(kSteerMotorType) + .withFeedbackSource(kSteerFeedbackType) + .withDriveMotorInitialConfigs(driveInitialConfigs) + .withSteerMotorInitialConfigs(steerInitialConfigs) + .withEncoderInitialConfigs(encoderInitialConfigs) + .withSteerInertia(kSteerInertia) + .withDriveInertia(kDriveInertia) + .withSteerFrictionVoltage(kSteerFrictionVoltage) + .withDriveFrictionVoltage(kDriveFrictionVoltage); + + // Front Left + private static final int kFrontLeftDriveMotorId = 1; + private static final int kFrontLeftSteerMotorId = 5; + private static final int kFrontLeftEncoderId = 10; + private static final Angle kFrontLeftEncoderOffset = Rotations.of(-0.463134765625 + 0.25); + private static final boolean kFrontLeftSteerMotorInverted = true; + private static final boolean kFrontLeftEncoderInverted = false; + + private static final Distance kFrontLeftXPos = Inches.of(11.5); + private static final Distance kFrontLeftYPos = Inches.of(11.5); + + // Front Right + private static final int kFrontRightDriveMotorId = 2; + private static final int kFrontRightSteerMotorId = 6; + private static final int kFrontRightEncoderId = 11; + private static final Angle kFrontRightEncoderOffset = Rotations.of(0.1376953125 + 0.25); + private static final boolean kFrontRightSteerMotorInverted = true; + private static final boolean kFrontRightEncoderInverted = false; + + private static final Distance kFrontRightXPos = Inches.of(11.5); + private static final Distance kFrontRightYPos = Inches.of(-11.5); + + // Back Left + private static final int kBackLeftDriveMotorId = 3; + private static final int kBackLeftSteerMotorId = 7; + private static final int kBackLeftEncoderId = 12; + private static final Angle kBackLeftEncoderOffset = Rotations.of(-0.018798828125 + 0.25); + private static final boolean kBackLeftSteerMotorInverted = true; + private static final boolean kBackLeftEncoderInverted = false; + + private static final Distance kBackLeftXPos = Inches.of(-11.5); + private static final Distance kBackLeftYPos = Inches.of(11.5); + + // Back Right + private static final int kBackRightDriveMotorId = 4; + private static final int kBackRightSteerMotorId = 8; + private static final int kBackRightEncoderId = 13; + private static final Angle kBackRightEncoderOffset = Rotations.of(0.477294921875 + 0.25); + private static final boolean kBackRightSteerMotorInverted = true; + private static final boolean kBackRightEncoderInverted = false; + + private static final Distance kBackRightXPos = Inches.of(-11.5); + private static final Distance kBackRightYPos = Inches.of(-11.5); + + public static final SwerveModuleConstants< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + FrontLeft = + ConstantCreator.createModuleConstants( + kFrontLeftSteerMotorId, + kFrontLeftDriveMotorId, + kFrontLeftEncoderId, + kFrontLeftEncoderOffset, + kFrontLeftXPos, + kFrontLeftYPos, + false, + kFrontLeftSteerMotorInverted, + kFrontLeftEncoderInverted); + public static final SwerveModuleConstants< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + FrontRight = + ConstantCreator.createModuleConstants( + kFrontRightSteerMotorId, + kFrontRightDriveMotorId, + kFrontRightEncoderId, + kFrontRightEncoderOffset, + kFrontRightXPos, + kFrontRightYPos, + kInvertRightSide, + kFrontRightSteerMotorInverted, + kFrontRightEncoderInverted); + public static final SwerveModuleConstants< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + BackLeft = + ConstantCreator.createModuleConstants( + kBackLeftSteerMotorId, + kBackLeftDriveMotorId, + kBackLeftEncoderId, + kBackLeftEncoderOffset, + kBackLeftXPos, + kBackLeftYPos, + true, + kBackLeftSteerMotorInverted, + kBackLeftEncoderInverted); + public static final SwerveModuleConstants< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + BackRight = + ConstantCreator.createModuleConstants( + kBackRightSteerMotorId, + kBackRightDriveMotorId, + kBackRightEncoderId, + kBackRightEncoderOffset, + kBackRightXPos, + kBackRightYPos, + true, + kBackRightSteerMotorInverted, + kBackRightEncoderInverted); + + /** Swerve Drive class utilizing CTR Electronics' Phoenix 6 API with the selected device types. */ + public static class TunerSwerveDrivetrain extends SwerveDrivetrain { + /** + * Constructs a CTRE SwerveDrivetrain using the specified constants. + * + *

This constructs the underlying hardware devices, so users should not construct the devices + * themselves. If they need the devices, they can access them through getters in the classes. + * + * @param drivetrainConstants Drivetrain-wide constants for the swerve drive + * @param modules Constants for each specific module + */ + public TunerSwerveDrivetrain( + SwerveDrivetrainConstants drivetrainConstants, SwerveModuleConstants... modules) { + super(TalonFX::new, TalonFX::new, CANcoder::new, drivetrainConstants, modules); + } + + /** + * Constructs a CTRE SwerveDrivetrain using the specified constants. + * + *

This constructs the underlying hardware devices, so users should not construct the devices + * themselves. If they need the devices, they can access them through getters in the classes. + * + * @param drivetrainConstants Drivetrain-wide constants for the swerve drive + * @param odometryUpdateFrequency The frequency to run the odometry loop. If unspecified or set + * to 0 Hz, this is 250 Hz on CAN FD, and 100 Hz on CAN 2.0. + * @param modules Constants for each specific module + */ + public TunerSwerveDrivetrain( + SwerveDrivetrainConstants drivetrainConstants, + double odometryUpdateFrequency, + SwerveModuleConstants... modules) { + super( + TalonFX::new, + TalonFX::new, + CANcoder::new, + drivetrainConstants, + odometryUpdateFrequency, + modules); + } + + /** + * Constructs a CTRE SwerveDrivetrain using the specified constants. + * + *

This constructs the underlying hardware devices, so users should not construct the devices + * themselves. If they need the devices, they can access them through getters in the classes. + * + * @param drivetrainConstants Drivetrain-wide constants for the swerve drive + * @param odometryUpdateFrequency The frequency to run the odometry loop. If unspecified or set + * to 0 Hz, this is 250 Hz on CAN FD, and 100 Hz on CAN 2.0. + * @param odometryStandardDeviation The standard deviation for odometry calculation in the form + * [x, y, theta], with units in meters and radians + * @param visionStandardDeviation The standard deviation for vision calculation in the form [x, + * y, theta], with units in meters and radians + * @param modules Constants for each specific module + */ + public TunerSwerveDrivetrain( + SwerveDrivetrainConstants drivetrainConstants, + double odometryUpdateFrequency, + Matrix odometryStandardDeviation, + Matrix visionStandardDeviation, + SwerveModuleConstants... modules) { + super( + TalonFX::new, + TalonFX::new, + CANcoder::new, + drivetrainConstants, + odometryUpdateFrequency, + odometryStandardDeviation, + visionStandardDeviation, + modules); + } + } +} diff --git a/lib/examples/swerve/src/main/java/frc/robot/util/Alert.java b/lib/examples/swerve/src/main/java/frc/robot/util/Alert.java new file mode 100644 index 00000000..e53f04eb --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/util/Alert.java @@ -0,0 +1,148 @@ +// Copyright (c) 2023 FRC 6328 +// http://github.com/Mechanical-Advantage +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file at +// the root directory of this project. + +package frc.robot.util; + +import edu.wpi.first.util.sendable.Sendable; +import edu.wpi.first.util.sendable.SendableBuilder; +import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.first.wpilibj.Timer; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; +import lombok.Getter; + +/** Class for managing persistent alerts to be sent over NetworkTables. */ +public class Alert { + private static final Map groups = new HashMap<>(); + + private final AlertType type; + @Getter private boolean active = false; + private double activeStartTime = 0.0; + private String text; + + /** + * Creates a new Alert in the default group - "Alerts". If this is the first to be instantiated, + * the appropriate entries will be added to NetworkTables. + * + * @param text Text to be displayed when the alert is active. + * @param type Alert level specifying urgency. + */ + public Alert(String text, AlertType type) { + this("Alerts", text, type); + } + + /** + * Creates a new Alert. If this is the first to be instantiated in its group, the appropriate + * entries will be added to NetworkTables. + * + * @param group Group identifier, also used as NetworkTables title + * @param text Text to be displayed when the alert is active. + * @param type Alert level specifying urgency. + */ + public Alert(String group, String text, AlertType type) { + if (!groups.containsKey(group)) { + groups.put(group, new SendableAlerts()); + SmartDashboard.putData(group, groups.get(group)); + } + + this.text = text; + this.type = type; + groups.get(group).alerts.add(this); + } + + /** + * Sets whether the alert should currently be displayed. When activated, the alert text will also + * be sent to the console. + */ + public void set(boolean active) { + if (active && !this.active) { + activeStartTime = Timer.getTimestamp(); + switch (type) { + case ERROR: + DriverStation.reportError(text, false); + break; + case WARNING: + DriverStation.reportWarning(text, false); + break; + case INFO: + System.out.println(text); + break; + } + } + this.active = active; + } + + /** Updates current alert text. */ + public void setText(String text) { + if (active && !text.equals(this.text)) { + switch (type) { + case ERROR: + DriverStation.reportError(text, false); + break; + case WARNING: + DriverStation.reportWarning(text, false); + break; + case INFO: + System.out.println(text); + break; + } + } + this.text = text; + } + + /** Represents an alert's level of urgency. */ + public enum AlertType { + /** + * High priority alert - displayed first on the dashboard with a red "X" symbol. Use this type + * for problems which will seriously affect the robot's functionality and thus require immediate + * attention. + */ + ERROR, + + /** + * Medium priority alert - displayed second on the dashboard with a yellow "!" symbol. Use this + * type for problems which could affect the robot's functionality but do not necessarily require + * immediate attention. + */ + WARNING, + + /** + * Low priority alert - displayed last on the dashboard with a green "i" symbol. Use this type + * for problems which are unlikely to affect the robot's functionality, or any other alerts + * which do not fall under "ERROR" or "WARNING". + */ + INFO + } + + private static class SendableAlerts implements Sendable { + public final List alerts = new ArrayList<>(); + + public String[] getStrings(AlertType type) { + Predicate activeFilter = (Alert x) -> x.type == type && x.active; + Comparator timeSorter = + (Alert a1, Alert a2) -> (int) (a2.activeStartTime - a1.activeStartTime); + return alerts.stream() + .filter(activeFilter) + .sorted(timeSorter) + .map((Alert a) -> a.text) + .toArray(String[]::new); + } + + @Override + public void initSendable(SendableBuilder builder) { + builder.setSmartDashboardType("Alerts"); + builder.addStringArrayProperty("errors", () -> getStrings(AlertType.ERROR), null); + builder.addStringArrayProperty("warnings", () -> getStrings(AlertType.WARNING), null); + builder.addStringArrayProperty("infos", () -> getStrings(AlertType.INFO), null); + } + } +} diff --git a/lib/examples/swerve/src/main/java/frc/robot/util/AllianceFlipUtil.java b/lib/examples/swerve/src/main/java/frc/robot/util/AllianceFlipUtil.java new file mode 100644 index 00000000..ca3ed684 --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/util/AllianceFlipUtil.java @@ -0,0 +1,58 @@ +package frc.robot.util; + +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.wpilibj.DriverStation; + +public class AllianceFlipUtil { + public static double fieldWidth = Units.feetToMeters(26.0) + Units.inchesToMeters(5.0); + public static double fieldLength = Units.feetToMeters(57.0) + Units.inchesToMeters(6.875); + + public static double applyX(double x) { + return shouldFlip() ? fieldLength - x : x; + } + + public static double applyY(double y) { + return shouldFlip() ? fieldWidth - y : y; + } + + public static Translation2d apply(Translation2d translation) { + return new Translation2d(applyX(translation.getX()), applyY(translation.getY())); + } + + public static Rotation2d apply(Rotation2d rotation) { + return shouldFlip() ? rotation.rotateBy(Rotation2d.kPi) : rotation; + } + + public static Pose2d apply(Pose2d pose) { + return new Pose2d(apply(pose.getTranslation()), apply(pose.getRotation())); + } + + public static double overrideApplyX(double x) { + return fieldLength - x; + } + + public static double overrideApplyY(double y) { + return fieldWidth - y; + } + + public static Translation2d overrideApply(Translation2d translation) { + return new Translation2d( + overrideApplyX(translation.getX()), overrideApplyY(translation.getY())); + } + + public static Rotation2d overrideApply(Rotation2d rotation) { + return rotation.rotateBy(Rotation2d.kPi); + } + + public static Pose2d overrideApply(Pose2d pose) { + return new Pose2d(overrideApply(pose.getTranslation()), overrideApply(pose.getRotation())); + } + + public static boolean shouldFlip() { + return DriverStation.getAlliance().isPresent() + && DriverStation.getAlliance().get() == DriverStation.Alliance.Red; + } +} diff --git a/lib/examples/swerve/src/main/java/frc/robot/util/Elastic.java b/lib/examples/swerve/src/main/java/frc/robot/util/Elastic.java new file mode 100644 index 00000000..d9a1ed7d --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/util/Elastic.java @@ -0,0 +1,321 @@ +// Copyright (c) 2023-2025 Gold87 and other Elastic contributors +// This software can be modified and/or shared under the terms +// defined by the Elastic license: +// https://github.com/Gold872/elastic_dashboard/blob/main/LICENSE + +package frc.robot.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.networktables.PubSubOption; +import edu.wpi.first.networktables.StringPublisher; +import edu.wpi.first.networktables.StringTopic; +import lombok.Getter; +import lombok.Setter; + +public final class Elastic { + private static final StringTopic notificationTopic = + NetworkTableInstance.getDefault().getStringTopic("/Elastic/RobotNotifications"); + private static final StringPublisher notificationPublisher = + notificationTopic.publish(PubSubOption.sendAll(true), PubSubOption.keepDuplicates(true)); + private static final StringTopic selectedTabTopic = + NetworkTableInstance.getDefault().getStringTopic("/Elastic/SelectedTab"); + private static final StringPublisher selectedTabPublisher = + selectedTabTopic.publish(PubSubOption.keepDuplicates(true)); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Sends an notification to the Elastic dashboard. The notification is serialized as a JSON string + * before being published. + * + * @param notification the {@link Notification} object containing notification details + */ + public static void sendNotification(Notification notification) { + try { + notificationPublisher.set(objectMapper.writeValueAsString(notification)); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + } + + /** + * Selects the tab of the dashboard with the given name. If no tab matches the name, this will + * have no effect on the widgets or tabs in view. + * + *

If the given name is a number, Elastic will select the tab whose index equals the number + * provided. + * + * @param tabName the name of the tab to select + */ + public static void selectTab(String tabName) { + selectedTabPublisher.set(tabName); + } + + /** + * Selects the tab of the dashboard at the given index. If this index is greater than or equal to + * the number of tabs, this will have no effect. + * + * @param tabIndex the index of the tab to select. + */ + public static void selectTab(int tabIndex) { + selectTab(Integer.toString(tabIndex)); + } + + /** + * Represents the possible levels of notifications for the Elastic dashboard. These levels are + * used to indicate the severity or type of notification. + */ + public enum NotificationLevel { + /** Informational Message */ + INFO, + /** Warning message */ + WARNING, + /** Error message */ + ERROR + } + + /** + * Represents an notification object to be sent to the Elastic dashboard. This object holds + * properties such as level, title, description, display time, and dimensions to control how the + * notification is displayed on the dashboard. + */ + @Setter + @Getter + public static class Notification { + /** + * Updates the level of this notification + * + * @return the level of this notification + * @param level the level to set the notification to + */ + private NotificationLevel level; + + /** + * Updates the title of this notification + * + * @return the title of this notification + * @param title the title to set the notification to + */ + private String title; + + /** + * Updates the description of this notification + * + * @param description the description to set the notification to + */ + private String description; + + /** + * Updates the display time of the notification in milliseconds + * + * @return the number of milliseconds the notification is displayed for + * @param displayTimeMillis the number of milliseconds to display the notification for + */ + private int displayTimeMillis; + + /** + * Updates the width of the notification + * + * @return the width of the notification + * @param width the width to set the notification to + */ + private double width; + + /** + * Updates the height of the notification + * + *

If the height is set to -1, the height will be determined automatically by the dashboard + * + * @return the height of the notification + * @param height the height to set the notification to + */ + private double height; + + /** + * Creates a new Notification with all default parameters. This constructor is intended to be + * used with the chainable decorator methods + * + *

Title and description fields are empty. + */ + public Notification() { + this(NotificationLevel.INFO, "", ""); + } + + /** + * Creates a new Notification with all properties specified. + * + * @param level the level of the notification (e.g., INFO, WARNING, ERROR) + * @param title the title text of the notification + * @param description the descriptive text of the notification + * @param displayTimeMillis the time in milliseconds for which the notification is displayed + * @param width the width of the notification display area + * @param height the height of the notification display area, inferred if below zero + */ + public Notification( + NotificationLevel level, + String title, + String description, + int displayTimeMillis, + double width, + double height) { + this.level = level; + this.title = title; + this.displayTimeMillis = displayTimeMillis; + this.description = description; + this.height = height; + this.width = width; + } + + /** + * Creates a new Notification with default display time and dimensions. + * + * @param level the level of the notification + * @param title the title text of the notification + * @param description the descriptive text of the notification + */ + public Notification(NotificationLevel level, String title, String description) { + this(level, title, description, 3000, 350, -1); + } + + /** + * Creates a new Notification with a specified display time and default dimensions. + * + * @param level the level of the notification + * @param title the title text of the notification + * @param description the descriptive text of the notification + * @param displayTimeMillis the display time in milliseconds + */ + public Notification( + NotificationLevel level, String title, String description, int displayTimeMillis) { + this(level, title, description, displayTimeMillis, 350, -1); + } + + /** + * Creates a new Notification with specified dimensions and default display time. If the height + * is below zero, it is automatically inferred based on screen size. + * + * @param level the level of the notification + * @param title the title text of the notification + * @param description the descriptive text of the notification + * @param width the width of the notification display area + * @param height the height of the notification display area, inferred if below zero + */ + public Notification( + NotificationLevel level, String title, String description, double width, double height) { + this(level, title, description, 3000, width, height); + } + + /** + * Updates the display time of the notification + * + * @param seconds the number of seconds to display the notification for + */ + public void setDisplayTimeSeconds(double seconds) { + setDisplayTimeMillis((int) Math.round(seconds * 1000)); + } + + /** + * Modifies the notification's level and returns itself to allow for method chaining + * + * @param level the level to set the notification to + * @return the current notification + */ + public Notification withLevel(NotificationLevel level) { + this.level = level; + return this; + } + + /** + * Modifies the notification's title and returns itself to allow for method chaining + * + * @param title the title to set the notification to + * @return the current notification + */ + public Notification withTitle(String title) { + setTitle(title); + return this; + } + + /** + * Modifies the notification's description and returns itself to allow for method chaining + * + * @param description the description to set the notification to + * @return the current notification + */ + public Notification withDescription(String description) { + setDescription(description); + return this; + } + + /** + * Modifies the notification's display time and returns itself to allow for method chaining + * + * @param seconds the number of seconds to display the notification for + * @return the current notification + */ + public Notification withDisplaySeconds(double seconds) { + return withDisplayMilliseconds((int) Math.round(seconds * 1000)); + } + + /** + * Modifies the notification's display time and returns itself to allow for method chaining + * + * @param displayTimeMillis the number of milliseconds to display the notification for + * @return the current notification + */ + public Notification withDisplayMilliseconds(int displayTimeMillis) { + setDisplayTimeMillis(displayTimeMillis); + return this; + } + + /** + * Modifies the notification's width and returns itself to allow for method chaining + * + * @param width the width to set the notification to + * @return the current notification + */ + public Notification withWidth(double width) { + setWidth(width); + return this; + } + + /** + * Modifies the notification's height and returns itself to allow for method chaining + * + * @param height the height to set the notification to + * @return the current notification + */ + public Notification withHeight(double height) { + setHeight(height); + return this; + } + + /** + * Modifies the notification's height and returns itself to allow for method chaining + * + *

This will set the height to -1 to have it automatically determined by the dashboard + * + * @return the current notification + */ + public Notification withAutomaticHeight() { + setHeight(-1); + return this; + } + + /** + * Modifies the notification to disable the auto dismiss behavior + * + *

This sets the display time to 0 milliseconds + * + *

The auto dismiss behavior can be re-enabled by setting the display time to a number + * greater than 0 + * + * @return the current notification + */ + public Notification withNoAutoDismiss() { + setDisplayTimeMillis(0); + return this; + } + } +} diff --git a/lib/examples/swerve/src/main/java/frc/robot/util/InternalLoggedTracer.java b/lib/examples/swerve/src/main/java/frc/robot/util/InternalLoggedTracer.java new file mode 100644 index 00000000..745e4baa --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/util/InternalLoggedTracer.java @@ -0,0 +1,31 @@ +// Copyright (c) 2025 FRC 6328 +// http://github.com/Mechanical-Advantage +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file at +// the root directory of this project. + +package frc.robot.util; + +import edu.wpi.first.wpilibj.Timer; +import org.littletonrobotics.junction.Logger; + +/** Utility class for logging code execution times. */ +public class InternalLoggedTracer { + private InternalLoggedTracer() {} + + private static double startTime = -1.0; + + /** Reset the clock. */ + public static void reset() { + startTime = Timer.getFPGATimestamp(); + } + + /** Save the time elapsed since the last reset or record. */ + public static void record(String epochName, String prefix) { + double now = Timer.getFPGATimestamp(); + Logger.recordOutput( + "LoggedTracer/" + prefix + "/" + epochName + "MS", (now - startTime) * 1000.0); + startTime = now; + } +} diff --git a/lib/examples/swerve/src/main/java/frc/robot/util/LTNUpdater.java b/lib/examples/swerve/src/main/java/frc/robot/util/LTNUpdater.java new file mode 100644 index 00000000..5498c297 --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/util/LTNUpdater.java @@ -0,0 +1 @@ +package frc.robot.util; diff --git a/lib/examples/swerve/src/main/java/frc/robot/util/NTPrefixes.java b/lib/examples/swerve/src/main/java/frc/robot/util/NTPrefixes.java new file mode 100644 index 00000000..2f7e564d --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/util/NTPrefixes.java @@ -0,0 +1,11 @@ +package frc.robot.util; + +public class NTPrefixes { + public static final String ROBOT_STATE = "RobotState/"; + public static final String OI_DATA = ROBOT_STATE + "Operator Input Data/"; + public static final String POSE_DATA = ROBOT_STATE + "Pose Data/"; + + public static final String CANIVORE_STATUS = ROBOT_STATE + "CANivore Status/"; + + public static final String SUPERSTRUCTURE = "Superstructure/"; +} diff --git a/lib/examples/swerve/src/main/java/frc/robot/util/ShotCalculator.java b/lib/examples/swerve/src/main/java/frc/robot/util/ShotCalculator.java new file mode 100644 index 00000000..419969a4 --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/util/ShotCalculator.java @@ -0,0 +1,65 @@ +package frc.robot.util; + +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Transform2d; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.geometry.Twist2d; +import edu.wpi.first.math.kinematics.ChassisSpeeds; +import java.util.function.Function; + +public interface ShotCalculator { + static final double phaseDelay = 0.3; // TODO: Change this value based on testing + + /** + * Calculates a corrected pose for a moving target based on the shooter's current velocity. + * + * @param initialPose The current pose of the shooter. + * @param targetPose The pose of the target. + * @param robotVelocityMetersPerSecond The shooter's velocity in meters per second. + * @param distanceToTimeFunction A function that converts distance to time. + * @return The corrected pose to aim at. + */ + public default Translation2d getAdjustedTargetPose( + Pose2d initialPose, + Pose2d targetPose, + ChassisSpeeds robotVelocityMetersPerSecond, + Function distanceToTimeFunction, + Transform2d centerToShooterCenter) { + + // Adds phase delay to the initial pose based on robot velocity to account for latency caused by + // target pose calculation + initialPose = + initialPose.exp( + new Twist2d( + robotVelocityMetersPerSecond.vxMetersPerSecond * phaseDelay, + robotVelocityMetersPerSecond.vyMetersPerSecond * phaseDelay, + robotVelocityMetersPerSecond.omegaRadiansPerSecond * phaseDelay)); + + Pose2d shooterPose = initialPose.plus(centerToShooterCenter); + Transform2d shooterToTarget = new Transform2d(shooterPose, targetPose); + + Translation2d shooterRobotFrameVelocityMetersPerSecond = + new Translation2d( + -centerToShooterCenter.getRotation().getSin(), + centerToShooterCenter.getRotation().getCos()) + .times(robotVelocityMetersPerSecond.omegaRadiansPerSecond) + .times(centerToShooterCenter.getTranslation().getNorm()); + + Translation2d shooterFieldFrameVelocityMetersPerSecond = + shooterRobotFrameVelocityMetersPerSecond + .rotateBy(initialPose.getRotation()) + .plus( + new Translation2d( + robotVelocityMetersPerSecond.vxMetersPerSecond, + robotVelocityMetersPerSecond.vyMetersPerSecond)); + + double deltaT = distanceToTimeFunction.apply(shooterToTarget.getTranslation().getNorm()); + + double correctedX = + targetPose.getX() - shooterFieldFrameVelocityMetersPerSecond.getX() * deltaT; + double correctedY = + targetPose.getY() - shooterFieldFrameVelocityMetersPerSecond.getY() * deltaT; + + return new Translation2d(correctedX, correctedY); + } +} diff --git a/lib/examples/swerve/src/main/java/frc/robot/util/SystemTimeValidReader.java b/lib/examples/swerve/src/main/java/frc/robot/util/SystemTimeValidReader.java new file mode 100644 index 00000000..6a8ffc85 --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/util/SystemTimeValidReader.java @@ -0,0 +1,40 @@ +// Copyright (c) 2025 FRC 6328 +// http://github.com/Mechanical-Advantage +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file at +// the root directory of this project. + +package frc.robot.util; + +import edu.wpi.first.wpilibj.RobotController; + +public class SystemTimeValidReader { + private static Thread thread = null; + private static boolean ready = false; + + public static void start() { + if (thread != null) return; + thread = + new Thread( + () -> { + while (true) { + boolean readyNew = RobotController.isSystemTimeValid(); + synchronized (SystemTimeValidReader.class) { + ready = readyNew; + } + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }); + thread.setName("SystemTimeValidReader"); + thread.start(); + } + + public static synchronized boolean isValid() { + return ready; + } +} diff --git a/lib/examples/swerve/src/main/java/frc/robot/util/input/XKeysInput.java b/lib/examples/swerve/src/main/java/frc/robot/util/input/XKeysInput.java new file mode 100644 index 00000000..879ca577 --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/util/input/XKeysInput.java @@ -0,0 +1,454 @@ +package frc.robot.util.input; + +import edu.wpi.first.wpilibj.event.EventLoop; +import edu.wpi.first.wpilibj2.command.CommandScheduler; +import edu.wpi.first.wpilibj2.command.button.CommandGenericHID; +import edu.wpi.first.wpilibj2.command.button.Trigger; + +@SuppressWarnings("MethodName") +public class XKeysInput extends CommandGenericHID { + /** + * Construct an instance of a controller. + * + * @param port The port index on the Driver Station that the controller is plugged into. + */ + public XKeysInput(int port) { + super(port); + } + + // Rows are lettered a-h from top to bottom + public enum Rows { + ka(11), + kb(12), + kc(13), + kd(14), + ke(15), + kf(16), + kg(17), + kh(18); + public final int value; + + Rows(int value) { + this.value = value; + } + } + + // Columns are numbered 1-10 from left to right + public enum Cols { + k1(1), + k2(2), + k3(3), + k4(4), + k5(5), + k6(6), + k7(7), + k8(8), + k9(9), + k10(10); + public final int value; + + Cols(int value) { + this.value = value; + } + } + + public Trigger a1() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.ka.value, loop).and(button(Cols.k1.value, loop)); + } + + public Trigger a2() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.ka.value, loop).and(button(Cols.k2.value, loop)); + } + + public Trigger a3() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.ka.value, loop).and(button(Cols.k3.value, loop)); + } + + public Trigger a4() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.ka.value, loop).and(button(Cols.k4.value, loop)); + } + + public Trigger a5() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.ka.value, loop).and(button(Cols.k5.value, loop)); + } + + public Trigger a6() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.ka.value, loop).and(button(Cols.k6.value, loop)); + } + + public Trigger a7() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.ka.value, loop).and(button(Cols.k7.value, loop)); + } + + public Trigger a8() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.ka.value, loop).and(button(Cols.k8.value, loop)); + } + + public Trigger a9() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.ka.value, loop).and(button(Cols.k9.value, loop)); + } + + public Trigger a10() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.ka.value, loop).and(button(Cols.k10.value, loop)); + } + + public Trigger b1() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kb.value, loop).and(button(Cols.k1.value, loop)); + } + + public Trigger b2() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kb.value, loop).and(button(Cols.k2.value, loop)); + } + + public Trigger b3() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kb.value, loop).and(button(Cols.k3.value, loop)); + } + + public Trigger b4() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kb.value, loop).and(button(Cols.k4.value, loop)); + } + + public Trigger b5() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kb.value, loop).and(button(Cols.k5.value, loop)); + } + + public Trigger b6() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kb.value, loop).and(button(Cols.k6.value, loop)); + } + + public Trigger b7() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kb.value, loop).and(button(Cols.k7.value, loop)); + } + + public Trigger b8() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kb.value, loop).and(button(Cols.k8.value, loop)); + } + + public Trigger b9() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kb.value, loop).and(button(Cols.k9.value, loop)); + } + + public Trigger b10() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kb.value, loop).and(button(Cols.k10.value, loop)); + } + + public Trigger c1() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kc.value, loop).and(button(Cols.k1.value, loop)); + } + + public Trigger c2() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kc.value, loop).and(button(Cols.k2.value, loop)); + } + + public Trigger c3() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kc.value, loop).and(button(Cols.k3.value, loop)); + } + + public Trigger c4() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kc.value, loop).and(button(Cols.k4.value, loop)); + } + + public Trigger c5() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kc.value, loop).and(button(Cols.k5.value, loop)); + } + + public Trigger c6() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kc.value, loop).and(button(Cols.k6.value, loop)); + } + + public Trigger c7() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kc.value, loop).and(button(Cols.k7.value, loop)); + } + + public Trigger c8() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kc.value, loop).and(button(Cols.k8.value, loop)); + } + + public Trigger c9() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kc.value, loop).and(button(Cols.k9.value, loop)); + } + + public Trigger c10() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kc.value, loop).and(button(Cols.k10.value, loop)); + } + + public Trigger d1() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kd.value, loop).and(button(Cols.k1.value, loop)); + } + + public Trigger d2() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kd.value, loop).and(button(Cols.k2.value, loop)); + } + + public Trigger d3() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kd.value, loop).and(button(Cols.k3.value, loop)); + } + + public Trigger d4() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kd.value, loop).and(button(Cols.k4.value, loop)); + } + + public Trigger d5() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kd.value, loop).and(button(Cols.k5.value, loop)); + } + + public Trigger d6() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kd.value, loop).and(button(Cols.k6.value, loop)); + } + + public Trigger d7() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kd.value, loop).and(button(Cols.k7.value, loop)); + } + + public Trigger d8() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kd.value, loop).and(button(Cols.k8.value, loop)); + } + + public Trigger d9() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kd.value, loop).and(button(Cols.k9.value, loop)); + } + + public Trigger d10() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kd.value, loop).and(button(Cols.k10.value, loop)); + } + + public Trigger e1() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.ke.value, loop).and(button(Cols.k1.value, loop)); + } + + public Trigger e2() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.ke.value, loop).and(button(Cols.k2.value, loop)); + } + + public Trigger e3() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.ke.value, loop).and(button(Cols.k3.value, loop)); + } + + public Trigger e4() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.ke.value, loop).and(button(Cols.k4.value, loop)); + } + + public Trigger e5() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.ke.value, loop).and(button(Cols.k5.value, loop)); + } + + public Trigger e6() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.ke.value, loop).and(button(Cols.k6.value, loop)); + } + + public Trigger e7() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.ke.value, loop).and(button(Cols.k7.value, loop)); + } + + public Trigger e8() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.ke.value, loop).and(button(Cols.k8.value, loop)); + } + + public Trigger e9() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.ke.value, loop).and(button(Cols.k9.value, loop)); + } + + public Trigger e10() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.ke.value, loop).and(button(Cols.k10.value, loop)); + } + + public Trigger f1() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kf.value, loop).and(button(Cols.k1.value, loop)); + } + + public Trigger f2() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kf.value, loop).and(button(Cols.k2.value, loop)); + } + + public Trigger f3() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kf.value, loop).and(button(Cols.k3.value, loop)); + } + + public Trigger f4() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kf.value, loop).and(button(Cols.k4.value, loop)); + } + + public Trigger f5() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kf.value, loop).and(button(Cols.k5.value, loop)); + } + + public Trigger f6() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kf.value, loop).and(button(Cols.k6.value, loop)); + } + + public Trigger f7() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kf.value, loop).and(button(Cols.k7.value, loop)); + } + + public Trigger f8() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kf.value, loop).and(button(Cols.k8.value, loop)); + } + + public Trigger f9() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kf.value, loop).and(button(Cols.k9.value, loop)); + } + + public Trigger f10() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kf.value, loop).and(button(Cols.k10.value, loop)); + } + + public Trigger g1() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kg.value, loop).and(button(Cols.k1.value, loop)); + } + + public Trigger g2() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kg.value, loop).and(button(Cols.k2.value, loop)); + } + + public Trigger g3() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kg.value, loop).and(button(Cols.k3.value, loop)); + } + + public Trigger g4() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kg.value, loop).and(button(Cols.k4.value, loop)); + } + + public Trigger g5() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kg.value, loop).and(button(Cols.k5.value, loop)); + } + + public Trigger g6() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kg.value, loop).and(button(Cols.k6.value, loop)); + } + + public Trigger g7() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kg.value, loop).and(button(Cols.k7.value, loop)); + } + + public Trigger g8() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kg.value, loop).and(button(Cols.k8.value, loop)); + } + + public Trigger g9() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kg.value, loop).and(button(Cols.k9.value, loop)); + } + + public Trigger g10() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kg.value, loop).and(button(Cols.k10.value, loop)); + } + + public Trigger h1() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kh.value, loop).and(button(Cols.k1.value, loop)); + } + + public Trigger h2() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kh.value, loop).and(button(Cols.k2.value, loop)); + } + + public Trigger h3() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kh.value, loop).and(button(Cols.k3.value, loop)); + } + + public Trigger h4() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kh.value, loop).and(button(Cols.k4.value, loop)); + } + + public Trigger h5() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kh.value, loop).and(button(Cols.k5.value, loop)); + } + + public Trigger h6() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kh.value, loop).and(button(Cols.k6.value, loop)); + } + + public Trigger h7() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kh.value, loop).and(button(Cols.k7.value, loop)); + } + + public Trigger h8() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kh.value, loop).and(button(Cols.k8.value, loop)); + } + + public Trigger h9() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kh.value, loop).and(button(Cols.k9.value, loop)); + } + + public Trigger h10() { + EventLoop loop = CommandScheduler.getInstance().getDefaultButtonLoop(); + return button(Rows.kh.value, loop).and(button(Cols.k10.value, loop)); + } +} diff --git a/lib/examples/swerve/src/main/java/frc/robot/util/input/XboxElite2Input.java b/lib/examples/swerve/src/main/java/frc/robot/util/input/XboxElite2Input.java new file mode 100644 index 00000000..c8f91dbd --- /dev/null +++ b/lib/examples/swerve/src/main/java/frc/robot/util/input/XboxElite2Input.java @@ -0,0 +1,102 @@ +package frc.robot.util.input; + +import edu.wpi.first.wpilibj.XboxController; +import edu.wpi.first.wpilibj.event.EventLoop; +import edu.wpi.first.wpilibj2.command.CommandScheduler; +import edu.wpi.first.wpilibj2.command.button.CommandXboxController; +import edu.wpi.first.wpilibj2.command.button.Trigger; + +public class XboxElite2Input extends CommandXboxController { + + public XboxElite2Input(int port) { + super(port); + } + + /** + * Constructs a Trigger instance around the top left paddle's digital signal. + * + * @return a Trigger instance representing the top left paddle's digital signal attached to the + * {@link CommandScheduler#getDefaultButtonLoop() default scheduler button loop}. + * @see #topLeftPaddle(EventLoop) + */ + public Trigger topLeftPaddle() { + return back(CommandScheduler.getInstance().getDefaultButtonLoop()); + } + + /** + * Constructs a Trigger instance around the top left paddle's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the top left paddle's digital signal attached to the + * given loop. + */ + public Trigger topLeftPaddle(EventLoop loop) { + return button(XboxController.Button.kBack.value, loop); + } + + /** + * Constructs a Trigger instance around the top right paddle's digital signal. + * + * @return a Trigger instance representing the top right paddle's digital signal attached to the + * {@link CommandScheduler#getDefaultButtonLoop() default scheduler button loop}. + * @see #topRightPaddle(EventLoop) + */ + public Trigger topRightPaddle() { + return start(CommandScheduler.getInstance().getDefaultButtonLoop()); + } + + /** + * Constructs a Trigger instance around the top right paddle's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the top right paddle's digital signal attached to the + * given loop. + */ + public Trigger topRightPaddle(EventLoop loop) { + return button(XboxController.Button.kStart.value, loop); + } + + /** + * Constructs a Trigger instance around the bottom left paddle's digital signal. + * + * @return a Trigger instance representing the bottom left paddle's digital signal attached to the + * {@link CommandScheduler#getDefaultButtonLoop() default scheduler button loop}. + * @see #bottomLeftPaddle(EventLoop) + */ + public Trigger bottomLeftPaddle() { + return leftStick(CommandScheduler.getInstance().getDefaultButtonLoop()); + } + + /** + * Constructs a Trigger instance around the bottom left paddle's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the bottom left paddle's digital signal attached to the + * given loop. + */ + public Trigger bottomLeftPaddle(EventLoop loop) { + return button(XboxController.Button.kLeftStick.value, loop); + } + + /** + * Constructs a Trigger instance around the bottom right paddle's digital signal. + * + * @return a Trigger instance representing the bottom right paddle's digital signal attached to + * the {@link CommandScheduler#getDefaultButtonLoop() default scheduler button loop}. + * @see #bottomRightPaddle(EventLoop) + */ + public Trigger bottomRightPaddle() { + return rightStick(CommandScheduler.getInstance().getDefaultButtonLoop()); + } + + /** + * Constructs a Trigger instance around the bottom right paddle's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the bottom right paddle's digital signal attached to + * the given loop. + */ + public Trigger bottomRightPaddle(EventLoop loop) { + return button(XboxController.Button.kRightStick.value, loop); + } +} diff --git a/lib/examples/swerve/vendordeps/AdvantageKit.json b/lib/examples/swerve/vendordeps/AdvantageKit.json new file mode 100644 index 00000000..2faa4db8 --- /dev/null +++ b/lib/examples/swerve/vendordeps/AdvantageKit.json @@ -0,0 +1,35 @@ +{ + "fileName": "AdvantageKit.json", + "name": "AdvantageKit", + "version": "26.0.0", + "uuid": "d820cc26-74e3-11ec-90d6-0242ac120003", + "frcYear": "2026", + "mavenUrls": [ + "https://frcmaven.wpi.edu/artifactory/littletonrobotics-mvn-release/" + ], + "jsonUrl": "https://github.com/Mechanical-Advantage/AdvantageKit/releases/latest/download/AdvantageKit.json", + "javaDependencies": [ + { + "groupId": "org.littletonrobotics.akit", + "artifactId": "akit-java", + "version": "26.0.0" + } + ], + "jniDependencies": [ + { + "groupId": "org.littletonrobotics.akit", + "artifactId": "akit-wpilibio", + "version": "26.0.0", + "skipInvalidPlatforms": false, + "isJar": false, + "validPlatforms": [ + "linuxathena", + "linuxx86-64", + "linuxarm64", + "osxuniversal", + "windowsx86-64" + ] + } + ], + "cppDependencies": [] +} diff --git a/lib/examples/swerve/vendordeps/ChoreoLib.json b/lib/examples/swerve/vendordeps/ChoreoLib.json new file mode 100644 index 00000000..cf7e7cf3 --- /dev/null +++ b/lib/examples/swerve/vendordeps/ChoreoLib.json @@ -0,0 +1,44 @@ +{ + "fileName": "ChoreoLib2026.json", + "name": "ChoreoLib", + "version": "2026.0.1", + "uuid": "b5e23f0a-dac9-4ad2-8dd6-02767c520aca", + "frcYear": "2026", + "mavenUrls": [ + "https://frcmaven.wpi.edu/artifactory/sleipnirgroup-mvn-release/", + "https://repo1.maven.org/maven2" + ], + "jsonUrl": "https://choreo.autos/lib/ChoreoLib2026.json", + "javaDependencies": [ + { + "groupId": "choreo", + "artifactId": "ChoreoLib-java", + "version": "2026.0.1" + }, + { + "groupId": "com.google.code.gson", + "artifactId": "gson", + "version": "2.11.0" + } + ], + "jniDependencies": [], + "cppDependencies": [ + { + "groupId": "choreo", + "artifactId": "ChoreoLib-cpp", + "version": "2026.0.1", + "libName": "ChoreoLib", + "headerClassifier": "headers", + "sharedLibrary": false, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal", + "linuxathena", + "linuxarm32", + "linuxarm64" + ] + } + ] +} diff --git a/lib/examples/swerve/vendordeps/Phoenix6-26.1.1.json b/lib/examples/swerve/vendordeps/Phoenix6-26.1.1.json new file mode 100644 index 00000000..b136b65b --- /dev/null +++ b/lib/examples/swerve/vendordeps/Phoenix6-26.1.1.json @@ -0,0 +1,449 @@ +{ + "fileName": "Phoenix6-frc2026-latest.json", + "name": "CTRE-Phoenix (v6)", + "version": "26.1.1", + "frcYear": "2026", + "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-frc2026-latest.json", + "conflictsWith": [ + { + "uuid": "e7900d8d-826f-4dca-a1ff-182f658e98af", + "errorMessage": "Users can not have both the replay and regular Phoenix 6 vendordeps in their robot program.", + "offlineFileName": "Phoenix6-replay-frc2026-latest.json" + } + ], + "javaDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "wpiapi-java", + "version": "26.1.1" + } + ], + "jniDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "api-cpp", + "version": "26.1.1", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6", + "artifactId": "tools", + "version": "26.1.1", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "api-cpp-sim", + "version": "26.1.1", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "tools-sim", + "version": "26.1.1", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonSRX", + "version": "26.1.1", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simVictorSPX", + "version": "26.1.1", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simPigeonIMU", + "version": "26.1.1", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProTalonFX", + "version": "26.1.1", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProTalonFXS", + "version": "26.1.1", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANcoder", + "version": "26.1.1", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProPigeon2", + "version": "26.1.1", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANrange", + "version": "26.1.1", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANdi", + "version": "26.1.1", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANdle", + "version": "26.1.1", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + } + ], + "cppDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "wpiapi-cpp", + "version": "26.1.1", + "libName": "CTRE_Phoenix6_WPI", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6", + "artifactId": "tools", + "version": "26.1.1", + "libName": "CTRE_PhoenixTools", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "wpiapi-cpp-sim", + "version": "26.1.1", + "libName": "CTRE_Phoenix6_WPISim", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "tools-sim", + "version": "26.1.1", + "libName": "CTRE_PhoenixTools_Sim", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonSRX", + "version": "26.1.1", + "libName": "CTRE_SimTalonSRX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simVictorSPX", + "version": "26.1.1", + "libName": "CTRE_SimVictorSPX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simPigeonIMU", + "version": "26.1.1", + "libName": "CTRE_SimPigeonIMU", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProTalonFX", + "version": "26.1.1", + "libName": "CTRE_SimProTalonFX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProTalonFXS", + "version": "26.1.1", + "libName": "CTRE_SimProTalonFXS", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANcoder", + "version": "26.1.1", + "libName": "CTRE_SimProCANcoder", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProPigeon2", + "version": "26.1.1", + "libName": "CTRE_SimProPigeon2", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANrange", + "version": "26.1.1", + "libName": "CTRE_SimProCANrange", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANdi", + "version": "26.1.1", + "libName": "CTRE_SimProCANdi", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANdle", + "version": "26.1.1", + "libName": "CTRE_SimProCANdle", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "swsim" + } + ] +} diff --git a/lib/examples/swerve/vendordeps/WPILibNewCommands.json b/lib/examples/swerve/vendordeps/WPILibNewCommands.json new file mode 100644 index 00000000..d90630e9 --- /dev/null +++ b/lib/examples/swerve/vendordeps/WPILibNewCommands.json @@ -0,0 +1,39 @@ +{ + "fileName": "WPILibNewCommands.json", + "name": "WPILib-New-Commands", + "version": "1.0.0", + "uuid": "111e20f7-815e-48f8-9dd6-e675ce75b266", + "frcYear": "2026", + "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": [ + "linuxsystemcore", + "linuxathena", + "linuxarm32", + "linuxarm64", + "windowsx86-64", + "windowsx86", + "linuxx86-64", + "osxuniversal" + ] + } + ] +} diff --git a/lib/gradle/libs.versions.toml b/lib/gradle/libs.versions.toml new file mode 100644 index 00000000..c4681e12 --- /dev/null +++ b/lib/gradle/libs.versions.toml @@ -0,0 +1,88 @@ + +[versions] +wpilib = "2026.2.1" +wpilib-jni = "2025.3.2" +phoenix6 = "26.1.3" +choreo = "2026.0.2" +aspectj = "1.9.21" +advantagekit = "26.0.2" +quickbuf = "1.4" +jackson = "2.17.2" +pathplanner = "2026.1.2" + +[libraries] +# WPILib Core +wpilib-wpilibj = { group = "edu.wpi.first.wpilibj", name = "wpilibj-java", version.ref = "wpilib" } +wpilib-wpimath = { group = "edu.wpi.first.wpimath", name = "wpimath-java", version.ref = "wpilib" } +wpilib-ntcore = { group = "edu.wpi.first.ntcore", name = "ntcore-java", version.ref = "wpilib" } +wpilib-wpinet = { group = "edu.wpi.first.wpinet", name = "wpinet-java", version.ref = "wpilib" } +wpilib-cscore = { group = "edu.wpi.first.cscore", name = "cscore-java", version.ref = "wpilib" } +wpilib-cameraserver = { group = "edu.wpi.first.cameraserver", name = "cameraserver-java", version.ref = "wpilib" } +wpilib-wpiutil = { group = "edu.wpi.first.wpiutil", name = "wpiutil-java", version.ref = "wpilib" } +wpilib-wpiunits = { group = "edu.wpi.first.wpiunits", name = "wpiunits-java", version.ref = "wpilib" } +wpilib-commands = { group = "edu.wpi.first.wpilibNewCommands", name = "wpilibNewCommands-java", version.ref = "wpilib" } +wpilib-hal = { group = "edu.wpi.first.hal", name = "hal-java", version.ref = "wpilib" } +wpilib-apriltag = {group = "edu.wpi.first.apriltag", name="apriltag-java", version.ref = "wpilib"} + +# WPILib JNI +wpilib-ntcore-jni = { group = "edu.wpi.first.ntcore", name = "ntcore-jni", version.ref = "wpilib-jni" } +wpilib-wpiutil-jni = { group = "edu.wpi.first.wpiutil", name = "wpiutil-jni", version.ref = "wpilib-jni" } +wpilib-hal-jni = { group = "edu.wpi.first.hal", name = "hal-jni", version.ref = "wpilib-jni" } + +# CTRE Phoenix 6/Pro +phoenix6 = { group = "com.ctre.phoenix6", name = "wpiapi-java", version.ref = "phoenix6" } + +# Choreo +choreo = { group = "choreo", name = "ChoreoLib-java", version.ref = "choreo" } + +# PathplannerLib +pathplanner = { group = "com.pathplanner.lib", name = "PathplannerLib-java", version.ref = "pathplanner" } + +# AspectJ +aspectj-rt = { group = "org.aspectj", name = "aspectjrt", version.ref = "aspectj" } +aspectj-tools = { group = "org.aspectj", name = "aspectjtools", version.ref = "aspectj" } + +# AdvantageKit +advantagekit-java = { group = "org.littletonrobotics.akit", name = "akit-java", version.ref = "advantagekit" } +advantagekit-autolog = { group = "org.littletonrobotics.akit", name = "akit-autolog", version.ref = "advantagekit" } + +# QuickBuf / Jackson (runtime dependencies needed for generated code) +quickbuf = { group = "us.hebi.quickbuf", name = "quickbuf-runtime", version.ref = "quickbuf" } +jackson-annotations = { group = "com.fasterxml.jackson.core", name = "jackson-annotations", version.ref = "jackson" } +jackson-core = { group = "com.fasterxml.jackson.core", name = "jackson-core", version.ref = "jackson" } +jackson-databind = { group = "com.fasterxml.jackson.core", name = "jackson-databind", version.ref = "jackson" } + +[bundles] +wpilib = [ + "wpilib-wpilibj", + "wpilib-wpimath", + "wpilib-ntcore", + "wpilib-wpinet", + "wpilib-cscore", + "wpilib-cameraserver", + "wpilib-wpiutil", + "wpilib-wpiunits", + "wpilib-commands", + "wpilib-hal", + "wpilib-apriltag" +] + +wpilib-runtime = [ + "wpilib-ntcore-jni", + "wpilib-wpiutil-jni", + "wpilib-hal-jni" +] + +aspectj = [ + "aspectj-rt", + "aspectj-tools" +] + +advantagekit = [ + "advantagekit-java", + "advantagekit-autolog", + "quickbuf", + "jackson-annotations", + "jackson-core", + "jackson-databind" +] diff --git a/lib/gradle/native-utils.gradle b/lib/gradle/native-utils.gradle new file mode 100644 index 00000000..31451f56 --- /dev/null +++ b/lib/gradle/native-utils.gradle @@ -0,0 +1,40 @@ +// Detect OS for FRC Natives +def osClassifier = { + def osName = System.getProperty("os.name").toLowerCase() + if (osName.contains("win")) return "windowsx86-64" + if (osName.contains("mac")) return "osxuniversal" + return "linuxx86-64" +}() + +configurations { + nativeDeps +} + +dependencies { + // Uses the version provider from your libs.versions.toml [cite: 4] + def wpilibVer = libs.versions.wpilib.jni.get() + nativeDeps "edu.wpi.first.ntcore:ntcore-jni:${wpilibVer}:${osClassifier}" + nativeDeps "edu.wpi.first.wpiutil:wpiutil-jni:${wpilibVer}:${osClassifier}" + nativeDeps "edu.wpi.first.hal:hal-jni:${wpilibVer}:${osClassifier}" +} + +def jniDestDir = layout.buildDirectory.dir("jni") + +tasks.register('extractNativeLibs', Copy) { + from { + configurations.nativeDeps.collect { zipTree(it) } + } + into jniDestDir + // Supports Windows (.dll), OSX (.dylib), and Linux (.so) + include "**/*.so", "**/*.dll", "**/*.dylib" + eachFile { path = name } + includeEmptyDirs = false +} + +// Ensure all test tasks depend on extraction and see the library path +tasks.withType(Test).configureEach { + dependsOn extractNativeLibs + // .get().asFile converts the DirectoryProperty to a standard File object + systemProperty "java.library.path", jniDestDir.get().asFile.absolutePath + systemProperty "jni.extract.path", jniDestDir.get().asFile.absolutePath +} diff --git a/lib/gradle/wrapper/gradle-wrapper.jar b/lib/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..249e5832 Binary files /dev/null and b/lib/gradle/wrapper/gradle-wrapper.jar differ diff --git a/lib/gradle/wrapper/gradle-wrapper.properties b/lib/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..99e3a00a --- /dev/null +++ b/lib/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Sep 18 10:41:29 EDT 2025 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/lib/gradlew b/lib/gradlew new file mode 100755 index 00000000..1b6c7873 --- /dev/null +++ b/lib/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# 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"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +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. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + 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. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/lib/gradlew.bat b/lib/gradlew.bat new file mode 100644 index 00000000..ac1b06f9 --- /dev/null +++ b/lib/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/lib/lombok.config b/lib/lombok.config new file mode 100644 index 00000000..8f7e8aa1 --- /dev/null +++ b/lib/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true \ No newline at end of file diff --git a/lib/settings.gradle b/lib/settings.gradle new file mode 100644 index 00000000..53311584 --- /dev/null +++ b/lib/settings.gradle @@ -0,0 +1,4 @@ +rootProject.name = 'gompeilib' + +include(":swerve") +project(":swerve").projectDir = file("examples/swerve") diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/GompeiLib.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/GompeiLib.java new file mode 100644 index 00000000..fa26bf97 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/GompeiLib.java @@ -0,0 +1,69 @@ +package edu.wpi.team190.gompeilib.core; + +import edu.wpi.team190.gompeilib.core.robot.RobotMode; + +/** Main class for the GompeiLib library. Must be initialized by calling init() in robotInit(). */ +public final class GompeiLib { + + private static boolean initialized = false; + private static RobotMode currentMode; + private static boolean tuningMode; + private static double loopPeriod; + + /** + * Initializes the GompeiLib library with global constants from the robot project. This method + * should be called once in robotInit(). + * + * @param mode The current robot mode (REAL, SIM, or REPLAY). + * @param isTuning Whether tuning mode is enabled. + * @param loopPeriodSecs The robot's main loop period in seconds. + */ + public static void init(RobotMode mode, boolean isTuning, double loopPeriodSecs) { + if (initialized) { + System.err.println("GompeiLib has already been initialized!"); + return; + } + + currentMode = mode; + tuningMode = isTuning; + loopPeriod = loopPeriodSecs; + + initialized = true; + } + + public static void deinit() { + if (!initialized) { + System.err.println("GompeiLib has already been deinitialized!"); + return; + } + + currentMode = null; + tuningMode = false; + loopPeriod = 0.0; + + initialized = false; + } + + /** Throws an exception if the library has not been initialized. */ + private static void checkInitialized() { + if (!initialized) { + throw new IllegalStateException("GompeiLib.init() must be called before using GompeiLib."); + } + } + + // Public getters for other classes in your library to use. + public static RobotMode getMode() { + checkInitialized(); + return currentMode; + } + + public static boolean isTuning() { + checkInitialized(); + return tuningMode; + } + + public static double getLoopPeriod() { + checkInitialized(); + return loopPeriod; + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/io/components/inertial/GyroIO.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/io/components/inertial/GyroIO.java new file mode 100644 index 00000000..6dc31cad --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/io/components/inertial/GyroIO.java @@ -0,0 +1,44 @@ +package edu.wpi.team190.gompeilib.core.io.components.inertial; + +import static edu.wpi.first.units.Units.RadiansPerSecond; + +import com.ctre.phoenix6.StatusSignal; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.units.measure.Angle; +import edu.wpi.first.units.measure.AngularVelocity; +import java.util.Queue; +import org.littletonrobotics.junction.AutoLog; + +public interface GyroIO { + @AutoLog + public static class GyroIOInputs { + public boolean connected = false; + public Rotation2d yawPosition = new Rotation2d(); + public double[] odometryYawTimestamps = new double[] {}; + public Rotation2d[] odometryYawPositions = new Rotation2d[] {}; + public double yawVelocityRadPerSec = 0.0; + + public Rotation2d pitchPosition = new Rotation2d(); + public AngularVelocity pitchVelocity = RadiansPerSecond.zero(); + + public Rotation2d rollPosition = new Rotation2d(); + public AngularVelocity rollVelocity = RadiansPerSecond.zero(); + } + + public default void updateInputs(GyroIOInputs inputs) {} + + public default void updateInputs( + GyroIOInputs inputs, Queue yawTimestampQueue, Queue yawPositionQueue) {} + + public default StatusSignal getYaw() { + return null; + } + + public default StatusSignal getRoll() { + return null; + } + + public default StatusSignal getPitch() { + return null; + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/io/components/inertial/GyroIOPigeon2.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/io/components/inertial/GyroIOPigeon2.java new file mode 100644 index 00000000..46a935b3 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/io/components/inertial/GyroIOPigeon2.java @@ -0,0 +1,89 @@ +package edu.wpi.team190.gompeilib.core.io.components.inertial; + +import com.ctre.phoenix6.BaseStatusSignal; +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 edu.wpi.first.networktables.NetworkTablesJNI; +import edu.wpi.first.units.measure.Angle; +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.team190.gompeilib.core.GompeiLib; +import edu.wpi.team190.gompeilib.core.logging.Trace; +import edu.wpi.team190.gompeilib.core.utility.phoenix.PhoenixUtil; +import edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive.SwerveDriveConstants; +import java.util.Queue; +import java.util.function.Consumer; +import lombok.Getter; + +/** IO implementation for Pigeon 2. */ +public class GyroIOPigeon2 implements GyroIO { + @Getter private final StatusSignal yaw; + private final StatusSignal yawVelocity; + + @Getter private final StatusSignal pitch; + private final StatusSignal pitchVelocity; + + @Getter private final StatusSignal roll; + private final StatusSignal rollVelocity; + + private final Consumer networktablesTimestampConsumer; + + public GyroIOPigeon2( + SwerveDriveConstants driveConstants, Consumer networkTablesTimestampConsumer) { + Pigeon2 pigeon = + new Pigeon2(driveConstants.driveConfig.pigeon2Id(), driveConstants.driveConfig.canBus()); + pigeon.getConfigurator().apply(new Pigeon2Configuration()); + pigeon.getConfigurator().setYaw(0.0); + + yaw = pigeon.getYaw(); + yawVelocity = pigeon.getAngularVelocityZWorld(); + + yaw.setUpdateFrequency(driveConstants.odometryFrequency); + + pitch = pigeon.getPitch(); + pitchVelocity = pigeon.getAngularVelocityXWorld(); + + roll = pigeon.getRoll(); + rollVelocity = pigeon.getAngularVelocityYWorld(); + + this.networktablesTimestampConsumer = networkTablesTimestampConsumer; + + BaseStatusSignal.setUpdateFrequencyForAll( + 2 / GompeiLib.getLoopPeriod(), yawVelocity, pitch, pitchVelocity, roll, rollVelocity); + + pigeon.optimizeBusUtilization(); + + PhoenixUtil.registerSignals( + driveConstants.driveConfig.canBus().isNetworkFD(), + yaw, + yawVelocity, + pitch, + pitchVelocity, + roll, + rollVelocity); + } + + @Trace + @Override + public void updateInputs( + GyroIOInputs inputs, Queue yawTimestampQueue, Queue yawPositionQueue) { + inputs.connected = + BaseStatusSignal.isAllGood(yaw, yawVelocity, pitch, pitchVelocity, roll, rollVelocity); + inputs.yawPosition = Rotation2d.fromDegrees(yaw.getValueAsDouble()); + networktablesTimestampConsumer.accept(NetworkTablesJNI.now()); + inputs.yawVelocityRadPerSec = Units.degreesToRadians(yawVelocity.getValueAsDouble()); + + inputs.pitchPosition = new Rotation2d(pitch.getValue()); + inputs.pitchVelocity = pitchVelocity.getValue(); + + inputs.rollPosition = new Rotation2d(roll.getValue()); + inputs.rollVelocity = rollVelocity.getValue(); + + inputs.odometryYawTimestamps = + yawTimestampQueue.stream().mapToDouble((Double value) -> value).toArray(); + inputs.odometryYawPositions = + yawPositionQueue.stream().map(Rotation2d::fromDegrees).toArray(Rotation2d[]::new); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/logging/Trace.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/logging/Trace.java new file mode 100644 index 00000000..a545931a --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/logging/Trace.java @@ -0,0 +1,14 @@ +package edu.wpi.team190.gompeilib.core.logging; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a method to be profiled for execution time. The time spent inside any method with this + * annotation will be automatically logged to AdvantageKit. + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Trace {} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/logging/TracerAspect.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/logging/TracerAspect.java new file mode 100644 index 00000000..2365c7b8 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/logging/TracerAspect.java @@ -0,0 +1,46 @@ +package edu.wpi.team190.gompeilib.core.logging; + +import edu.wpi.first.wpilibj.Timer; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.littletonrobotics.junction.Logger; + +/** + * An AspectJ aspect that intercepts calls to methods annotated with @Tracer and logs their + * execution time to AdvantageKit's logger. This is the core of the automatic profiling system. + */ +@Aspect +public class TracerAspect { + + /** + * This is the "advice" that runs "around" any method annotated with @Tracer. + * + * @param joinPoint The point in the code where the advice is being executed. + * @return The return value of the original method. + * @throws Throwable If the original method throws an exception. + */ + @Around("execution(@Trace * *.*(..))") + public Object profile(ProceedingJoinPoint joinPoint) throws Throwable { + // Use FPGA timestamp for high-resolution, synchronized timing + double startTime = Timer.getFPGATimestamp(); + + // Proceed with the original method call + Object result = joinPoint.proceed(); + + double endTime = Timer.getFPGATimestamp(); + double executionTimeMs = (endTime - startTime) * 1000.0; + + // Get method details for logging + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + String className = signature.getDeclaringType().getSimpleName(); + String methodName = signature.getName(); + + // Log the result to AdvantageKit Logger under the "Tracer/" directory + String logKey = "Tracer/" + className + "/" + methodName + "MS"; + Logger.recordOutput(logKey, executionTimeMs); + + return result; + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/robot/RobotContainer.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/robot/RobotContainer.java new file mode 100644 index 00000000..42ff76b5 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/robot/RobotContainer.java @@ -0,0 +1,13 @@ +package edu.wpi.team190.gompeilib.core.robot; + +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; + +public interface RobotContainer { + + public default void robotPeriodic() {} + + public default Command getAutonomousCommand() { + return Commands.none(); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/robot/RobotMode.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/robot/RobotMode.java new file mode 100644 index 00000000..f1935e5c --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/robot/RobotMode.java @@ -0,0 +1,7 @@ +package edu.wpi.team190.gompeilib.core.robot; + +public enum RobotMode { + REAL, + SIM, + REPLAY +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/robot/RobotState.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/robot/RobotState.java new file mode 100644 index 00000000..c0a4287f --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/robot/RobotState.java @@ -0,0 +1,6 @@ +package edu.wpi.team190.gompeilib.core.robot; + +public interface RobotState { + + public default void periodic() {} +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/state/localization/EstimationRegion.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/state/localization/EstimationRegion.java new file mode 100644 index 00000000..86445303 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/state/localization/EstimationRegion.java @@ -0,0 +1,144 @@ +package edu.wpi.team190.gompeilib.core.state.localization; + +import edu.wpi.first.apriltag.AprilTag; +import edu.wpi.first.math.VecBuilder; +import edu.wpi.first.math.estimator.SwerveDrivePoseEstimator; +import edu.wpi.first.math.geometry.*; +import edu.wpi.first.math.kinematics.SwerveDriveKinematics; +import edu.wpi.first.math.kinematics.SwerveModulePosition; +import edu.wpi.team190.gompeilib.core.utility.GeometryUtil; +import edu.wpi.team190.gompeilib.subsystems.vision.data.VisionMultiTxTyObservation; +import edu.wpi.team190.gompeilib.subsystems.vision.data.VisionPoseObservation; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.Getter; + +/** + * Represents an independent localization context that estimates the robot's field-relative pose + * using a constrained subset of vision targets. + * + *

An {@code EstimationRegion} owns its own {@link SwerveDrivePoseEstimator} and is configured + * with a fixed set of AprilTags that are considered valid measurement sources for this region. This + * allows multiple estimators to run in parallel, each optimized for different areas of the field, + * tag groupings, or sensing conditions. + * + *

The estimator continuously integrates drivetrain odometry and accepts vision updates in two + * forms: + * + *

    + *
  • Full field-relative pose observations (e.g. solvePNP-based estimates) + *
  • Angular {@code tx/ty}-based observations that are projected into a 2D field pose using + * known AprilTag locations and the camera transform + *
+ * + * Vision measurements are assumed to originate only from the tags assigned to this region; + * observations referencing unknown tags are ignored. + * + *

This abstraction enables higher-level localization logic to dynamically select or weight + * estimators based on robot position, visibility, confidence, or game-specific constraints, without + * entangling tag-selection logic with the underlying state estimation. + */ +public class EstimationRegion { + @Getter private final Map aprilTags; + private final SwerveDrivePoseEstimator poseEstimator; + + public EstimationRegion(Set aprilTags, SwerveDriveKinematics kinematics) { + this.aprilTags = + aprilTags.stream() + .map(aprilTag -> Map.entry(aprilTag.ID, aprilTag.pose)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + SwerveModulePosition[] swerveModulePositions = new SwerveModulePosition[4]; + for (int i = 0; i < swerveModulePositions.length; i++) { + swerveModulePositions[i] = new SwerveModulePosition(); + } + + this.poseEstimator = + new SwerveDrivePoseEstimator( + kinematics, Rotation2d.kZero, swerveModulePositions, Pose2d.kZero); + } + + public void addOdometryObservation( + double timestamp, Rotation2d heading, SwerveModulePosition[] modulePositions) { + poseEstimator.updateWithTime(timestamp, heading, modulePositions); + } + + public void addPoseObservation(VisionPoseObservation observation) { + poseEstimator.addVisionMeasurement( + observation.pose(), observation.timestamp(), observation.stddevs()); + } + + public void addTxTyObservation(VisionMultiTxTyObservation observation) { + + // Get odometry-based pose at the timestamp + var sample = poseEstimator.sampleAt(observation.timestamp()); + if (sample.isEmpty()) return; + + // Average tx and ty over four corners + double tx = 0.0; + double ty = 0.0; + for (int j = 0; j < 4; j++) { + tx += observation.tx()[j]; + ty += observation.ty()[j]; + } + tx /= 4.0; + ty /= 4.0; + + Pose3d cameraPose = observation.cameraPose(); + + // Project 3D distance onto horizontal plane + Rotation3d camToTagRotation3d = new Rotation3d(0.0, -cameraPose.getRotation().getY() - ty, 0.0); + + Translation3d camToTag = new Translation3d(observation.distance(), camToTagRotation3d); + + double distance2d = camToTag.toTranslation2d().getNorm(); + + // Compute rotation from camera to tag + // tx and ty are flipped from WPILib convention. + Rotation2d camToTagRotation2d = + sample + .get() + .getRotation() + .plus(cameraPose.toPose2d().getRotation().plus(Rotation2d.fromRadians(-tx))); + int tagId = observation.tagId(); + + Pose2d tagPose2d = aprilTags.get(tagId).toPose2d(); + if (tagPose2d == null) return; + + // Compute camera position in field frame + Rotation2d tagToCameraRotation = camToTagRotation2d.plus(Rotation2d.kPi); + + Translation2d fieldToCameraTranslation = + new Pose2d(tagPose2d.getTranslation(), tagToCameraRotation) + .transformBy(GeometryUtil.toTransform2d(distance2d, 0.0)) + .getTranslation(); + + // Compute robot poses + Pose2d robotPose = + new Pose2d( + fieldToCameraTranslation, + sample.get().getRotation().plus(cameraPose.toPose2d().getRotation())) + .transformBy(new Transform2d(cameraPose.toPose2d(), Pose2d.kZero)); + + // Use odometry rotation only + robotPose = new Pose2d(robotPose.getTranslation(), sample.get().getRotation()); + + double xystdev = + LocalizationConstants.XY_STDDEV_COEFFICIENT + * Math.pow(observation.distance(), LocalizationConstants.XY_STDDEV_DISTANCE_EXPONENT); + + poseEstimator.addVisionMeasurement( + robotPose, + observation.timestamp(), + VecBuilder.fill(xystdev, xystdev, Double.POSITIVE_INFINITY)); + } + + public Pose2d getEstimatedPose() { + return poseEstimator.getEstimatedPosition(); + } + + public void resetPose(Pose2d pose) { + poseEstimator.resetPose(pose); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/state/localization/FieldZone.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/state/localization/FieldZone.java new file mode 100644 index 00000000..4c7fbf67 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/state/localization/FieldZone.java @@ -0,0 +1,27 @@ +package edu.wpi.team190.gompeilib.core.state.localization; + +import edu.wpi.first.apriltag.AprilTag; +import java.util.Set; + +/** + * Defines a logical region of the field in terms of the AprilTags that are considered visible, + * reliable, or relevant within that area. + * + *

A {@code FieldZone} contains no estimation or stateful logic on its own. Instead, it serves as + * a configuration object that describes which vision targets should be associated with a given + * localization strategy or {@link EstimationRegion}. + * + *

By separating field structure from estimation behavior, field zones make it possible to: + * + *

    + *
  • Partition the field into overlapping or disjoint regions + *
  • Restrict vision updates to specific tag subsets + *
  • Swap or combine localization strategies based on robot position + *
+ * + *

This abstraction is intentionally lightweight and immutable, making it safe to share across + * subsystems and reuse throughout the localization stack. + * + * @param aprilTags the set of AprilTags associated with this field zone + */ +public record FieldZone(Set aprilTags) {} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/state/localization/Localization.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/state/localization/Localization.java new file mode 100644 index 00000000..74626d8b --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/state/localization/Localization.java @@ -0,0 +1,94 @@ +package edu.wpi.team190.gompeilib.core.state.localization; + +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; +import edu.wpi.team190.gompeilib.core.utility.GeometryUtil; +import edu.wpi.team190.gompeilib.subsystems.vision.data.VisionMultiTxTyObservation; +import edu.wpi.team190.gompeilib.subsystems.vision.data.VisionPoseObservation; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class Localization { + private final List estimationRegions; + + private final SwerveDrivePoseEstimator globalPoseEstimator; + + public Localization( + List estimationZones, + SwerveDriveKinematics kinematics, + double bufferLengthSeconds) { + this.globalPoseEstimator = + new SwerveDrivePoseEstimator( + kinematics, + Rotation2d.kZero, + new SwerveModulePosition[] { + new SwerveModulePosition(), + new SwerveModulePosition(), + new SwerveModulePosition(), + new SwerveModulePosition() + }, + Pose2d.kZero); + + this.estimationRegions = + estimationZones.stream() + .map(zone -> new EstimationRegion(zone.aprilTags(), kinematics)) + .toList(); + } + + public void addOdometryObservation( + double timestamp, Rotation2d rawHeading, SwerveModulePosition[] modulePositions) { + globalPoseEstimator.updateWithTime(timestamp, rawHeading, modulePositions); + estimationRegions.forEach( + region -> region.addOdometryObservation(timestamp, rawHeading, modulePositions)); + } + + public void addPoseObservations(List poseObservations) { + poseObservations.stream() + .filter(observation -> !GeometryUtil.isNaN(observation.pose())) + .forEach( + observation -> + globalPoseEstimator.addVisionMeasurement( + observation.pose(), observation.timestamp(), observation.stddevs())); + estimationRegions.forEach( + zone -> + poseObservations.stream() + .filter( + observation -> zone.getAprilTags().keySet().containsAll(observation.tagIds())) + .filter(observation -> !GeometryUtil.isNaN(observation.pose())) + .forEach(zone::addPoseObservation)); + } + + public void addTxTyObservations(List txTyObservations) { + estimationRegions.forEach( + zone -> + txTyObservations.stream() + .filter(observation -> zone.getAprilTags().containsKey(observation.tagId())) + .forEach(zone::addTxTyObservation)); + } + + public Pose2d getEstimatedPose(FieldZone fieldZone) { + Set zoneTagIDs = + fieldZone.aprilTags().stream().map(t -> t.ID).collect(Collectors.toSet()); + + return estimationRegions.stream() + .filter(region -> zoneTagIDs.equals(region.getAprilTags().keySet())) + .findFirst() + .map(EstimationRegion::getEstimatedPose) + .orElseGet(globalPoseEstimator::getEstimatedPosition); + } + + public Rotation2d getHeading() { + return globalPoseEstimator.getEstimatedPosition().getRotation(); + } + + public void resetPose(Pose2d pose) { + for (EstimationRegion region : estimationRegions) { + region.resetPose(pose); + } + globalPoseEstimator.resetPose(pose); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/state/localization/LocalizationConstants.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/state/localization/LocalizationConstants.java new file mode 100644 index 00000000..76cfa574 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/state/localization/LocalizationConstants.java @@ -0,0 +1,6 @@ +package edu.wpi.team190.gompeilib.core.state.localization; + +public class LocalizationConstants { + public static final double XY_STDDEV_COEFFICIENT = 0.1; + public static final double XY_STDDEV_DISTANCE_EXPONENT = 1.2; +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/GeometryUtil.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/GeometryUtil.java new file mode 100644 index 00000000..22b19075 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/GeometryUtil.java @@ -0,0 +1,228 @@ +package edu.wpi.team190.gompeilib.core.utility; + +import edu.wpi.first.math.geometry.*; + +public class GeometryUtil { + /** + * Creates a pure translating transform + * + * @param translation The translation to create the transform with + * @return The resulting transform + */ + public static Transform2d toTransform2d(Translation2d translation) { + return new Transform2d(translation, new Rotation2d()); + } + + /** + * Creates a pure translating transform + * + * @param x The x coordinate of the translation + * @param y The y coordinate of the translation + * @return The resulting transform + */ + public static Transform2d toTransform2d(double x, double y) { + return new Transform2d(x, y, new Rotation2d()); + } + + /** + * Creates a pure rotating transform + * + * @param rotation The rotation to create the transform with + * @return The resulting transform + */ + public static Transform2d toTransform2d(Rotation2d rotation) { + return new Transform2d(new Translation2d(), rotation); + } + + /** + * Converts a Pose2d to a Transform2d to be used in a kinematic chain + * + * @param pose The pose that will represent the transform + * @return The resulting transform + */ + public static Transform2d toTransform2d(Pose2d pose) { + return new Transform2d(pose.getTranslation(), pose.getRotation()); + } + + /** + * Checks if a pose is (0, 0, 0) + * + * @param pose The pose to check + * @return Whether the pose is zero + */ + public static boolean isZero(Pose2d pose) { + return pose.getX() == 0.0 && pose.getY() == 0.0 && pose.getRotation().getDegrees() == 0.0; + } + + /** + * Checks if any poses are (0, 0, 0) + * + * @param poses The poses to check + * @return Whether any poses are zero + */ + public static boolean isZero(Pose2d[] poses) { + for (Pose2d p : poses) { + if (isZero(p)) { + return true; + } + } + return false; + } + + /** + * Checks if a translation is (0, 0) + * + * @param translation The translation to check + * @return Whether the translation is zero + */ + public static boolean isZero(Translation2d translation) { + return translation.getX() == 0.0 && translation.getY() == 0.0; + } + + /** + * Checks if a rotation is zero + * + * @param rotation The rotation to check + * @return Whether the rotation is zero + */ + public static boolean isZero(Rotation2d rotation) { + return rotation.getDegrees() == 0.0; + } + + /** + * Checks if a pose is NaN + * + * @param pose The pose to check + * @return Whether the pose is NaN + */ + public static boolean isNaN(Pose2d pose) { + return Double.isNaN(pose.getX()) + || Double.isNaN(pose.getY()) + || Double.isNaN(pose.getRotation().getDegrees()); + } + + /** + * Checks if any poses are NaN + * + * @param poses The poses to check + * @return Whether any poses are NaN + */ + public static boolean isNaN(Pose2d[] poses) { + for (Pose2d p : poses) { + if (isNaN(p)) { + return true; + } + } + return false; + } + + /** + * @param rectangle2ds Array of rectangles to check if the pose is in + * @param pose The pose to check is contained by the rectangle + * @return Whether the pose is contained by the rectangle + */ + public static boolean contains(Rectangle2d[] rectangle2ds, Pose2d pose) { + for (Rectangle2d rectangle : rectangle2ds) { + if (rectangle.contains(pose.getTranslation())) { + return true; + } + } + return false; + } + + /** + * @param rectangle2ds Array of rectangles to check against + * @param pose Center of the target rectangle + * @param x Full width of the target rectangle + * @param y Full height of the target rectangle + * @return Whether ANY part of the target rectangle is inside any rectangle + */ + public static boolean intersects(Rectangle2d[] rectangle2ds, Pose2d pose, double x, double y) { + Rectangle2d target = new Rectangle2d(pose, x, y); + + double hx = x / 2.0; + double hy = y / 2.0; + + // Target corners (correctly rotated) + Translation2d[] corners = + new Translation2d[] { + new Translation2d(hx, hy), + new Translation2d(-hx, hy), + new Translation2d(-hx, -hy), + new Translation2d(hx, -hy) + }; + + Translation2d center = pose.getTranslation(); + Rotation2d rot = pose.getRotation(); + + for (Rectangle2d rectangle : rectangle2ds) { + + for (Translation2d corner : corners) { + Translation2d worldCorner = center.plus(corner.rotateBy(rot)); + if (rectangle.contains(worldCorner)) { + return true; + } + } + + if (rectangle.contains(center)) { + return true; + } + + if (target.contains(rectangle.getCenter().getTranslation())) { + return true; + } + } + + return false; + } + + /** + * @param rectangle2ds Array of rectangles to check if the translation is in + * @param translation2d The translation to check is contained by the rectangle + * @return Whether the translation is contained by the rectangle + */ + public static boolean contains(Rectangle2d[] rectangle2ds, Translation2d translation2d) { + for (Rectangle2d rectangle : rectangle2ds) { + if (rectangle.contains(translation2d)) { + return true; + } + } + return false; + } + + /** + * Gets the center and corners of a rectangle2d + * + * @param rectangle2d The rectangle2d to get the center and corners of + * @return The poses from the rectangle2d + */ + public static Pose2d[] rectanglePose2ds(Rectangle2d rectangle2d) { + Pose2d[] poses = new Pose2d[5]; + poses[0] = + rectangle2d + .getCenter() + .transformBy( + new Transform2d( + rectangle2d.getXWidth(), rectangle2d.getYWidth(), new Rotation2d())); + poses[1] = + rectangle2d + .getCenter() + .transformBy( + new Transform2d( + -rectangle2d.getXWidth(), rectangle2d.getYWidth(), new Rotation2d())); + poses[2] = + rectangle2d + .getCenter() + .transformBy( + new Transform2d( + -rectangle2d.getXWidth(), -rectangle2d.getYWidth(), new Rotation2d())); + poses[3] = + rectangle2d + .getCenter() + .transformBy( + new Transform2d( + rectangle2d.getXWidth(), -rectangle2d.getYWidth(), new Rotation2d())); + poses[4] = rectangle2d.getCenter(); + return poses; + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/LimelightHelpers.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/LimelightHelpers.java new file mode 100644 index 00000000..652638a7 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/LimelightHelpers.java @@ -0,0 +1,1987 @@ +// LimelightHelpers v1.14 (REQUIRES LLOS 2026.0 OR LATER) + +package edu.wpi.team190.gompeilib.core.utility; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonFormat.Shape; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.net.PortForwarder; +import edu.wpi.first.networktables.DoubleArrayEntry; +import edu.wpi.first.networktables.NetworkTable; +import edu.wpi.first.networktables.NetworkTableEntry; +import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.networktables.TimestampedDoubleArray; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * LimelightHelpers provides static methods and classes for interfacing with Limelight vision + * cameras in FRC. This library supports all Limelight features including AprilTag tracking, Neural + * Networks, and standard color/retroreflective tracking. + */ +public class LimelightHelpers { + + private static final Map doubleArrayEntries = new ConcurrentHashMap<>(); + + /** Represents a Color/Retroreflective Target Result extracted from JSON Output */ + public static class LimelightTarget_Retro { + + @JsonProperty("t6c_ts") + private double[] cameraPose_TargetSpace; + + @JsonProperty("t6r_fs") + private double[] robotPose_FieldSpace; + + @JsonProperty("t6r_ts") + private double[] robotPose_TargetSpace; + + @JsonProperty("t6t_cs") + private double[] targetPose_CameraSpace; + + @JsonProperty("t6t_rs") + private double[] targetPose_RobotSpace; + + public Pose3d getCameraPose_TargetSpace() { + return toPose3D(cameraPose_TargetSpace); + } + + public Pose3d getRobotPose_FieldSpace() { + return toPose3D(robotPose_FieldSpace); + } + + public Pose3d getRobotPose_TargetSpace() { + return toPose3D(robotPose_TargetSpace); + } + + public Pose3d getTargetPose_CameraSpace() { + return toPose3D(targetPose_CameraSpace); + } + + public Pose3d getTargetPose_RobotSpace() { + return toPose3D(targetPose_RobotSpace); + } + + public Pose2d getCameraPose_TargetSpace2D() { + return toPose2D(cameraPose_TargetSpace); + } + + public Pose2d getRobotPose_FieldSpace2D() { + return toPose2D(robotPose_FieldSpace); + } + + public Pose2d getRobotPose_TargetSpace2D() { + return toPose2D(robotPose_TargetSpace); + } + + public Pose2d getTargetPose_CameraSpace2D() { + return toPose2D(targetPose_CameraSpace); + } + + public Pose2d getTargetPose_RobotSpace2D() { + return toPose2D(targetPose_RobotSpace); + } + + @JsonProperty("ta") + public double ta; + + @JsonProperty("tx") + public double tx; + + @JsonProperty("ty") + public double ty; + + @JsonProperty("txp") + public double tx_pixels; + + @JsonProperty("typ") + public double ty_pixels; + + @JsonProperty("tx_nocross") + public double tx_nocrosshair; + + @JsonProperty("ty_nocross") + public double ty_nocrosshair; + + @JsonProperty("ts") + public double ts; + + public LimelightTarget_Retro() { + cameraPose_TargetSpace = new double[6]; + robotPose_FieldSpace = new double[6]; + robotPose_TargetSpace = new double[6]; + targetPose_CameraSpace = new double[6]; + targetPose_RobotSpace = new double[6]; + } + } + + /** Represents an AprilTag/Fiducial Target Result extracted from JSON Output */ + public static class LimelightTarget_Fiducial { + + @JsonProperty("fID") + public double fiducialID; + + @JsonProperty("fam") + public String fiducialFamily; + + @JsonProperty("t6c_ts") + private double[] cameraPose_TargetSpace; + + @JsonProperty("t6r_fs") + private double[] robotPose_FieldSpace; + + @JsonProperty("t6r_ts") + private double[] robotPose_TargetSpace; + + @JsonProperty("t6t_cs") + private double[] targetPose_CameraSpace; + + @JsonProperty("t6t_rs") + private double[] targetPose_RobotSpace; + + public Pose3d getCameraPose_TargetSpace() { + return toPose3D(cameraPose_TargetSpace); + } + + public Pose3d getRobotPose_FieldSpace() { + return toPose3D(robotPose_FieldSpace); + } + + public Pose3d getRobotPose_TargetSpace() { + return toPose3D(robotPose_TargetSpace); + } + + public Pose3d getTargetPose_CameraSpace() { + return toPose3D(targetPose_CameraSpace); + } + + public Pose3d getTargetPose_RobotSpace() { + return toPose3D(targetPose_RobotSpace); + } + + public Pose2d getCameraPose_TargetSpace2D() { + return toPose2D(cameraPose_TargetSpace); + } + + public Pose2d getRobotPose_FieldSpace2D() { + return toPose2D(robotPose_FieldSpace); + } + + public Pose2d getRobotPose_TargetSpace2D() { + return toPose2D(robotPose_TargetSpace); + } + + public Pose2d getTargetPose_CameraSpace2D() { + return toPose2D(targetPose_CameraSpace); + } + + public Pose2d getTargetPose_RobotSpace2D() { + return toPose2D(targetPose_RobotSpace); + } + + @JsonProperty("ta") + public double ta; + + @JsonProperty("tx") + public double tx; + + @JsonProperty("ty") + public double ty; + + @JsonProperty("txp") + public double tx_pixels; + + @JsonProperty("typ") + public double ty_pixels; + + @JsonProperty("tx_nocross") + public double tx_nocrosshair; + + @JsonProperty("ty_nocross") + public double ty_nocrosshair; + + @JsonProperty("ts") + public double ts; + + public LimelightTarget_Fiducial() { + cameraPose_TargetSpace = new double[6]; + robotPose_FieldSpace = new double[6]; + robotPose_TargetSpace = new double[6]; + targetPose_CameraSpace = new double[6]; + targetPose_RobotSpace = new double[6]; + } + } + + /** Represents a Barcode Target Result extracted from JSON Output */ + public static class LimelightTarget_Barcode { + + /** Barcode family type (e.g. "QR", "DataMatrix", etc.) */ + @JsonProperty("fam") + public String family; + + /** Gets the decoded data content of the barcode */ + @JsonProperty("data") + public String data; + + @JsonProperty("txp") + public double tx_pixels; + + @JsonProperty("typ") + public double ty_pixels; + + @JsonProperty("tx") + public double tx; + + @JsonProperty("ty") + public double ty; + + @JsonProperty("tx_nocross") + public double tx_nocrosshair; + + @JsonProperty("ty_nocross") + public double ty_nocrosshair; + + @JsonProperty("ta") + public double ta; + + @JsonProperty("pts") + public double[][] corners; + + public LimelightTarget_Barcode() {} + + public String getFamily() { + return family; + } + } + + /** Represents a Neural Classifier Pipeline Result extracted from JSON Output */ + public static class LimelightTarget_Classifier { + + @JsonProperty("class") + public String className; + + @JsonProperty("classID") + public double classID; + + @JsonProperty("conf") + public double confidence; + + @JsonProperty("zone") + public double zone; + + @JsonProperty("tx") + public double tx; + + @JsonProperty("txp") + public double tx_pixels; + + @JsonProperty("ty") + public double ty; + + @JsonProperty("typ") + public double ty_pixels; + + public LimelightTarget_Classifier() {} + } + + /** Represents a Neural Detector Pipeline Result extracted from JSON Output */ + public static class LimelightTarget_Detector { + + @JsonProperty("class") + public String className; + + @JsonProperty("classID") + public double classID; + + @JsonProperty("conf") + public double confidence; + + @JsonProperty("ta") + public double ta; + + @JsonProperty("tx") + public double tx; + + @JsonProperty("ty") + public double ty; + + @JsonProperty("txp") + public double tx_pixels; + + @JsonProperty("typ") + public double ty_pixels; + + @JsonProperty("tx_nocross") + public double tx_nocrosshair; + + @JsonProperty("ty_nocross") + public double ty_nocrosshair; + + public LimelightTarget_Detector() {} + } + + /** Represents hardware statistics from the Limelight. */ + public static class HardwareReport { + @JsonProperty("cid") + public String cameraId; + + @JsonProperty("cpu") + public double cpuUsage; + + @JsonProperty("dfree") + public double diskFree; + + @JsonProperty("dtot") + public double diskTotal; + + @JsonProperty("ram") + public double ramUsage; + + @JsonProperty("temp") + public double temperature; + + public HardwareReport() {} + } + + /** Represents IMU data from the JSON results. */ + public static class IMUResults { + @JsonProperty("data") + public double[] data; + + @JsonProperty("quat") + public double[] quaternion; + + @JsonProperty("yaw") + public double yaw; + + // Parsed from data array + public double robotYaw; + public double roll; + public double pitch; + public double rawYaw; + public double gyroZ; + public double gyroX; + public double gyroY; + public double accelZ; + public double accelX; + public double accelY; + + public IMUResults() { + data = new double[0]; + quaternion = new double[4]; + } + + public void parseDataArray() { + if (data != null && data.length >= 10) { + robotYaw = data[0]; + roll = data[1]; + pitch = data[2]; + rawYaw = data[3]; + gyroZ = data[4]; + gyroX = data[5]; + gyroY = data[6]; + accelZ = data[7]; + accelX = data[8]; + accelY = data[9]; + } + } + } + + /** Represents capture rewind buffer statistics. */ + public static class RewindStats { + @JsonProperty("bufferUsage") + public double bufferUsage; + + @JsonProperty("enabled") + public int enabled; + + @JsonProperty("flushing") + public int flushing; + + @JsonProperty("frameCount") + public int frameCount; + + @JsonProperty("latpen") + public int latencyPenalty; + + @JsonProperty("storedSeconds") + public double storedSeconds; + + public RewindStats() {} + } + + /** Limelight Results object, parsed from a Limelight's JSON results output. */ + public static class LimelightResults { + + public String error; + + @JsonProperty("pID") + public double pipelineID; + + @JsonProperty("tl") + public double latency_pipeline; + + @JsonProperty("cl") + public double latency_capture; + + public double latency_jsonParse; + + @JsonProperty("ts") + public double timestamp_LIMELIGHT_publish; + + @JsonProperty("ts_rio") + public double timestamp_RIOFPGA_capture; + + @JsonProperty("ts_nt") + public long timestamp_nt; + + @JsonProperty("ts_sys") + public long timestamp_sys; + + @JsonProperty("ts_us") + public long timestamp_us; + + @JsonProperty("v") + @JsonFormat(shape = Shape.NUMBER) + public boolean valid; + + @JsonProperty("pTYPE") + public String pipelineType; + + @JsonProperty("tx") + public double tx; + + @JsonProperty("ty") + public double ty; + + @JsonProperty("txnc") + public double tx_nocrosshair; + + @JsonProperty("tync") + public double ty_nocrosshair; + + @JsonProperty("ta") + public double ta; + + @JsonProperty("botpose") + public double[] botpose; + + @JsonProperty("botpose_wpired") + public double[] botpose_wpired; + + @JsonProperty("botpose_wpiblue") + public double[] botpose_wpiblue; + + @JsonProperty("botpose_tagcount") + public double botpose_tagcount; + + @JsonProperty("botpose_span") + public double botpose_span; + + @JsonProperty("botpose_avgdist") + public double botpose_avgdist; + + @JsonProperty("botpose_avgarea") + public double botpose_avgarea; + + @JsonProperty("botpose_orb") + public double[] botpose_orb; + + @JsonProperty("botpose_orb_wpiblue") + public double[] botpose_orb_wpiblue; + + @JsonProperty("botpose_orb_wpired") + public double[] botpose_orb_wpired; + + @JsonProperty("t6c_rs") + public double[] camerapose_robotspace; + + @JsonProperty("hw") + public HardwareReport hardware; + + @JsonProperty("imu") + public IMUResults imuResults; + + @JsonProperty("rewind") + public RewindStats rewindStats; + + @JsonProperty("PythonOut") + public double[] pythonOutput; + + public Pose3d getBotPose3d() { + return toPose3D(botpose); + } + + public Pose3d getBotPose3d_wpiRed() { + return toPose3D(botpose_wpired); + } + + public Pose3d getBotPose3d_wpiBlue() { + return toPose3D(botpose_wpiblue); + } + + public Pose2d getBotPose2d() { + return toPose2D(botpose); + } + + public Pose2d getBotPose2d_wpiRed() { + return toPose2D(botpose_wpired); + } + + public Pose2d getBotPose2d_wpiBlue() { + return toPose2D(botpose_wpiblue); + } + + @JsonProperty("Retro") + public LimelightTarget_Retro[] targets_Retro; + + @JsonProperty("Fiducial") + public LimelightTarget_Fiducial[] targets_Fiducials; + + @JsonProperty("Classifier") + public LimelightTarget_Classifier[] targets_Classifier; + + @JsonProperty("Detector") + public LimelightTarget_Detector[] targets_Detector; + + @JsonProperty("Barcode") + public LimelightTarget_Barcode[] targets_Barcode; + + public LimelightResults() { + botpose = new double[6]; + botpose_wpired = new double[6]; + botpose_wpiblue = new double[6]; + botpose_orb = new double[6]; + botpose_orb_wpiblue = new double[6]; + botpose_orb_wpired = new double[6]; + camerapose_robotspace = new double[6]; + targets_Retro = new LimelightTarget_Retro[0]; + targets_Fiducials = new LimelightTarget_Fiducial[0]; + targets_Classifier = new LimelightTarget_Classifier[0]; + targets_Detector = new LimelightTarget_Detector[0]; + targets_Barcode = new LimelightTarget_Barcode[0]; + pythonOutput = new double[0]; + pipelineType = ""; + } + } + + /** Represents a Limelight Raw Fiducial result from Limelight's NetworkTables output. */ + public static class RawFiducial { + public int id = 0; + public double txnc = 0; + public double tync = 0; + public double ta = 0; + public double distToCamera = 0; + public double distToRobot = 0; + public double ambiguity = 0; + + public RawFiducial( + int id, + double txnc, + double tync, + double ta, + double distToCamera, + double distToRobot, + double ambiguity) { + this.id = id; + this.txnc = txnc; + this.tync = tync; + this.ta = ta; + this.distToCamera = distToCamera; + this.distToRobot = distToRobot; + this.ambiguity = ambiguity; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + RawFiducial other = (RawFiducial) obj; + return id == other.id + && Double.compare(txnc, other.txnc) == 0 + && Double.compare(tync, other.tync) == 0 + && Double.compare(ta, other.ta) == 0 + && Double.compare(distToCamera, other.distToCamera) == 0 + && Double.compare(distToRobot, other.distToRobot) == 0 + && Double.compare(ambiguity, other.ambiguity) == 0; + } + } + + /** Represents a Limelight Raw Target/Contour result from Limelight's NetworkTables output. */ + public static class RawTarget { + public double txnc = 0; + public double tync = 0; + public double ta = 0; + + public RawTarget(double txnc, double tync, double ta) { + this.txnc = txnc; + this.tync = tync; + this.ta = ta; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + RawTarget other = (RawTarget) obj; + return Double.compare(txnc, other.txnc) == 0 + && Double.compare(tync, other.tync) == 0 + && Double.compare(ta, other.ta) == 0; + } + } + + /** Represents a Limelight Raw Neural Detector result from Limelight's NetworkTables output. */ + public static class RawDetection { + public int classId = 0; + public double txnc = 0; + public double tync = 0; + public double ta = 0; + public double corner0_X = 0; + public double corner0_Y = 0; + public double corner1_X = 0; + public double corner1_Y = 0; + public double corner2_X = 0; + public double corner2_Y = 0; + public double corner3_X = 0; + public double corner3_Y = 0; + + public RawDetection( + int classId, + double txnc, + double tync, + double ta, + double corner0_X, + double corner0_Y, + double corner1_X, + double corner1_Y, + double corner2_X, + double corner2_Y, + double corner3_X, + double corner3_Y) { + this.classId = classId; + this.txnc = txnc; + this.tync = tync; + this.ta = ta; + this.corner0_X = corner0_X; + this.corner0_Y = corner0_Y; + this.corner1_X = corner1_X; + this.corner1_Y = corner1_Y; + this.corner2_X = corner2_X; + this.corner2_Y = corner2_Y; + this.corner3_X = corner3_X; + this.corner3_Y = corner3_Y; + } + } + + /** Represents a 3D Pose Estimate. */ + public static class PoseEstimate { + public Pose2d pose; + public double timestampSeconds; + public double latency; + public int tagCount; + public double tagSpan; + public double avgTagDist; + public double avgTagArea; + + public RawFiducial[] rawFiducials; + public boolean isMegaTag2; + + /** Instantiates a PoseEstimate object with default values */ + public PoseEstimate() { + this.pose = new Pose2d(); + this.timestampSeconds = 0; + this.latency = 0; + this.tagCount = 0; + this.tagSpan = 0; + this.avgTagDist = 0; + this.avgTagArea = 0; + this.rawFiducials = new RawFiducial[] {}; + this.isMegaTag2 = false; + } + + public PoseEstimate( + Pose2d pose, + double timestampSeconds, + double latency, + int tagCount, + double tagSpan, + double avgTagDist, + double avgTagArea, + RawFiducial[] rawFiducials, + boolean isMegaTag2) { + + this.pose = pose; + this.timestampSeconds = timestampSeconds; + this.latency = latency; + this.tagCount = tagCount; + this.tagSpan = tagSpan; + this.avgTagDist = avgTagDist; + this.avgTagArea = avgTagArea; + this.rawFiducials = rawFiducials; + this.isMegaTag2 = isMegaTag2; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + PoseEstimate that = (PoseEstimate) obj; + // We don't compare the timestampSeconds as it isn't relevant for equality and makes + // unit testing harder + return Double.compare(that.latency, latency) == 0 + && tagCount == that.tagCount + && Double.compare(that.tagSpan, tagSpan) == 0 + && Double.compare(that.avgTagDist, avgTagDist) == 0 + && Double.compare(that.avgTagArea, avgTagArea) == 0 + && pose.equals(that.pose) + && Arrays.equals(rawFiducials, that.rawFiducials); + } + } + + /** Encapsulates the state of an internal Limelight IMU. */ + public static class IMUData { + public double robotYaw = 0.0; + public double Roll = 0.0; + public double Pitch = 0.0; + public double Yaw = 0.0; + public double gyroX = 0.0; + public double gyroY = 0.0; + public double gyroZ = 0.0; + public double accelX = 0.0; + public double accelY = 0.0; + public double accelZ = 0.0; + + public IMUData() {} + + public IMUData(double[] imuData) { + if (imuData != null && imuData.length >= 10) { + this.robotYaw = imuData[0]; + this.Roll = imuData[1]; + this.Pitch = imuData[2]; + this.Yaw = imuData[3]; + this.gyroX = imuData[4]; + this.gyroY = imuData[5]; + this.gyroZ = imuData[6]; + this.accelX = imuData[7]; + this.accelY = imuData[8]; + this.accelZ = imuData[9]; + } + } + } + + private static ObjectMapper mapper; + + /** Print JSON Parse time to the console in milliseconds */ + static boolean profileJSON = false; + + static final String sanitizeName(String name) { + if ("".equals(name) || name == null) { + return "limelight"; + } + return name; + } + + /** + * Takes a 6-length array of pose data and converts it to a Pose3d object. Array format: [x, y, z, + * roll, pitch, yaw] where angles are in degrees. + * + * @param inData Array containing pose data [x, y, z, roll, pitch, yaw] + * @return Pose3d object representing the pose, or empty Pose3d if invalid data + */ + public static Pose3d toPose3D(double[] inData) { + if (inData.length < 6) { + // System.err.println("Bad LL 3D Pose Data!"); + return new Pose3d(); + } + return new Pose3d( + new Translation3d(inData[0], inData[1], inData[2]), + new Rotation3d( + Units.degreesToRadians(inData[3]), + Units.degreesToRadians(inData[4]), + Units.degreesToRadians(inData[5]))); + } + + /** + * Takes a 6-length array of pose data and converts it to a Pose2d object. Uses only x, y, and yaw + * components, ignoring z, roll, and pitch. Array format: [x, y, z, roll, pitch, yaw] where angles + * are in degrees. + * + * @param inData Array containing pose data [x, y, z, roll, pitch, yaw] + * @return Pose2d object representing the pose, or empty Pose2d if invalid data + */ + public static Pose2d toPose2D(double[] inData) { + if (inData.length < 6) { + // System.err.println("Bad LL 2D Pose Data!"); + return new Pose2d(); + } + Translation2d tran2d = new Translation2d(inData[0], inData[1]); + Rotation2d r2d = new Rotation2d(Units.degreesToRadians(inData[5])); + return new Pose2d(tran2d, r2d); + } + + /** + * Converts a Pose3d object to an array of doubles in the format [x, y, z, roll, pitch, yaw]. + * Translation components are in meters, rotation components are in degrees. + * + * @param pose The Pose3d object to convert + * @return A 6-element array containing [x, y, z, roll, pitch, yaw] + */ + public static double[] pose3dToArray(Pose3d pose) { + double[] result = new double[6]; + result[0] = pose.getTranslation().getX(); + result[1] = pose.getTranslation().getY(); + result[2] = pose.getTranslation().getZ(); + result[3] = Units.radiansToDegrees(pose.getRotation().getX()); + result[4] = Units.radiansToDegrees(pose.getRotation().getY()); + result[5] = Units.radiansToDegrees(pose.getRotation().getZ()); + return result; + } + + /** + * Converts a Pose2d object to an array of doubles in the format [x, y, z, roll, pitch, yaw]. + * Translation components are in meters, rotation components are in degrees. Note: z, roll, and + * pitch will be 0 since Pose2d only contains x, y, and yaw. + * + * @param pose The Pose2d object to convert + * @return A 6-element array containing [x, y, 0, 0, 0, yaw] + */ + public static double[] pose2dToArray(Pose2d pose) { + double[] result = new double[6]; + result[0] = pose.getTranslation().getX(); + result[1] = pose.getTranslation().getY(); + result[2] = 0; + result[3] = Units.radiansToDegrees(0); + result[4] = Units.radiansToDegrees(0); + result[5] = Units.radiansToDegrees(pose.getRotation().getRadians()); + return result; + } + + private static double extractArrayEntry(double[] inData, int position) { + if (inData.length < position + 1) { + return 0; + } + return inData[position]; + } + + private static PoseEstimate getBotPoseEstimate( + String limelightName, String entryName, boolean isMegaTag2) { + DoubleArrayEntry poseEntry = + LimelightHelpers.getLimelightDoubleArrayEntry(limelightName, entryName); + + TimestampedDoubleArray tsValue = poseEntry.getAtomic(); + double[] poseArray = tsValue.value; + long timestamp = tsValue.timestamp; + + if (poseArray.length == 0) { + // Handle the case where no data is available + return new PoseEstimate(); + } + + var pose = toPose2D(poseArray); + double latency = extractArrayEntry(poseArray, 6); + int tagCount = (int) extractArrayEntry(poseArray, 7); + double tagSpan = extractArrayEntry(poseArray, 8); + double tagDist = extractArrayEntry(poseArray, 9); + double tagArea = extractArrayEntry(poseArray, 10); + + // Convert server timestamp from microseconds to seconds and adjust for latency + double adjustedTimestamp = (timestamp / 1000000.0) - (latency / 1000.0); + + int valsPerFiducial = 7; + int expectedTotalVals = 11 + valsPerFiducial * tagCount; + RawFiducial[] rawFiducials; + + if (poseArray.length != expectedTotalVals) { + // Array size mismatch - return empty array instead of null-filled array + rawFiducials = new RawFiducial[0]; + } else { + rawFiducials = new RawFiducial[tagCount]; + for (int i = 0; i < tagCount; i++) { + int baseIndex = 11 + (i * valsPerFiducial); + int id = (int) poseArray[baseIndex]; + double txnc = poseArray[baseIndex + 1]; + double tync = poseArray[baseIndex + 2]; + double ta = poseArray[baseIndex + 3]; + double distToCamera = poseArray[baseIndex + 4]; + double distToRobot = poseArray[baseIndex + 5]; + double ambiguity = poseArray[baseIndex + 6]; + rawFiducials[i] = new RawFiducial(id, txnc, tync, ta, distToCamera, distToRobot, ambiguity); + } + } + + return new PoseEstimate( + pose, + adjustedTimestamp, + latency, + tagCount, + tagSpan, + tagDist, + tagArea, + rawFiducials, + isMegaTag2); + } + + /** + * Gets the latest raw fiducial/AprilTag detection results from NetworkTables. + * + * @param limelightName Name/identifier of the Limelight + * @return Array of RawFiducial objects containing detection details + */ + public static RawFiducial[] getRawFiducials(String limelightName) { + var entry = LimelightHelpers.getLimelightNTTableEntry(limelightName, "rawfiducials"); + var rawFiducialArray = entry.getDoubleArray(new double[0]); + int valsPerEntry = 7; + if (rawFiducialArray.length % valsPerEntry != 0) { + return new RawFiducial[0]; + } + + int numFiducials = rawFiducialArray.length / valsPerEntry; + RawFiducial[] rawFiducials = new RawFiducial[numFiducials]; + + for (int i = 0; i < numFiducials; i++) { + int baseIndex = i * valsPerEntry; + int id = (int) extractArrayEntry(rawFiducialArray, baseIndex); + double txnc = extractArrayEntry(rawFiducialArray, baseIndex + 1); + double tync = extractArrayEntry(rawFiducialArray, baseIndex + 2); + double ta = extractArrayEntry(rawFiducialArray, baseIndex + 3); + double distToCamera = extractArrayEntry(rawFiducialArray, baseIndex + 4); + double distToRobot = extractArrayEntry(rawFiducialArray, baseIndex + 5); + double ambiguity = extractArrayEntry(rawFiducialArray, baseIndex + 6); + + rawFiducials[i] = new RawFiducial(id, txnc, tync, ta, distToCamera, distToRobot, ambiguity); + } + + return rawFiducials; + } + + /** + * Gets the latest raw neural detector results from NetworkTables + * + * @param limelightName Name/identifier of the Limelight + * @return Array of RawDetection objects containing detection details + */ + public static RawDetection[] getRawDetections(String limelightName) { + var entry = LimelightHelpers.getLimelightNTTableEntry(limelightName, "rawdetections"); + var rawDetectionArray = entry.getDoubleArray(new double[0]); + int valsPerEntry = 12; + if (rawDetectionArray.length % valsPerEntry != 0) { + return new RawDetection[0]; + } + + int numDetections = rawDetectionArray.length / valsPerEntry; + RawDetection[] rawDetections = new RawDetection[numDetections]; + + for (int i = 0; i < numDetections; i++) { + int baseIndex = i * valsPerEntry; // Starting index for this detection's data + int classId = (int) extractArrayEntry(rawDetectionArray, baseIndex); + double txnc = extractArrayEntry(rawDetectionArray, baseIndex + 1); + double tync = extractArrayEntry(rawDetectionArray, baseIndex + 2); + double ta = extractArrayEntry(rawDetectionArray, baseIndex + 3); + double corner0_X = extractArrayEntry(rawDetectionArray, baseIndex + 4); + double corner0_Y = extractArrayEntry(rawDetectionArray, baseIndex + 5); + double corner1_X = extractArrayEntry(rawDetectionArray, baseIndex + 6); + double corner1_Y = extractArrayEntry(rawDetectionArray, baseIndex + 7); + double corner2_X = extractArrayEntry(rawDetectionArray, baseIndex + 8); + double corner2_Y = extractArrayEntry(rawDetectionArray, baseIndex + 9); + double corner3_X = extractArrayEntry(rawDetectionArray, baseIndex + 10); + double corner3_Y = extractArrayEntry(rawDetectionArray, baseIndex + 11); + + rawDetections[i] = + new RawDetection( + classId, txnc, tync, ta, corner0_X, corner0_Y, corner1_X, corner1_Y, corner2_X, + corner2_Y, corner3_X, corner3_Y); + } + + return rawDetections; + } + + /** + * Gets the raw target contours from NetworkTables. Returns ungrouped contours in normalized + * screen space (-1 to 1). + * + * @param limelightName Name/identifier of the Limelight + * @return Array of RawTarget objects containing up to 3 contours + */ + public static RawTarget[] getRawTargets(String limelightName) { + var entry = LimelightHelpers.getLimelightNTTableEntry(limelightName, "rawtargets"); + var rawTargetArray = entry.getDoubleArray(new double[0]); + int valsPerEntry = 3; + if (rawTargetArray.length % valsPerEntry != 0) { + return new RawTarget[0]; + } + + int numTargets = rawTargetArray.length / valsPerEntry; + RawTarget[] rawTargets = new RawTarget[numTargets]; + + for (int i = 0; i < numTargets; i++) { + int baseIndex = i * valsPerEntry; + double txnc = extractArrayEntry(rawTargetArray, baseIndex); + double tync = extractArrayEntry(rawTargetArray, baseIndex + 1); + double ta = extractArrayEntry(rawTargetArray, baseIndex + 2); + + rawTargets[i] = new RawTarget(txnc, tync, ta); + } + + return rawTargets; + } + + /** + * Gets the corner coordinates of detected targets from NetworkTables. Requires "send contours" to + * be enabled in the Limelight Output tab. + * + * @param limelightName Name/identifier of the Limelight + * @return Array of doubles containing corner coordinates [x0, y0, x1, y1, ...] + */ + public static double[] getCornerCoordinates(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "tcornxy"); + } + + /** + * Prints detailed information about a PoseEstimate to standard output. Includes timestamp, + * latency, tag count, tag span, average tag distance, average tag area, and detailed information + * about each detected fiducial. + * + * @param pose The PoseEstimate object to print. If null, prints "No PoseEstimate available." + */ + public static void printPoseEstimate(PoseEstimate pose) { + if (pose == null) { + System.out.println("No PoseEstimate available."); + return; + } + + System.out.printf("Pose Estimate Information:%n"); + System.out.printf("Timestamp (Seconds): %.3f%n", pose.timestampSeconds); + System.out.printf("Latency: %.3f ms%n", pose.latency); + System.out.printf("Tag Count: %d%n", pose.tagCount); + System.out.printf("Tag Span: %.2f meters%n", pose.tagSpan); + System.out.printf("Average Tag Distance: %.2f meters%n", pose.avgTagDist); + System.out.printf("Average Tag Area: %.2f%% of image%n", pose.avgTagArea); + System.out.printf("Is MegaTag2: %b%n", pose.isMegaTag2); + System.out.println(); + + if (pose.rawFiducials == null || pose.rawFiducials.length == 0) { + System.out.println("No RawFiducials data available."); + return; + } + + System.out.println("Raw Fiducials Details:"); + for (int i = 0; i < pose.rawFiducials.length; i++) { + RawFiducial fiducial = pose.rawFiducials[i]; + System.out.printf(" Fiducial #%d:%n", i + 1); + System.out.printf(" ID: %d%n", fiducial.id); + System.out.printf(" TXNC: %.2f%n", fiducial.txnc); + System.out.printf(" TYNC: %.2f%n", fiducial.tync); + System.out.printf(" TA: %.2f%n", fiducial.ta); + System.out.printf(" Distance to Camera: %.2f meters%n", fiducial.distToCamera); + System.out.printf(" Distance to Robot: %.2f meters%n", fiducial.distToRobot); + System.out.printf(" Ambiguity: %.2f%n", fiducial.ambiguity); + System.out.println(); + } + } + + public static Boolean validPoseEstimate(PoseEstimate pose) { + return pose != null && pose.rawFiducials != null && pose.rawFiducials.length != 0; + } + + public static NetworkTable getLimelightNTTable(String tableName) { + return NetworkTableInstance.getDefault().getTable(sanitizeName(tableName)); + } + + public static void Flush() { + NetworkTableInstance.getDefault().flush(); + } + + public static NetworkTableEntry getLimelightNTTableEntry(String tableName, String entryName) { + return getLimelightNTTable(tableName).getEntry(entryName); + } + + public static DoubleArrayEntry getLimelightDoubleArrayEntry(String tableName, String entryName) { + String key = tableName + "/" + entryName; + return doubleArrayEntries.computeIfAbsent( + key, + k -> { + NetworkTable table = getLimelightNTTable(tableName); + return table.getDoubleArrayTopic(entryName).getEntry(new double[0]); + }); + } + + public static double getLimelightNTDouble(String tableName, String entryName) { + return getLimelightNTTableEntry(tableName, entryName).getDouble(0.0); + } + + public static void setLimelightNTDouble(String tableName, String entryName, double val) { + getLimelightNTTableEntry(tableName, entryName).setDouble(val); + } + + public static void setLimelightNTDoubleArray(String tableName, String entryName, double[] val) { + getLimelightNTTableEntry(tableName, entryName).setDoubleArray(val); + } + + public static double[] getLimelightNTDoubleArray(String tableName, String entryName) { + return getLimelightNTTableEntry(tableName, entryName).getDoubleArray(new double[0]); + } + + public static String getLimelightNTString(String tableName, String entryName) { + return getLimelightNTTableEntry(tableName, entryName).getString(""); + } + + public static String[] getLimelightNTStringArray(String tableName, String entryName) { + return getLimelightNTTableEntry(tableName, entryName).getStringArray(new String[0]); + } + + ///// + + /** + * Does the Limelight have a valid target? + * + * @param limelightName Name of the Limelight camera ("" for default) + * @return True if a valid target is present, false otherwise + */ + public static boolean getTV(String limelightName) { + return 1.0 == getLimelightNTDouble(limelightName, "tv"); + } + + /** + * Gets the horizontal offset from the crosshair to the target in degrees. + * + * @param limelightName Name of the Limelight camera ("" for default) + * @return Horizontal offset angle in degrees + */ + public static double getTX(String limelightName) { + return getLimelightNTDouble(limelightName, "tx"); + } + + /** + * Gets the vertical offset from the crosshair to the target in degrees. + * + * @param limelightName Name of the Limelight camera ("" for default) + * @return Vertical offset angle in degrees + */ + public static double getTY(String limelightName) { + return getLimelightNTDouble(limelightName, "ty"); + } + + /** + * Gets the horizontal offset from the principal pixel/point to the target in degrees. This is the + * most accurate 2d metric if you are using a calibrated camera and you don't need adjustable + * crosshair functionality. + * + * @param limelightName Name of the Limelight camera ("" for default) + * @return Horizontal offset angle in degrees + */ + public static double getTXNC(String limelightName) { + return getLimelightNTDouble(limelightName, "txnc"); + } + + /** + * Gets the vertical offset from the principal pixel/point to the target in degrees. This is the + * most accurate 2d metric if you are using a calibrated camera and you don't need adjustable + * crosshair functionality. + * + * @param limelightName Name of the Limelight camera ("" for default) + * @return Vertical offset angle in degrees + */ + public static double getTYNC(String limelightName) { + return getLimelightNTDouble(limelightName, "tync"); + } + + /** + * Gets the target area as a percentage of the image (0-100%). + * + * @param limelightName Name of the Limelight camera ("" for default) + * @return Target area percentage (0-100) + */ + public static double getTA(String limelightName) { + return getLimelightNTDouble(limelightName, "ta"); + } + + /** + * T2D is an array that contains several targeting metrcis + * + * @param limelightName Name of the Limelight camera + * @return Array containing [targetValid, targetCount, targetLatency, captureLatency, tx, ty, + * txnc, tync, ta, tid, targetClassIndexDetector, targetClassIndexClassifier, + * targetLongSidePixels, targetShortSidePixels, targetHorizontalExtentPixels, + * targetVerticalExtentPixels, targetSkewDegrees] + */ + public static double[] getT2DArray(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "t2d"); + } + + /** + * Gets the number of targets currently detected. + * + * @param limelightName Name of the Limelight camera + * @return Number of detected targets + */ + public static int getTargetCount(String limelightName) { + double[] t2d = getT2DArray(limelightName); + if (t2d.length == 17) { + return (int) t2d[1]; + } + return 0; + } + + /** + * Gets the classifier class index from the currently running neural classifier pipeline + * + * @param limelightName Name of the Limelight camera + * @return Class index from classifier pipeline + */ + public static int getClassifierClassIndex(String limelightName) { + double[] t2d = getT2DArray(limelightName); + if (t2d.length == 17) { + return (int) t2d[11]; + } + return 0; + } + + /** + * Gets the detector class index from the primary result of the currently running neural detector + * pipeline. + * + * @param limelightName Name of the Limelight camera + * @return Class index from detector pipeline + */ + public static int getDetectorClassIndex(String limelightName) { + double[] t2d = getT2DArray(limelightName); + if (t2d.length == 17) { + return (int) t2d[10]; + } + return 0; + } + + /** + * Gets the current neural classifier result class name. + * + * @param limelightName Name of the Limelight camera + * @return Class name string from classifier pipeline + */ + public static String getClassifierClass(String limelightName) { + return getLimelightNTString(limelightName, "tcclass"); + } + + /** + * Gets the primary neural detector result class name. + * + * @param limelightName Name of the Limelight camera + * @return Class name string from detector pipeline + */ + public static String getDetectorClass(String limelightName) { + return getLimelightNTString(limelightName, "tdclass"); + } + + /** + * Gets the pipeline's processing latency contribution. + * + * @param limelightName Name of the Limelight camera + * @return Pipeline latency in milliseconds + */ + public static double getLatency_Pipeline(String limelightName) { + return getLimelightNTDouble(limelightName, "tl"); + } + + /** + * Gets the capture latency. + * + * @param limelightName Name of the Limelight camera + * @return Capture latency in milliseconds + */ + public static double getLatency_Capture(String limelightName) { + return getLimelightNTDouble(limelightName, "cl"); + } + + /** + * Gets the active pipeline index. + * + * @param limelightName Name of the Limelight camera + * @return Current pipeline index (0-9) + */ + public static double getCurrentPipelineIndex(String limelightName) { + return getLimelightNTDouble(limelightName, "getpipe"); + } + + /** + * Gets the current pipeline type. + * + * @param limelightName Name of the Limelight camera + * @return Pipeline type string (e.g. "retro", "apriltag", etc) + */ + public static String getCurrentPipelineType(String limelightName) { + return getLimelightNTString(limelightName, "getpipetype"); + } + + /** + * Gets the full JSON results dump. + * + * @param limelightName Name of the Limelight camera + * @return JSON string containing all current results + */ + public static String getJSONDump(String limelightName) { + return getLimelightNTString(limelightName, "json"); + } + + /** + * Switch to getBotPose + * + * @param limelightName + * @return + */ + @Deprecated + public static double[] getBotpose(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "botpose"); + } + + /** + * Switch to getBotPose_wpiRed + * + * @param limelightName + * @return + */ + @Deprecated + public static double[] getBotpose_wpiRed(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "botpose_wpired"); + } + + /** + * Switch to getBotPose_wpiBlue + * + * @param limelightName + * @return + */ + @Deprecated + public static double[] getBotpose_wpiBlue(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "botpose_wpiblue"); + } + + public static double[] getBotPose(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "botpose"); + } + + public static double[] getBotPose_wpiRed(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "botpose_wpired"); + } + + public static double[] getBotPose_wpiBlue(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "botpose_wpiblue"); + } + + public static double[] getBotPose_TargetSpace(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "botpose_targetspace"); + } + + public static double[] getCameraPose_TargetSpace(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "camerapose_targetspace"); + } + + public static double[] getTargetPose_CameraSpace(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "targetpose_cameraspace"); + } + + public static double[] getTargetPose_RobotSpace(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "targetpose_robotspace"); + } + + /** + * Gets the average color under the crosshair region as a 3-element array. + * + * @param limelightName Name of the Limelight camera + * @return Array containing [Blue, Green, Red] color values (BGR order) + */ + public static double[] getTargetColor(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "tc"); + } + + public static double getFiducialID(String limelightName) { + return getLimelightNTDouble(limelightName, "tid"); + } + + /** + * Gets the Limelight heartbeat value. Increments once per frame, allowing you to detect if the + * Limelight is connected and alive. + * + * @param limelightName Name of the Limelight camera + * @return Heartbeat value that increments each frame + */ + public static double getHeartbeat(String limelightName) { + return getLimelightNTDouble(limelightName, "hb"); + } + + public static String getNeuralClassID(String limelightName) { + return getLimelightNTString(limelightName, "tclass"); + } + + public static String[] getRawBarcodeData(String limelightName) { + return getLimelightNTStringArray(limelightName, "rawbarcodes"); + } + + ///// + ///// + + public static Pose3d getBotPose3d(String limelightName) { + double[] poseArray = getLimelightNTDoubleArray(limelightName, "botpose"); + return toPose3D(poseArray); + } + + /** + * (Not Recommended) Gets the robot's 3D pose in the WPILib Red Alliance Coordinate System. + * + * @param limelightName Name/identifier of the Limelight + * @return Pose3d object representing the robot's position and orientation in Red Alliance field + * space + */ + public static Pose3d getBotPose3d_wpiRed(String limelightName) { + double[] poseArray = getLimelightNTDoubleArray(limelightName, "botpose_wpired"); + return toPose3D(poseArray); + } + + /** + * (Recommended) Gets the robot's 3D pose in the WPILib Blue Alliance Coordinate System. + * + * @param limelightName Name/identifier of the Limelight + * @return Pose3d object representing the robot's position and orientation in Blue Alliance field + * space + */ + public static Pose3d getBotPose3d_wpiBlue(String limelightName) { + double[] poseArray = getLimelightNTDoubleArray(limelightName, "botpose_wpiblue"); + return toPose3D(poseArray); + } + + /** + * Gets the robot's 3D pose with respect to the currently tracked target's coordinate system. + * + * @param limelightName Name/identifier of the Limelight + * @return Pose3d object representing the robot's position and orientation relative to the target + */ + public static Pose3d getBotPose3d_TargetSpace(String limelightName) { + double[] poseArray = getLimelightNTDoubleArray(limelightName, "botpose_targetspace"); + return toPose3D(poseArray); + } + + /** + * Gets the camera's 3D pose with respect to the currently tracked target's coordinate system. + * + * @param limelightName Name/identifier of the Limelight + * @return Pose3d object representing the camera's position and orientation relative to the target + */ + public static Pose3d getCameraPose3d_TargetSpace(String limelightName) { + double[] poseArray = getLimelightNTDoubleArray(limelightName, "camerapose_targetspace"); + return toPose3D(poseArray); + } + + /** + * Gets the target's 3D pose with respect to the camera's coordinate system. + * + * @param limelightName Name/identifier of the Limelight + * @return Pose3d object representing the target's position and orientation relative to the camera + */ + public static Pose3d getTargetPose3d_CameraSpace(String limelightName) { + double[] poseArray = getLimelightNTDoubleArray(limelightName, "targetpose_cameraspace"); + return toPose3D(poseArray); + } + + /** + * Gets the target's 3D pose with respect to the robot's coordinate system. + * + * @param limelightName Name/identifier of the Limelight + * @return Pose3d object representing the target's position and orientation relative to the robot + */ + public static Pose3d getTargetPose3d_RobotSpace(String limelightName) { + double[] poseArray = getLimelightNTDoubleArray(limelightName, "targetpose_robotspace"); + return toPose3D(poseArray); + } + + /** + * Gets the camera's 3D pose with respect to the robot's coordinate system. + * + * @param limelightName Name/identifier of the Limelight + * @return Pose3d object representing the camera's position and orientation relative to the robot + */ + public static Pose3d getCameraPose3d_RobotSpace(String limelightName) { + double[] poseArray = getLimelightNTDoubleArray(limelightName, "camerapose_robotspace"); + return toPose3D(poseArray); + } + + /** + * Gets the Pose2d for easy use with Odometry vision pose estimator (addVisionMeasurement) + * + * @param limelightName + * @return + */ + public static Pose2d getBotPose2d_wpiBlue(String limelightName) { + + double[] result = getBotPose_wpiBlue(limelightName); + return toPose2D(result); + } + + /** + * Gets the MegaTag1 Pose2d and timestamp for use with WPILib pose estimator + * (addVisionMeasurement) in the WPILib Blue alliance coordinate system. + * + * @param limelightName + * @return + */ + public static PoseEstimate getBotPoseEstimate_wpiBlue(String limelightName) { + return getBotPoseEstimate(limelightName, "botpose_wpiblue", false); + } + + /** + * Gets the MegaTag2 Pose2d and timestamp for use with WPILib pose estimator + * (addVisionMeasurement) in the WPILib Blue alliance coordinate system. Make sure you are calling + * setRobotOrientation() before calling this method. + * + * @param limelightName + * @return + */ + public static PoseEstimate getBotPoseEstimate_wpiBlue_MegaTag2(String limelightName) { + return getBotPoseEstimate(limelightName, "botpose_orb_wpiblue", true); + } + + /** + * Gets the Pose2d for easy use with Odometry vision pose estimator (addVisionMeasurement) + * + * @param limelightName + * @return + */ + public static Pose2d getBotPose2d_wpiRed(String limelightName) { + + double[] result = getBotPose_wpiRed(limelightName); + return toPose2D(result); + } + + /** + * Gets the Pose2d and timestamp for use with WPILib pose estimator (addVisionMeasurement) when + * you are on the RED alliance + * + * @param limelightName + * @return + */ + public static PoseEstimate getBotPoseEstimate_wpiRed(String limelightName) { + return getBotPoseEstimate(limelightName, "botpose_wpired", false); + } + + /** + * Gets the Pose2d and timestamp for use with WPILib pose estimator (addVisionMeasurement) when + * you are on the RED alliance + * + * @param limelightName + * @return + */ + public static PoseEstimate getBotPoseEstimate_wpiRed_MegaTag2(String limelightName) { + return getBotPoseEstimate(limelightName, "botpose_orb_wpired", true); + } + + /** + * Gets the Pose2d for easy use with Odometry vision pose estimator (addVisionMeasurement) + * + * @param limelightName + * @return + */ + public static Pose2d getBotPose2d(String limelightName) { + + double[] result = getBotPose(limelightName); + return toPose2D(result); + } + + /** + * Gets the current IMU data from NetworkTables. IMU data is formatted as [robotYaw, Roll, Pitch, + * Yaw, gyroX, gyroY, gyroZ, accelX, accelY, accelZ]. Returns all zeros if data is invalid or + * unavailable. + * + * @param limelightName Name/identifier of the Limelight + * @return IMUData object containing all current IMU data + */ + public static IMUData getIMUData(String limelightName) { + double[] imuData = getLimelightNTDoubleArray(limelightName, "imu"); + if (imuData == null || imuData.length < 10) { + return new IMUData(); // Returns object with all zeros + } + return new IMUData(imuData); + } + + ///// + ///// + + public static void setPipelineIndex(String limelightName, int pipelineIndex) { + setLimelightNTDouble(limelightName, "pipeline", pipelineIndex); + } + + public static void setPriorityTagID(String limelightName, int ID) { + setLimelightNTDouble(limelightName, "priorityid", ID); + } + + /** + * Sets LED mode to be controlled by the current pipeline. + * + * @param limelightName Name of the Limelight camera + */ + public static void setLEDMode_PipelineControl(String limelightName) { + setLimelightNTDouble(limelightName, "ledMode", 0); + } + + public static void setLEDMode_ForceOff(String limelightName) { + setLimelightNTDouble(limelightName, "ledMode", 1); + } + + public static void setLEDMode_ForceBlink(String limelightName) { + setLimelightNTDouble(limelightName, "ledMode", 2); + } + + public static void setLEDMode_ForceOn(String limelightName) { + setLimelightNTDouble(limelightName, "ledMode", 3); + } + + /** + * Enables standard side-by-side stream mode. + * + * @param limelightName Name of the Limelight camera + */ + public static void setStreamMode_Standard(String limelightName) { + setLimelightNTDouble(limelightName, "stream", 0); + } + + /** + * Enables Picture-in-Picture mode with secondary stream in the corner. + * + * @param limelightName Name of the Limelight camera + */ + public static void setStreamMode_PiPMain(String limelightName) { + setLimelightNTDouble(limelightName, "stream", 1); + } + + /** + * Enables Picture-in-Picture mode with primary stream in the corner. + * + * @param limelightName Name of the Limelight camera + */ + public static void setStreamMode_PiPSecondary(String limelightName) { + setLimelightNTDouble(limelightName, "stream", 2); + } + + /** + * Sets the crop window for the camera. The crop window in the UI must be completely open. + * + * @param limelightName Name of the Limelight camera + * @param cropXMin Minimum X value (-1 to 1) + * @param cropXMax Maximum X value (-1 to 1) + * @param cropYMin Minimum Y value (-1 to 1) + * @param cropYMax Maximum Y value (-1 to 1) + */ + public static void setCropWindow( + String limelightName, double cropXMin, double cropXMax, double cropYMin, double cropYMax) { + double[] entries = new double[4]; + entries[0] = cropXMin; + entries[1] = cropXMax; + entries[2] = cropYMin; + entries[3] = cropYMax; + setLimelightNTDoubleArray(limelightName, "crop", entries); + } + + /** + * Sets the keystone modification for the crop window. + * + * @param limelightName Name of the Limelight camera + * @param horizontal Horizontal keystone value (-0.95 to 0.95) + * @param vertical Vertical keystone value (-0.95 to 0.95) + */ + public static void setKeystone(String limelightName, double horizontal, double vertical) { + double[] entries = new double[2]; + entries[0] = horizontal; + entries[1] = vertical; + setLimelightNTDoubleArray(limelightName, "keystone_set", entries); + } + + /** Sets 3D offset point for easy 3D targeting. */ + public static void setFiducial3DOffset( + String limelightName, double offsetX, double offsetY, double offsetZ) { + double[] entries = new double[3]; + entries[0] = offsetX; + entries[1] = offsetY; + entries[2] = offsetZ; + setLimelightNTDoubleArray(limelightName, "fiducial_offset_set", entries); + } + + /** + * Sets robot orientation values used by MegaTag2 localization algorithm. + * + * @param limelightName Name/identifier of the Limelight + * @param yaw Robot yaw in degrees. 0 = robot facing red alliance wall in FRC + * @param yawRate (Unnecessary) Angular velocity of robot yaw in degrees per second + * @param pitch (Unnecessary) Robot pitch in degrees + * @param pitchRate (Unnecessary) Angular velocity of robot pitch in degrees per second + * @param roll (Unnecessary) Robot roll in degrees + * @param rollRate (Unnecessary) Angular velocity of robot roll in degrees per second + */ + public static void SetRobotOrientation( + String limelightName, + double yaw, + double yawRate, + double pitch, + double pitchRate, + double roll, + double rollRate) { + SetRobotOrientation_INTERNAL( + limelightName, yaw, yawRate, pitch, pitchRate, roll, rollRate, true); + } + + public static void SetRobotOrientation_NoFlush( + String limelightName, + double yaw, + double yawRate, + double pitch, + double pitchRate, + double roll, + double rollRate) { + SetRobotOrientation_INTERNAL( + limelightName, yaw, yawRate, pitch, pitchRate, roll, rollRate, false); + } + + private static void SetRobotOrientation_INTERNAL( + String limelightName, + double yaw, + double yawRate, + double pitch, + double pitchRate, + double roll, + double rollRate, + boolean flush) { + + double[] entries = new double[6]; + entries[0] = yaw; + entries[1] = yawRate; + entries[2] = pitch; + entries[3] = pitchRate; + entries[4] = roll; + entries[5] = rollRate; + setLimelightNTDoubleArray(limelightName, "robot_orientation_set", entries); + if (flush) { + Flush(); + } + } + + /** + * Configures the IMU mode for MegaTag2 Localization + * + * @param limelightName Name/identifier of the Limelight + * @param mode IMU mode. + */ + public static void SetIMUMode(String limelightName, int mode) { + setLimelightNTDouble(limelightName, "imumode_set", mode); + } + + /** + * Configures the complementary filter alpha value for IMU Assist Modes (Modes 3 and 4) + * + * @param limelightName Name/identifier of the Limelight + * @param alpha Defaults to .001. Higher values will cause the internal IMU to converge onto the + * assist source more rapidly. + */ + public static void SetIMUAssistAlpha(String limelightName, double alpha) { + setLimelightNTDouble(limelightName, "imuassistalpha_set", alpha); + } + + /** + * Configures the throttle value. Set to 100-200 while disabled to reduce thermal + * output/temperature. + * + * @param limelightName Name/identifier of the Limelight + * @param throttle Defaults to 0. Your Limelgiht will process one frame after skipping + * frames. + */ + public static void SetThrottle(String limelightName, int throttle) { + setLimelightNTDouble(limelightName, "throttle_set", throttle); + } + + /** + * Overrides the valid AprilTag IDs that will be used for localization. Tags not in this list will + * be ignored for robot pose estimation. + * + * @param limelightName Name/identifier of the Limelight + * @param validIDs Array of valid AprilTag IDs to track + */ + public static void SetFiducialIDFiltersOverride(String limelightName, int[] validIDs) { + double[] validIDsDouble = new double[validIDs.length]; + for (int i = 0; i < validIDs.length; i++) { + validIDsDouble[i] = validIDs[i]; + } + setLimelightNTDoubleArray(limelightName, "fiducial_id_filters_set", validIDsDouble); + } + + /** + * Sets the downscaling factor for AprilTag detection. Increasing downscale can improve + * performance at the cost of potentially reduced detection range. + * + * @param limelightName Name/identifier of the Limelight + * @param downscale Downscale factor. Valid values: 1.0 (no downscale), 1.5, 2.0, 3.0, 4.0. Set to + * 0 for pipeline control. + */ + public static void SetFiducialDownscalingOverride(String limelightName, float downscale) { + int d = 0; // pipeline + if (downscale == 1.0) { + d = 1; + } + if (downscale == 1.5) { + d = 2; + } + if (downscale == 2) { + d = 3; + } + if (downscale == 3) { + d = 4; + } + if (downscale == 4) { + d = 5; + } + setLimelightNTDouble(limelightName, "fiducial_downscale_set", d); + } + + /** + * Sets the camera pose relative to the robot. + * + * @param limelightName Name of the Limelight camera + * @param forward Forward offset in meters + * @param side Side offset in meters + * @param up Up offset in meters + * @param roll Roll angle in degrees + * @param pitch Pitch angle in degrees + * @param yaw Yaw angle in degrees + */ + public static void setCameraPose_RobotSpace( + String limelightName, + double forward, + double side, + double up, + double roll, + double pitch, + double yaw) { + double[] entries = new double[6]; + entries[0] = forward; + entries[1] = side; + entries[2] = up; + entries[3] = roll; + entries[4] = pitch; + entries[5] = yaw; + setLimelightNTDoubleArray(limelightName, "camerapose_robotspace_set", entries); + } + + ///// + ///// + + public static void setPythonScriptData(String limelightName, double[] outgoingPythonData) { + setLimelightNTDoubleArray(limelightName, "llrobot", outgoingPythonData); + } + + public static double[] getPythonScriptData(String limelightName) { + return getLimelightNTDoubleArray(limelightName, "llpython"); + } + + ///// + ///// + + /** + * Triggers a snapshot capture via NetworkTables by incrementing the snapshot counter. + * Rate-limited to once per 10 frames on the Limelight. + * + * @param limelightName Name of the Limelight camera + */ + public static void triggerSnapshot(String limelightName) { + double current = getLimelightNTDouble(limelightName, "snapshot"); + setLimelightNTDouble(limelightName, "snapshot", current + 1); + } + + /** + * Enables or pauses the rewind buffer recording. + * + * @param limelightName Name of the Limelight camera + * @param enabled True to enable recording, false to pause + */ + public static void setRewindEnabled(String limelightName, boolean enabled) { + setLimelightNTDouble(limelightName, "rewind_enable_set", enabled ? 1 : 0); + } + + /** + * Triggers a rewind capture with the specified duration. Maximum duration is 165 seconds. + * Rate-limited on the Limelight. + * + * @param limelightName Name of the Limelight camera + * @param durationSeconds Duration of rewind capture in seconds (max 165) + */ + public static void triggerRewindCapture(String limelightName, double durationSeconds) { + double[] currentArray = getLimelightNTDoubleArray(limelightName, "capture_rewind"); + double counter = (currentArray.length > 0) ? currentArray[0] : 0; + double[] entries = new double[2]; + entries[0] = counter + 1; + entries[1] = Math.min(durationSeconds, 165); + setLimelightNTDoubleArray(limelightName, "capture_rewind", entries); + } + + /** + * Gets the latest JSON results output and returns a LimelightResults object. + * + * @param limelightName Name of the Limelight camera + * @return LimelightResults object containing all current target data + */ + public static LimelightResults getLatestResults(String limelightName) { + + long start = System.nanoTime(); + LimelightHelpers.LimelightResults results = new LimelightHelpers.LimelightResults(); + if (mapper == null) { + mapper = + new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + try { + String jsonString = getJSONDump(limelightName); + if (jsonString == null || jsonString.isEmpty() || jsonString.isBlank()) { + results.error = "lljson error: empty json"; + } else { + results = mapper.readValue(jsonString, LimelightResults.class); + if (results.imuResults != null) { + results.imuResults.parseDataArray(); + } + } + } catch (JsonProcessingException e) { + results.error = "lljson error: " + e.getMessage(); + } + + long end = System.nanoTime(); + double millis = (end - start) * .000001; + results.latency_jsonParse = millis; + if (profileJSON) { + System.out.printf("lljson: %.2f\r\n", millis); + } + + return results; + } + + /** + * Sets up port forwarding for a Limelight 3A/3G connected via USB. This allows access to the + * Limelight web interface and video stream when connected to the robot over USB. + * + *

For usbIndex 0: ports 5800-5809 forward to 172.29.0.1 For usbIndex 1: ports 5810-5819 + * forward to 172.29.1.1 etc. + * + *

Call this method once during robot initialization. To access the interface of the camera + * with usbIndex0, you would go to roboRIO-(teamnum)-FRC.local:5801. Port 5811 for usb index 1 + * + * @param usbIndex The USB index of the Limelight (0, 1, 2, etc.) + */ + public static void setupPortForwardingUSB(int usbIndex) { + String ip = "172.29." + usbIndex + ".1"; + int basePort = 5800 + (usbIndex * 10); + + for (int i = 0; i < 10; i++) { + PortForwarder.add(basePort + i, ip, 5800 + i); + } + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/Setpoint.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/Setpoint.java new file mode 100644 index 00000000..17070f46 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/Setpoint.java @@ -0,0 +1,91 @@ +package edu.wpi.team190.gompeilib.core.utility; + +import edu.wpi.first.units.Measure; +import edu.wpi.first.units.Unit; +import edu.wpi.team190.gompeilib.core.logging.Trace; +import lombok.Getter; + +public class Setpoint { + @Getter private Measure setpoint; + @Getter private Measure newSetpoint; + @Getter private Measure offset; + private final Measure step; + public Measure min; + public Measure max; + + public Setpoint(Measure setpoint, Measure step, Measure min, Measure max) { + this.setpoint = setpoint; + this.offset = setpoint.times(0); + this.step = step; + this.min = min.minus(setpoint); + this.max = max.minus(setpoint); + this.newSetpoint = setpoint; + } + + public Setpoint(Measure setpoint, Measure step) { + this.setpoint = setpoint; + this.offset = setpoint.times(0); + this.step = step; + this.min = step.times(Double.NEGATIVE_INFINITY); + this.max = step.times(Double.POSITIVE_INFINITY); + this.newSetpoint = setpoint; + } + + public void setSetpoint(Measure setpoint) { + this.min = min.plus(this.setpoint).minus(setpoint); + this.max = max.plus(this.setpoint).minus(setpoint); + this.setpoint = setpoint; + + // offset = clamp(offset); + newSetpoint = calculateSetpoint(); + } + + @Trace + private Measure calculateSetpoint() { + double sign = Math.signum(setpoint.magnitude()); + + // Apply offset in the direction of the setpoint's sign + // positive offset always increases magnitude, negative always decreases + var newMagnitude = setpoint.plus(offset.times(sign)); + + // Clamp to zero if we've crossed zero (don't flip sign) + if (Math.signum(newMagnitude.magnitude()) != sign && sign != 0) { + return setpoint.times(0); // return zero in same unit + } + + return newMagnitude; + } + + public void increment() { + increment(step); + } + + public void decrement() { + decrement(step); + } + + public void increment(Measure step) { + Measure next = offset.plus(step); + if (next.gt(max)) { + offset = max; + } else { + offset = next; + } + newSetpoint = calculateSetpoint(); + } + + public void decrement(Measure step) { + Measure next = offset.minus(step); + if (next.lt(min)) { + offset = min; + } else { + offset = next; + } + newSetpoint = calculateSetpoint(); + } + + public void reset() { + offset = offset.times(0); + newSetpoint = setpoint; + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/VirtualSubsystem.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/VirtualSubsystem.java new file mode 100644 index 00000000..c3444e3e --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/VirtualSubsystem.java @@ -0,0 +1,30 @@ +// Copyright (c) 2023 FRC 6328 +// http://github.com/Mechanical-Advantage +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file at +// the root directory of this project. + +package edu.wpi.team190.gompeilib.core.utility; + +import java.util.ArrayList; +import java.util.List; + +/** Represents a subsystem unit that requires a periodic callback but not a hardware mutex. */ +public abstract class VirtualSubsystem { + private static List subsystems = new ArrayList<>(); + + public VirtualSubsystem() { + subsystems.add(this); + } + + /** Calls {@link #periodic()} on all virtual subsystems. */ + public static void periodicAll() { + for (var subsystem : subsystems) { + subsystem.periodic(); + } + } + + /** This method is called periodically once per loop cycle. */ + public abstract void periodic(); +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/control/CurrentLimits.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/control/CurrentLimits.java new file mode 100644 index 00000000..c707ba18 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/control/CurrentLimits.java @@ -0,0 +1,18 @@ +package edu.wpi.team190.gompeilib.core.utility.control; + +import edu.wpi.first.units.Units; +import edu.wpi.first.units.measure.Current; +import lombok.Builder; + +@Builder(setterPrefix = "with") +public record CurrentLimits(Current supplyCurrentLimit, Current statorCurrentLimit) { + @Builder( + setterPrefix = "with", + builderClassName = "FromDoubles", + builderMethodName = "fromDoubles") + public CurrentLimits(double supplyCurrentLimit, double statorCurrentLimit) { + this( + Current.ofBaseUnits(supplyCurrentLimit, Units.Amps), + Current.ofBaseUnits(statorCurrentLimit, Units.Amps)); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/control/Gains.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/control/Gains.java new file mode 100644 index 00000000..1cf75598 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/control/Gains.java @@ -0,0 +1,96 @@ +package edu.wpi.team190.gompeilib.core.utility.control; + +import edu.wpi.team190.gompeilib.core.utility.tunable.LoggedTunableNumber; +import java.util.function.Consumer; +import lombok.Builder; +import lombok.NonNull; + +@Builder(setterPrefix = "with") +public record Gains( + LoggedTunableNumber kP, + LoggedTunableNumber kI, + LoggedTunableNumber kD, + LoggedTunableNumber kS, + LoggedTunableNumber kV, + LoggedTunableNumber kA, + LoggedTunableNumber kG) { + + @Builder( + setterPrefix = "with", + builderMethodName = "fromDoubles", + builderClassName = "FromDoubles") + public Gains( + @NonNull String prefix, + double kP, + double kI, + double kD, + double kS, + double kV, + double kA, + double kG) { + this( + new LoggedTunableNumber(prefix + "/Kp", kP), + new LoggedTunableNumber(prefix + "/Ki", kI), + new LoggedTunableNumber(prefix + "/Kd", kD), + new LoggedTunableNumber(prefix + "/Ks", kS), + new LoggedTunableNumber(prefix + "/Kv", kV), + new LoggedTunableNumber(prefix + "/Ka", kA), + new LoggedTunableNumber(prefix + "/Kg", kG)); + } + + public Gains { + if (kP == null) { + kP = new LoggedTunableNumber(""); + } + if (kI == null) { + kI = new LoggedTunableNumber(""); + } + if (kD == null) { + kD = new LoggedTunableNumber(""); + } + if (kS == null) { + kS = new LoggedTunableNumber(""); + } + if (kV == null) { + kV = new LoggedTunableNumber(""); + } + if (kA == null) { + kA = new LoggedTunableNumber(""); + } + if (kG == null) { + kG = new LoggedTunableNumber(""); + } + } + + public double getKP() { + return kP.get(); + } + + public double getKI() { + return kI.get(); + } + + public double getKD() { + return kD.get(); + } + + public double getKS() { + return kS.get(); + } + + public double getKV() { + return kV.get(); + } + + public double getKA() { + return kA.get(); + } + + public double getKG() { + return kG.get(); + } + + public void update(int id, Consumer consumer) { + LoggedTunableNumber.ifChanged(id, g -> consumer.accept(this), kP, kI, kD, kS, kV, kA, kG); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/control/LinearProfile.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/control/LinearProfile.java new file mode 100644 index 00000000..24640e1a --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/control/LinearProfile.java @@ -0,0 +1,101 @@ +// Copyright (c) 2024 FRC 6328 +// http://github.com/Mechanical-Advantage +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file at +// the root directory of this project. + +package edu.wpi.team190.gompeilib.core.utility.control; + +import edu.wpi.first.math.MathUtil; +import edu.wpi.first.math.geometry.Twist2d; +import lombok.Getter; +import lombok.Setter; + +/** Ramps up and down to setpoint for velocity closed loop control */ +public class LinearProfile { + private double dv; + private double maxVelocity; + @Getter private final double period; + @Getter private double currentSetpoint = 0; + @Getter @Setter private double goal = 0; + + /** + * Creates a new LinearProfile + * + * @param maxAcceleration The max ramp rate in velocity in rps/sec + * @param period Period of control loop (0.02) + */ + public LinearProfile(double maxAcceleration, double maxVelocity, double period) { + this.period = period; + setMaxAcceleration(maxAcceleration); + } + + /** Set the max acceleration constraint in rpm/sec */ + public void setMaxAcceleration(double maxAcceleration) { + dv = maxAcceleration * period; + } + + public void setMaxVelocity(double maxVelocity) { + this.maxVelocity = maxVelocity; + } + + /** + * Sets the target setpoint, starting from the current speed + * + * @param goal Target setpoint + * @param currentSpeed Current speed, to be used as the starting setpoint + */ + public void setGoal(double goal, double currentSpeed) { + this.goal = goal; + currentSetpoint = currentSpeed; + } + + /** Resets target setpoint and current setpoint */ + public void reset() { + currentSetpoint = 0; + goal = 0; + } + + /** + * Returns the current setpoint to send to motors + * + * @return Setpoint to send to motors + */ + public double calculateSetpoint() { + if (EqualsUtil.epsilonEquals(goal, currentSetpoint)) { + return currentSetpoint; + } + if (goal > currentSetpoint) { + currentSetpoint += dv; + if (currentSetpoint > goal) { + currentSetpoint = goal; + } + } else if (goal < currentSetpoint) { + currentSetpoint -= dv; + if (currentSetpoint < goal) { + currentSetpoint = goal; + } + } + return MathUtil.clamp(currentSetpoint, -maxVelocity, maxVelocity); + } + + public class EqualsUtil { + public static boolean epsilonEquals(double a, double b, double epsilon) { + return (a - epsilon <= b) && (a + epsilon >= b); + } + + public static boolean epsilonEquals(double a, double b) { + return epsilonEquals(a, b, 1e-9); + } + + /** Extension methods for wpi geometry objects */ + public static class GeomExtensions { + public static boolean epsilonEquals(Twist2d twist, Twist2d other) { + return EqualsUtil.epsilonEquals(twist.dx, other.dx) + && EqualsUtil.epsilonEquals(twist.dy, other.dy) + && EqualsUtil.epsilonEquals(twist.dtheta, other.dtheta); + } + } + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/control/constraints/AngularPositionConstraints.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/control/constraints/AngularPositionConstraints.java new file mode 100644 index 00000000..b542df45 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/control/constraints/AngularPositionConstraints.java @@ -0,0 +1,49 @@ +package edu.wpi.team190.gompeilib.core.utility.control.constraints; + +import edu.wpi.first.units.*; +import edu.wpi.team190.gompeilib.core.utility.tunable.LoggedTunableMeasure; +import java.util.function.Consumer; +import lombok.Builder; +import lombok.NonNull; + +/** Specifically for Angular constraints (Degrees, Radians, Rotations). */ +@Builder(setterPrefix = "with") +public record AngularPositionConstraints( + LoggedTunableMeasure goalTolerance, + LoggedTunableMeasure maxVelocity, + LoggedTunableMeasure maxAcceleration) + implements Constraints, + Constraints.PositionConstraints { + + @Builder( + setterPrefix = "with", + builderClassName = "FromMeasures", + builderMethodName = "fromMeasures") + public AngularPositionConstraints( + @NonNull String prefix, + Measure goalTolerance, + Measure maxVelocity, + Measure maxAcceleration) { + this( + new LoggedTunableMeasure<>(String.format("%s/Goal Tolerance", prefix), goalTolerance), + new LoggedTunableMeasure<>(String.format("%s/Max Velocity", prefix), maxVelocity), + new LoggedTunableMeasure<>(String.format("%s/Max Acceleration", prefix), maxAcceleration)); + } + + public double getGoalTolerance(AngleUnit unit) { + return goalTolerance.get(unit); + } + + public double getMaxVelocity(AngularVelocityUnit unit) { + return maxVelocity.get(unit); + } + + public double getMaxAcceleration(AngularAccelerationUnit unit) { + return maxAcceleration.get(unit); + } + + public void update(int id, Consumer consumer) { + LoggedTunableMeasure.ifChanged( + id, () -> consumer.accept(this), goalTolerance, maxVelocity, maxAcceleration); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/control/constraints/AngularVelocityConstraints.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/control/constraints/AngularVelocityConstraints.java new file mode 100644 index 00000000..d66c91fc --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/control/constraints/AngularVelocityConstraints.java @@ -0,0 +1,38 @@ +package edu.wpi.team190.gompeilib.core.utility.control.constraints; + +import edu.wpi.first.units.AngularAccelerationUnit; +import edu.wpi.first.units.AngularVelocityUnit; +import edu.wpi.first.units.Measure; +import edu.wpi.team190.gompeilib.core.utility.tunable.LoggedTunableMeasure; +import java.util.function.Consumer; +import lombok.Builder; +import lombok.NonNull; + +/** Specifically for Angular constraints (Degrees, Radians, Rotations). */ +@Builder(setterPrefix = "with") +public record AngularVelocityConstraints( + LoggedTunableMeasure goalTolerance, + LoggedTunableMeasure maxVelocity, + LoggedTunableMeasure maxAcceleration) + implements Constraints { + + @Builder( + setterPrefix = "with", + builderClassName = "FromMeasures", + builderMethodName = "fromMeasures") + public AngularVelocityConstraints( + @NonNull String prefix, + Measure goalTolerance, + Measure maxVelocity, + Measure maxAcceleration) { + this( + new LoggedTunableMeasure<>(String.format("%s/Goal Tolerance", prefix), goalTolerance), + new LoggedTunableMeasure<>(String.format("%s/Max Velocity", prefix), maxVelocity), + new LoggedTunableMeasure<>(String.format("%s/Max Acceleration", prefix), maxAcceleration)); + } + + public void update(int id, Consumer consumer) { + LoggedTunableMeasure.ifChanged( + id, () -> consumer.accept(this), goalTolerance, maxVelocity, maxAcceleration); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/control/constraints/Constraints.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/control/constraints/Constraints.java new file mode 100644 index 00000000..fd09f865 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/control/constraints/Constraints.java @@ -0,0 +1,10 @@ +package edu.wpi.team190.gompeilib.core.utility.control.constraints; + +import java.util.function.Consumer; + +public interface Constraints> { + public void update(int id, Consumer consumer); + + public sealed interface PositionConstraints> + extends Constraints permits AngularPositionConstraints, LinearConstraints {} +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/control/constraints/LinearConstraints.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/control/constraints/LinearConstraints.java new file mode 100644 index 00000000..0de82929 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/control/constraints/LinearConstraints.java @@ -0,0 +1,48 @@ +package edu.wpi.team190.gompeilib.core.utility.control.constraints; + +import edu.wpi.first.units.*; +import edu.wpi.team190.gompeilib.core.utility.tunable.LoggedTunableMeasure; +import java.util.function.Consumer; +import lombok.Builder; +import lombok.NonNull; + +/** Specifically for Angular constraints (Degrees, Radians, Rotations). */ +@Builder(setterPrefix = "with") +public record LinearConstraints( + LoggedTunableMeasure goalTolerance, + LoggedTunableMeasure maxVelocity, + LoggedTunableMeasure maxAcceleration) + implements Constraints, Constraints.PositionConstraints { + + @Builder( + setterPrefix = "with", + builderMethodName = "fromMeasures", + builderClassName = "FromMeasures") + public LinearConstraints( + @NonNull String prefix, + Measure goalTolerance, + Measure maxVelocity, + Measure maxAcceleration) { + this( + new LoggedTunableMeasure<>(String.format("%s/Goal Tolerance", prefix), goalTolerance), + new LoggedTunableMeasure<>(String.format("%s/Max Velocity", prefix), maxVelocity), + new LoggedTunableMeasure<>(String.format("%s/Max Acceleration", prefix), maxAcceleration)); + } + + public double getGoalToleranceMeters(DistanceUnit unit) { + return goalTolerance.get(unit); + } + + public double getMaxVelocityMetersPerSecond(LinearVelocityUnit unit) { + return maxVelocity.get(unit); + } + + public double getMaxAccelerationMetersPerSecondSquared(LinearAccelerationUnit unit) { + return maxAcceleration.get(unit); + } + + public void update(int id, Consumer consumer) { + LoggedTunableMeasure.ifChanged( + id, () -> consumer.accept(this), goalTolerance, maxVelocity, maxAcceleration); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/phoenix/GainSlot.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/phoenix/GainSlot.java new file mode 100644 index 00000000..328ee878 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/phoenix/GainSlot.java @@ -0,0 +1,17 @@ +package edu.wpi.team190.gompeilib.core.utility.phoenix; + +/** An Enum class that handles all possible gain slots for motor control */ +public enum GainSlot { + ZERO, + ONE, + TWO; + + public static GainSlot integerToGainSlot(Integer integer) { + return switch (integer) { + case 0 -> ZERO; + case 1 -> ONE; + case 2 -> TWO; + default -> null; + }; + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/phoenix/PhoenixOdometryThread.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/phoenix/PhoenixOdometryThread.java new file mode 100644 index 00000000..77cd15be --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/phoenix/PhoenixOdometryThread.java @@ -0,0 +1,167 @@ +// Copyright 2021-2024 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 edu.wpi.team190.gompeilib.core.utility.phoenix; + +import com.ctre.phoenix6.BaseStatusSignal; +import com.ctre.phoenix6.StatusSignal; +import edu.wpi.first.units.measure.Angle; +import edu.wpi.first.wpilibj.RobotController; +import edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive.SwerveDriveConstants; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.DoubleSupplier; + +/** + * Provides an interface for asynchronously reading high-frequency measurements to a set of queues. + * + *

This version is intended for Phoenix 6 devices on both the RIO and CANivore buses. When using + * a CANivore, the thread uses the "waitForAll" blocking method to enable more consistent sampling. + * This also allows Phoenix Pro users to benefit from lower latency between devices using CANivore + * time synchronization. + */ +public class PhoenixOdometryThread extends Thread { + private final SwerveDriveConstants driveConstants; + private final Lock signalsLock = + new ReentrantLock(); // Prevents conflicts when registering signals + private BaseStatusSignal[] phoenixSignals = new BaseStatusSignal[0]; + private final List genericSignals = new ArrayList<>(); + private final List> phoenixQueues = new ArrayList<>(); + private final List> genericQueues = new ArrayList<>(); + private final List> timestampQueues = new ArrayList<>(); + + private static boolean isCANFD; + private static PhoenixOdometryThread instance = null; + + public static PhoenixOdometryThread getInstance(SwerveDriveConstants driveConstants) { + if (instance == null) { + instance = new PhoenixOdometryThread(driveConstants); + } + return instance; + } + + private PhoenixOdometryThread(SwerveDriveConstants driveConstants) { + this.driveConstants = driveConstants; + isCANFD = driveConstants.driveConfig.canBus().isNetworkFD(); + setName("PhoenixOdometryThread"); + setDaemon(true); + } + + @Override + public void start() { + if (timestampQueues.size() > 0) { + super.start(); + } + } + + /** Registers a Phoenix signal to be read from the thread. */ + public Queue registerSignal(StatusSignal signal) { + Queue queue = new ArrayBlockingQueue<>(20); + signalsLock.lock(); + driveConstants.reentrantLock.lock(); + try { + BaseStatusSignal[] newSignals = new BaseStatusSignal[phoenixSignals.length + 1]; + System.arraycopy(phoenixSignals, 0, newSignals, 0, phoenixSignals.length); + newSignals[phoenixSignals.length] = signal; + phoenixSignals = newSignals; + phoenixQueues.add(queue); + } finally { + signalsLock.unlock(); + driveConstants.reentrantLock.unlock(); + } + return queue; + } + + /** Registers a generic signal to be read from the thread. */ + public Queue registerSignal(DoubleSupplier signal) { + Queue queue = new ArrayBlockingQueue<>(20); + signalsLock.lock(); + driveConstants.reentrantLock.lock(); + try { + genericSignals.add(signal); + genericQueues.add(queue); + } finally { + signalsLock.unlock(); + driveConstants.reentrantLock.unlock(); + } + return queue; + } + + /** Returns a new queue that returns timestamp values for each sample. */ + public Queue makeTimestampQueue() { + Queue queue = new ArrayBlockingQueue<>(20); + driveConstants.reentrantLock.lock(); + try { + timestampQueues.add(queue); + } finally { + driveConstants.reentrantLock.unlock(); + } + return queue; + } + + @Override + public void run() { + while (true) { + // Wait for updates from all signals + signalsLock.lock(); + try { + if (isCANFD && phoenixSignals.length > 0) { + BaseStatusSignal.waitForAll(2.0 / driveConstants.odometryFrequency, phoenixSignals); + } else { + // "waitForAll" does not support blocking on multiple signals with a bus + // that is not CAN FD, regardless of Pro licensing. No reasoning for this + // behavior is provided by the documentation. + Thread.sleep((long) (1000.0 / driveConstants.odometryFrequency)); + if (phoenixSignals.length > 0) BaseStatusSignal.refreshAll(phoenixSignals); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + signalsLock.unlock(); + } + + // Save new data to queues + driveConstants.reentrantLock.lock(); + try { + // Sample timestamp is current FPGA time minus average CAN latency + // Default timestamps from Phoenix are NOT compatible with + // FPGA timestamps, this solution is imperfect but close + double timestamp = RobotController.getFPGATime() / 1e6; + double totalLatency = 0.0; + for (BaseStatusSignal signal : phoenixSignals) { + totalLatency += signal.getTimestamp().getLatency(); + } + if (phoenixSignals.length > 0) { + timestamp -= totalLatency / phoenixSignals.length; + } + + // Add new samples to queues + for (int i = 0; i < phoenixSignals.length; i++) { + phoenixQueues.get(i).offer(phoenixSignals[i].getValueAsDouble()); + } + for (int i = 0; i < genericSignals.size(); i++) { + genericQueues.get(i).offer(genericSignals.get(i).getAsDouble()); + } + for (int i = 0; i < timestampQueues.size(); i++) { + timestampQueues.get(i).offer(timestamp); + } + } finally { + driveConstants.reentrantLock.unlock(); + } + } + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/phoenix/PhoenixUtil.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/phoenix/PhoenixUtil.java new file mode 100644 index 00000000..8c6c09fe --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/phoenix/PhoenixUtil.java @@ -0,0 +1,45 @@ +package edu.wpi.team190.gompeilib.core.utility.phoenix; + +import com.ctre.phoenix6.BaseStatusSignal; +import com.ctre.phoenix6.StatusCode; +import java.util.function.Supplier; + +public class PhoenixUtil { + /** Attempts to run the command until no error is produced. */ + public static void tryUntilOk(int maxAttempts, Supplier command) { + for (int i = 0; i < maxAttempts; i++) { + var error = command.get(); + if (error.isOK()) break; + } + } + + /** Signals for synchronized refresh. */ + private static BaseStatusSignal[] canivoreSignals = new BaseStatusSignal[0]; + + private static BaseStatusSignal[] rioSignals = new BaseStatusSignal[0]; + + /** Registers a set of signals for synchronized refresh. */ + public static void registerSignals(boolean canivore, BaseStatusSignal... signals) { + if (canivore) { + BaseStatusSignal[] newSignals = new BaseStatusSignal[canivoreSignals.length + signals.length]; + System.arraycopy(canivoreSignals, 0, newSignals, 0, canivoreSignals.length); + System.arraycopy(signals, 0, newSignals, canivoreSignals.length, signals.length); + canivoreSignals = newSignals; + } else { + BaseStatusSignal[] newSignals = new BaseStatusSignal[rioSignals.length + signals.length]; + System.arraycopy(rioSignals, 0, newSignals, 0, rioSignals.length); + System.arraycopy(signals, 0, newSignals, rioSignals.length, signals.length); + rioSignals = newSignals; + } + } + + /** Refresh all registered signals. */ + public static void refreshAll() { + if (canivoreSignals.length > 0) { + BaseStatusSignal.refreshAll(canivoreSignals); + } + if (rioSignals.length > 0) { + BaseStatusSignal.refreshAll(rioSignals); + } + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/sysid/CustomSysIdRoutine.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/sysid/CustomSysIdRoutine.java new file mode 100644 index 00000000..8f53a9d4 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/sysid/CustomSysIdRoutine.java @@ -0,0 +1,151 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package edu.wpi.team190.gompeilib.core.utility.sysid; + +import static edu.wpi.first.units.Units.*; + +import edu.wpi.first.units.*; +import edu.wpi.first.units.measure.Per; +import edu.wpi.first.wpilibj.Timer; +import edu.wpi.first.wpilibj.sysid.SysIdRoutineLog; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Subsystem; +import java.util.function.Consumer; + +/** + * A generic SysId characterization routine. Subclass this for specific units. + * + * @param The unit type for the output (e.g., VoltageUnit, CurrentUnit) + */ +public class CustomSysIdRoutine extends SysIdRoutineLog { + private final Config config; + private final Mechanism mechanism; + private final MutableMeasure outputValue; + private final Consumer recordState; + + /** + * Create a new SysId characterization routine. * @param config Configuration with strongly typed + * measures. + * + * @param mechanism Mechanism interface. + * @param initialMutable The mutable measure instance (created by the subclass). + */ + public CustomSysIdRoutine( + Config config, Mechanism mechanism, MutableMeasure initialMutable) { + super(mechanism.name); + this.config = config; + this.mechanism = mechanism; + outputValue = initialMutable; + recordState = config.recordState != null ? config.recordState : this::recordState; + } + + /** + * @param rampRate We use Measure for ramp rate because it is U/Time. + */ + public record Config( + Per rampRate, + Measure stepOutput, + Measure timeout, + Consumer recordState, + U outputUnit) { + public Config( + Per rampRate, + Measure stepOutput, + Measure timeout, + Consumer recordState, + U outputUnit) { + this.rampRate = rampRate; + this.stepOutput = stepOutput; + this.timeout = timeout != null ? timeout : Seconds.of(10); + this.recordState = recordState; + this.outputUnit = outputUnit; + } + } + + public static class Mechanism { + public final Consumer> drive; + public final Consumer log; + public final Subsystem subsystem; + public final String name; + + public Mechanism( + Consumer> drive, + Consumer log, + Subsystem subsystem, + String name) { + this.drive = drive; + this.log = log != null ? log : l -> {}; + this.subsystem = subsystem; + this.name = name != null ? name : subsystem.getName(); + } + + public Mechanism(Consumer> drive, Subsystem subsystem) { + this(drive, null, subsystem, null); + } + } + + public enum Direction { + kForward, + kReverse + } + + public Command quasistatic(Direction direction) { + State state = + (direction == Direction.kForward) ? State.kQuasistaticForward : State.kQuasistaticReverse; + + double outputSign = (direction == Direction.kForward) ? 1.0 : -1.0; + Timer timer = new Timer(); + + double rampRateUnitsPerSec = config.rampRate.magnitude(); + + return mechanism + .subsystem + .runOnce(timer::restart) + .andThen( + mechanism.subsystem.run( + () -> { + mechanism.drive.accept( + outputValue.mut_replace( + outputSign * timer.get() * rampRateUnitsPerSec, config.outputUnit)); + + mechanism.log.accept(this); + recordState.accept(state); + })) + .finallyDo( + () -> { + mechanism.drive.accept(outputValue.mut_replace(0, config.outputUnit)); + recordState.accept(State.kNone); + timer.stop(); + }) + .withName("sysid-" + state + "-" + mechanism.name) + .withTimeout(config.timeout.in(Seconds)); + } + + public Command dynamic(Direction direction) { + double outputSign = (direction == Direction.kForward) ? 1.0 : -1.0; + State state = (direction == Direction.kForward) ? State.kDynamicForward : State.kDynamicReverse; + + // OPTIMIZED: Pre-calculate step magnitude safely + double stepMagnitude = config.stepOutput.in(config.outputUnit); + + return mechanism + .subsystem + .runOnce(() -> outputValue.mut_replace(stepMagnitude * outputSign, config.outputUnit)) + .andThen( + mechanism.subsystem.run( + () -> { + mechanism.drive.accept(outputValue); + mechanism.log.accept(this); + recordState.accept(state); + })) + .finallyDo( + () -> { + mechanism.drive.accept(outputValue.mut_replace(0, config.outputUnit)); + recordState.accept(State.kNone); + }) + .withName("sysid-" + state.toString() + "-" + mechanism.name) + .withTimeout(config.timeout.in(Seconds)); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/sysid/CustomUnits.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/sysid/CustomUnits.java new file mode 100644 index 00000000..845eb212 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/sysid/CustomUnits.java @@ -0,0 +1,13 @@ +package edu.wpi.team190.gompeilib.core.utility.sysid; + +import static edu.wpi.first.units.Units.*; + +import edu.wpi.first.units.CurrentUnit; +import edu.wpi.first.units.PerUnit; +import edu.wpi.first.units.TimeUnit; +import edu.wpi.first.units.VoltageUnit; + +public class CustomUnits { + public static final PerUnit ampsPerSecond = Amps.per(Second); + public static final PerUnit voltsPerSecond = Volts.per(Second); +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/tunable/LoggedTunableMeasure.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/tunable/LoggedTunableMeasure.java new file mode 100644 index 00000000..c4179743 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/tunable/LoggedTunableMeasure.java @@ -0,0 +1,100 @@ +package edu.wpi.team190.gompeilib.core.utility.tunable; + +import edu.wpi.first.units.Measure; +import edu.wpi.first.units.Unit; +import edu.wpi.team190.gompeilib.core.GompeiLib; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Supplier; +import org.littletonrobotics.junction.networktables.LoggedNetworkNumber; + +/** + * Class for a tunable measure (unit-safe). Gets value from dashboard in tuning mode, returns + * default if not or value not in dashboard. + */ +public class LoggedTunableMeasure implements Supplier> { + private static final String tableKey = "TunableNumbers"; + + private final String key; + private final U unit; + private boolean hasDefault = false; + private Measure defaultValue; + private LoggedNetworkNumber dashboardNumber; + private final Map> lastHasChangedValues = new HashMap<>(); + + /** + * Create a new LoggedTunableMeasure with the default value + * + * @param dashboardKey Key on dashboard + * @param defaultValue Default measure value + */ + public LoggedTunableMeasure(String dashboardKey, Measure defaultValue) { + this.key = tableKey + "/" + dashboardKey + " (" + defaultValue.unit() + ")"; + this.unit = defaultValue.unit(); + initDefault(defaultValue); + } + + /** Set the default value. The default value can only be set once. */ + private void initDefault(Measure defaultValue) { + hasDefault = true; + this.defaultValue = defaultValue; + if (GompeiLib.isTuning()) { + // We store the raw double value in the base unit + dashboardNumber = new LoggedNetworkNumber(key, defaultValue.in(unit)); + } + } + + /** Get the current measure, from dashboard if available and in tuning mode. */ + @Override + @SuppressWarnings("unchecked") // safe + public Measure get() { + return (Measure) unit.of(getRawValue()); + } + + /** + * Get the current value converted to the specified unit. The unit must be of the same dimension + * (e.g., CurrentUnit for current values). + * + * @param targetUnit The unit to convert the value to (must be compatible with the base unit) + * @return The current value in the specified unit + */ + @SuppressWarnings("unchecked") + public double get(Unit targetUnit) { + return get().in((U) targetUnit); + } + + public double getRawValue() { + return GompeiLib.isTuning() ? dashboardNumber.get() : defaultValue.in(unit); + } + + /** Checks whether the measure has changed since our last check */ + public boolean hasChanged(int id) { + Measure currentValue = get(); + Measure lastValue = lastHasChangedValues.get(id); + if (!currentValue.equals(lastValue)) { + lastHasChangedValues.put(id, currentValue); + return true; + } + return false; + } + + /** Runs action if any of the tunableMeasures have changed */ + @SafeVarargs + public static void ifChanged( + int id, Consumer[]> action, LoggedTunableMeasure... tunableNumbers) { + if (Arrays.stream(tunableNumbers).anyMatch(n -> n.hasChanged(id))) { + @SuppressWarnings("unchecked") + Measure[] values = + Arrays.stream(tunableNumbers).map(LoggedTunableMeasure::get).toArray(Measure[]::new); + action.accept(values); + } + } + + public static void ifChanged(int id, Runnable action, LoggedTunableMeasure... tunableNumbers) { + if (Arrays.stream(tunableNumbers).anyMatch(n -> n.hasChanged(id))) { + action.run(); + } + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/tunable/LoggedTunableNumber.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/tunable/LoggedTunableNumber.java new file mode 100644 index 00000000..07b0db43 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/tunable/LoggedTunableNumber.java @@ -0,0 +1,123 @@ +// Copyright (c) 2024 FRC 6328 +// http://github.com/Mechanical-Advantage +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file at +// the root directory of this project. + +package edu.wpi.team190.gompeilib.core.utility.tunable; + +import edu.wpi.team190.gompeilib.core.GompeiLib; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.DoubleSupplier; +import org.littletonrobotics.junction.networktables.LoggedNetworkNumber; + +/** + * Class for a tunable number. Gets value from dashboard in tuning mode, returns default if not or + * value not in dashboard. + */ +public class LoggedTunableNumber implements DoubleSupplier { + private static final String tableKey = "TunableNumbers"; + + private final String key; + private boolean hasDefault = false; + private double defaultValue; + private LoggedNetworkNumber dashboardNumber; + private final Map lastHasChangedValues = new HashMap<>(); + + /** + * Create a new LoggedTunableNumber + * + * @param dashboardKey Key on dashboard + */ + public LoggedTunableNumber(String dashboardKey) { + this.key = tableKey + "/" + dashboardKey; + } + + /** + * Create a new LoggedTunableNumber with the default value + * + * @param dashboardKey Key on dashboard + * @param defaultValue Default value + */ + public LoggedTunableNumber(String dashboardKey, double defaultValue) { + this(dashboardKey); + initDefault(defaultValue); + } + + /** + * Set the default value of the number. The default value can only be set once. + * + * @param defaultValue The default value + */ + public void initDefault(double defaultValue) { + if (!hasDefault) { + hasDefault = true; + this.defaultValue = defaultValue; + if (GompeiLib.isTuning()) { + dashboardNumber = new LoggedNetworkNumber(key, defaultValue); + } + } + } + + /** + * Get the current value, from dashboard if available and in tuning mode. + * + * @return The current value + */ + public double get() { + if (!hasDefault) { + return 0.0; + } else { + return GompeiLib.isTuning() ? dashboardNumber.get() : defaultValue; + } + } + + /** + * Checks whether the number has changed since our last check + * + * @param id Unique identifier for the caller to avoid conflicts when shared between multiple + * objects. Recommended approach is to pass the result of "hashCode()" + * @return True if the number has changed since the last time this method was called, false + * otherwise. + */ + public boolean hasChanged(int id) { + double currentValue = get(); + Double lastValue = lastHasChangedValues.get(id); + if (lastValue == null || currentValue != lastValue) { + lastHasChangedValues.put(id, currentValue); + return true; + } + + return false; + } + + /** + * Runs action if any of the tunableNumbers have changed + * + * @param id Unique identifier for the caller to avoid conflicts when shared between multiple * + * objects. Recommended approach is to pass the result of "hashCode()" + * @param action Callback to run when any of the tunable numbers have changed. Access tunable + * numbers in order inputted in method + * @param tunableNumbers All tunable numbers to check + */ + public static void ifChanged( + int id, Consumer action, LoggedTunableNumber... tunableNumbers) { + if (Arrays.stream(tunableNumbers).anyMatch(tunableNumber -> tunableNumber.hasChanged(id))) { + action.accept(Arrays.stream(tunableNumbers).mapToDouble(LoggedTunableNumber::get).toArray()); + } + } + + /** Runs action if any of the tunableNumbers have changed */ + public static void ifChanged(int id, Runnable action, LoggedTunableNumber... tunableNumbers) { + ifChanged(id, values -> action.run(), tunableNumbers); + } + + @Override + public double getAsDouble() { + return get(); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/tunable/TunableUpdaterRegistry.java b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/tunable/TunableUpdaterRegistry.java new file mode 100644 index 00000000..b08556c6 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/core/utility/tunable/TunableUpdaterRegistry.java @@ -0,0 +1,63 @@ +package edu.wpi.team190.gompeilib.core.utility.tunable; + +import edu.wpi.team190.gompeilib.core.utility.control.Gains; +import edu.wpi.team190.gompeilib.core.utility.control.constraints.Constraints; +import java.util.Arrays; +import java.util.HashMap; +import java.util.function.Consumer; + +/** + * Central registry for tunable objects that need to be polled for runtime updates. + * + *

Each registry maps a tunable object or group of tunable objects to the action that should run + * when a value changes. The {@link #periodic()} method should be called regularly to detect changes + * and invoke the corresponding update callbacks. + */ +public class TunableUpdaterRegistry { + + private TunableUpdaterRegistry() {} + + /** Registered gain sets and their update callbacks. */ + private static final HashMap> GAINS_UPDATER = new HashMap<>(); + + /** Registered constraint sets and their update callbacks. */ + private static final HashMap, Consumer> CONSTRAINTS_UPDATER = new HashMap<>(); + + /** Registered tunable number groups and their update callbacks. */ + private static final HashMap> NUMBER_UPDATER = + new HashMap<>(); + + /** Registered tunable measure groups and their update callbacks. */ + private static final HashMap[], Runnable> MEASURE_UPDATER = + new HashMap<>(); + + /** + * Checks all registered tunables for changes and runs any associated update callbacks. + * + *

This method is intended to be called periodically, once per robot loop, so runtime tuning + * changes are detected and applied. + */ + public static void periodic() { + GAINS_UPDATER.forEach((k, v) -> k.update(k.hashCode(), v)); + CONSTRAINTS_UPDATER.forEach((k, v) -> k.update(k.hashCode(), v)); + NUMBER_UPDATER.forEach((k, v) -> LoggedTunableNumber.ifChanged(Arrays.hashCode(k), v, k)); + MEASURE_UPDATER.forEach((k, v) -> LoggedTunableMeasure.ifChanged(Arrays.hashCode(k), v, k)); + } + + public static void registerGains(Gains g, Consumer c) { + GAINS_UPDATER.putIfAbsent(g, c); + } + + public static void registerConstraints( + Constraints constraints, Consumer> consumer) { + CONSTRAINTS_UPDATER.putIfAbsent(constraints, consumer); + } + + public static void registerNumber(LoggedTunableNumber[] numbers, Consumer consumer) { + NUMBER_UPDATER.putIfAbsent(numbers, consumer); + } + + public static void registerMeasure(LoggedTunableMeasure[] measures, Runnable consumer) { + MEASURE_UPDATER.putIfAbsent(measures, consumer); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/arm/Arm.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/arm/Arm.java new file mode 100644 index 00000000..ff81c393 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/arm/Arm.java @@ -0,0 +1,183 @@ +package edu.wpi.team190.gompeilib.subsystems.arm; + +import static edu.wpi.first.units.Units.*; + +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.units.AngleUnit; +import edu.wpi.first.units.VoltageUnit; +import edu.wpi.first.units.measure.Angle; +import edu.wpi.first.units.measure.Voltage; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; +import edu.wpi.first.wpilibj2.command.Subsystem; +import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine; +import edu.wpi.team190.gompeilib.core.logging.Trace; +import edu.wpi.team190.gompeilib.core.utility.Setpoint; +import edu.wpi.team190.gompeilib.core.utility.control.Gains; +import edu.wpi.team190.gompeilib.core.utility.control.constraints.AngularPositionConstraints; +import edu.wpi.team190.gompeilib.core.utility.phoenix.GainSlot; +import lombok.Getter; +import org.littletonrobotics.junction.Logger; + +public class Arm { + public ArmIO io; + public ArmIOInputsAutoLogged inputs; + + private final String aKitTopic; + + @Getter private ArmState currentState; + + @Getter private Setpoint voltageGoal; + @Getter private Setpoint positionGoal; + + private final SysIdRoutine characterizationRoutine; + + public Arm( + ArmIO io, + Subsystem subsystem, + int index, + ArmConstants constants, + Setpoint positionGoal, + Setpoint voltageGoal) { + this.io = io; + this.inputs = new ArmIOInputsAutoLogged(); + + aKitTopic = subsystem.getName() + "/Arms" + index; + + currentState = ArmState.IDLE; + + this.positionGoal = positionGoal; + this.voltageGoal = voltageGoal; + + characterizationRoutine = + new SysIdRoutine( + new SysIdRoutine.Config( + Volts.of(1).per(Second), + Volts.of(9), + Seconds.of(12), + (state) -> Logger.recordOutput(aKitTopic + "/SysIdState", state.toString())), + new SysIdRoutine.Mechanism(io::setVoltageGoal, null, subsystem)); + } + + public Arm(ArmIO io, Subsystem subsystem, int index, ArmConstants constants) { + this( + io, + subsystem, + index, + constants, + new Setpoint<>( + Rotation2d.kZero.getMeasure(), + constants.positionOffsetStep.getMeasure(), + constants.armParameters.maxAngle().getMeasure(), + constants.armParameters.minAngle().getMeasure()), + new Setpoint<>(Volts.of(0), constants.voltageOffsetStep, Volts.of(-12), Volts.of(12))); + } + + public Arm( + ArmIO io, + Subsystem subsystem, + int index, + ArmConstants constants, + Setpoint positionGoal) { + this( + io, + subsystem, + index, + constants, + positionGoal, + new Setpoint<>(Volts.of(0), constants.voltageOffsetStep, Volts.of(-12), Volts.of(12))); + } + + @Trace + public void periodic() { + io.updateInputs(inputs); + Logger.processInputs(aKitTopic, inputs); + + Logger.recordOutput(aKitTopic + "/State", currentState.name()); + Logger.recordOutput(aKitTopic + "/Voltage Goal", voltageGoal.getSetpoint()); + Logger.recordOutput( + aKitTopic + "/Position Goal", new Rotation2d((Angle) positionGoal.getSetpoint())); + Logger.recordOutput(aKitTopic + "/Voltage Offset", voltageGoal.getOffset()); + Logger.recordOutput(aKitTopic + "/Position Offset", positionGoal.getOffset()); + Logger.recordOutput(aKitTopic + "/At Voltage Goal", atVoltageGoal()); + Logger.recordOutput(aKitTopic + "/At Position Goal", atPositionGoal()); + + switch (currentState) { + case OPEN_LOOP_VOLTAGE_CONTROL -> io.setVoltageGoal((Voltage) voltageGoal.getNewSetpoint()); + case CLOSED_LOOP_POSITION_CONTROL -> + io.setPositionGoal(new Rotation2d((Angle) positionGoal.getNewSetpoint())); + } + } + + public Rotation2d getArmPosition() { + return inputs.position; + } + + public void setVoltageGoal(Voltage voltageGoal) { + currentState = ArmState.OPEN_LOOP_VOLTAGE_CONTROL; + this.voltageGoal.setSetpoint(voltageGoal); + } + + public void setVoltageGoal(Setpoint voltageGoal) { + currentState = ArmState.OPEN_LOOP_VOLTAGE_CONTROL; + this.voltageGoal = voltageGoal; + } + + public void setPositionGoal(Rotation2d positionGoal) { + currentState = ArmState.CLOSED_LOOP_POSITION_CONTROL; + this.positionGoal.setSetpoint(positionGoal.getMeasure()); + } + + public void setPositionGoal(Setpoint positionGoal) { + currentState = ArmState.CLOSED_LOOP_POSITION_CONTROL; + this.positionGoal = positionGoal; + } + + public boolean atVoltageGoal(Voltage voltageReference) { + return io.atVoltageGoal(voltageReference); + } + + public boolean atPositionGoal(Rotation2d positionReference) { + return io.atPositionGoal(positionReference); + } + + public boolean atVoltageGoal() { + return atVoltageGoal((Voltage) voltageGoal.getNewSetpoint()); + } + + public boolean atPositionGoal() { + return atPositionGoal(new Rotation2d((Angle) positionGoal.getNewSetpoint())); + } + + public void setPosition(Rotation2d position) { + io.setPosition(position); + } + + public void setGainSlot(GainSlot slot) { + io.setGainSlot(slot); + } + + public Command waitUntilAtGoal() { + return Commands.waitUntil(this::atPositionGoal); + } + + public void updateGains(Gains gains, GainSlot slot) { + io.updateGains(gains, slot); + } + + public void updateConstraints(AngularPositionConstraints constraints) { + io.updateConstraints(constraints); + } + + public Command sysIdRoutine() { + return Commands.sequence( + Commands.runOnce(() -> currentState = ArmState.IDLE), + characterizationRoutine.quasistatic(SysIdRoutine.Direction.kForward), + Commands.waitSeconds(1.0), + characterizationRoutine.quasistatic(SysIdRoutine.Direction.kReverse), + Commands.waitSeconds(1.0), + characterizationRoutine.dynamic(SysIdRoutine.Direction.kForward), + Commands.waitSeconds(1.0), + characterizationRoutine.dynamic(SysIdRoutine.Direction.kReverse)); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmConstants.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmConstants.java new file mode 100644 index 00000000..3099c33a --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmConstants.java @@ -0,0 +1,39 @@ +package edu.wpi.team190.gompeilib.subsystems.arm; + +import com.ctre.phoenix6.CANBus; +import com.ctre.phoenix6.signals.InvertedValue; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.system.plant.DCMotor; +import edu.wpi.first.units.measure.Voltage; +import edu.wpi.team190.gompeilib.core.utility.control.CurrentLimits; +import edu.wpi.team190.gompeilib.core.utility.control.Gains; +import edu.wpi.team190.gompeilib.core.utility.control.constraints.AngularPositionConstraints; +import lombok.Builder; +import lombok.NonNull; + +@Builder(setterPrefix = "with") +public class ArmConstants { + @NonNull public final Integer armCANID; + @NonNull public final CANBus canBus; + @NonNull public final ArmParameters armParameters; + @NonNull public final Gains slot0Gains; + @Builder.Default public final Gains slot1Gains = Gains.builder().build(); + @Builder.Default public final Gains slot2Gains = Gains.builder().build(); + @NonNull public final AngularPositionConstraints constraints; + @NonNull public final CurrentLimits currentLimits; + @NonNull public final Boolean enableFOC; + @NonNull public final InvertedValue invertedValue; + @NonNull public final Voltage voltageOffsetStep; + @NonNull public final Rotation2d positionOffsetStep; + + @Builder(setterPrefix = "with") + public record ArmParameters( + @NonNull DCMotor motorConfig, + @NonNull Rotation2d minAngle, + @NonNull Rotation2d maxAngle, + @NonNull Boolean continuousOutput, + @NonNull Integer numMotors, + @NonNull Double gearRatio, + @NonNull Double lengthMeters, + @NonNull Double momentOfInertia) {} +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIO.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIO.java new file mode 100644 index 00000000..ed9e00c1 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIO.java @@ -0,0 +1,98 @@ +package edu.wpi.team190.gompeilib.subsystems.arm; + +import static edu.wpi.first.units.Units.RadiansPerSecond; +import static edu.wpi.first.units.Units.RadiansPerSecondPerSecond; + +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.units.measure.*; +import edu.wpi.team190.gompeilib.core.utility.control.Gains; +import edu.wpi.team190.gompeilib.core.utility.control.constraints.AngularPositionConstraints; +import edu.wpi.team190.gompeilib.core.utility.phoenix.GainSlot; +import org.littletonrobotics.junction.AutoLog; + +public interface ArmIO { + @AutoLog + public static class ArmIOInputs { + public Rotation2d position = new Rotation2d(); + public AngularVelocity velocity = RadiansPerSecond.of(0.0); + public AngularAcceleration acceleration = RadiansPerSecondPerSecond.of(0.0); + + public double[] appliedVolts = new double[] {}; + public double[] supplyCurrentAmps = new double[] {}; + public double[] torqueCurrentAmps = new double[] {}; + public double[] temperatureCelsius = new double[] {}; + + public Rotation2d positionGoal = new Rotation2d(); + public Rotation2d positionSetpoint = new Rotation2d(); + public Rotation2d positionError = new Rotation2d(); + + public GainSlot gainSlot = GainSlot.ZERO; + } + + /** + * Updates the inputs for the arm. + * + * @param inputs The inputs to update. + */ + default void updateInputs(ArmIOInputs inputs) {} + + /** + * Sets the voltage for the arm. + * + * @param voltageGoal The voltage of the arm in volts + */ + default void setVoltageGoal(Voltage voltageGoal) {} + + /** + * Sets the position goal of the arm + * + * @param positionGoal the position goal of the arm + */ + default void setPositionGoal(Rotation2d positionGoal) {} + + /** + * Checks if the voltage of the arm matches the volts argument + * + * @param voltageReference The voltage to check against + * @return True if the voltage matches, false otherwise + */ + default boolean atVoltageGoal(Voltage voltageReference) { + return false; + } + + /** + * Checks if the position of the arm matches the positionGoal argument + * + * @param positionReference the position to check against + * @return True if the position matches, false otherwise + */ + default boolean atPositionGoal(Rotation2d positionReference) { + return false; + } + + /** + * Sets the position of the arm. + * + * @param position The position to set. + */ + default void setPosition(Rotation2d position) {} + + /** + * @param gainSlot The CTRE gain slot to set the arm to. + */ + default void setGainSlot(GainSlot gainSlot) {} + + /** + * Sets the gains for the arm. + * + * @param gains the gains to update + */ + default void updateGains(Gains gains, GainSlot gainSlot) {} + + /** + * Sets the constraints for the arm. + * + * @param constraints the constraints to update + */ + default void updateConstraints(AngularPositionConstraints constraints) {} +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIOSim.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIOSim.java new file mode 100644 index 00000000..9b5db8a9 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIOSim.java @@ -0,0 +1,164 @@ +package edu.wpi.team190.gompeilib.subsystems.arm; + +import static edu.wpi.first.units.Units.*; + +import edu.wpi.first.math.MathUtil; +import edu.wpi.first.math.controller.ArmFeedforward; +import edu.wpi.first.math.controller.ProfiledPIDController; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.system.plant.LinearSystemId; +import edu.wpi.first.math.trajectory.TrapezoidProfile.Constraints; +import edu.wpi.first.units.measure.*; +import edu.wpi.first.wpilibj.simulation.SingleJointedArmSim; +import edu.wpi.team190.gompeilib.core.GompeiLib; +import edu.wpi.team190.gompeilib.core.utility.control.Gains; +import edu.wpi.team190.gompeilib.core.utility.control.constraints.AngularPositionConstraints; +import edu.wpi.team190.gompeilib.core.utility.phoenix.GainSlot; +import java.util.Arrays; + +public class ArmIOSim implements ArmIO { + private final SingleJointedArmSim armSim; + + private Voltage appliedVolts; + private boolean isClosedLoop; + private GainSlot gainSlot; + + private final ProfiledPIDController feedback; + private ArmFeedforward feedforward; + + private final ArmConstants constants; + + public ArmIOSim(ArmConstants constants) { + armSim = + new SingleJointedArmSim( + LinearSystemId.createSingleJointedArmSystem( + constants.armParameters.motorConfig(), + constants.armParameters.momentOfInertia(), + constants.armParameters.gearRatio()), + constants.armParameters.motorConfig(), + constants.armParameters.gearRatio(), + constants.armParameters.lengthMeters(), + constants.armParameters.minAngle().getRadians(), + constants.armParameters.maxAngle().getRadians(), + true, + constants.armParameters.minAngle().getRadians()); + + appliedVolts = Volts.of(0.0); + isClosedLoop = true; + gainSlot = GainSlot.ZERO; + + feedback = + new ProfiledPIDController( + constants.slot0Gains.kP().get(), + 0.0, + constants.slot0Gains.kD().get(), + new Constraints( + constants.constraints.maxVelocity().get().in(RadiansPerSecond), + constants.constraints.maxAcceleration().get().in(RadiansPerSecondPerSecond))); + if (constants.armParameters.continuousOutput()) { + feedback.enableContinuousInput( + constants.armParameters.minAngle().getRadians(), + constants.armParameters.maxAngle().getRadians()); + } + feedback.setTolerance(constants.constraints.goalTolerance().get().in(Radians)); + feedforward = + new ArmFeedforward( + constants.slot0Gains.kS().get(), + constants.slot0Gains.kV().get(), + constants.slot0Gains.kA().get(), + constants.slot0Gains.kG().get()); + + this.constants = constants; + } + + @Override + public void updateInputs(ArmIOInputs inputs) { + if (isClosedLoop) + appliedVolts = + Volts.of( + feedback.calculate(armSim.getAngleRads()) + + feedforward.calculate( + feedback.getSetpoint().position, feedback.getSetpoint().velocity)); + + appliedVolts = Volts.of(MathUtil.clamp(appliedVolts.in(Volts), -12.0, 12.0)); + armSim.setInputVoltage(appliedVolts.in(Volts)); + armSim.update(GompeiLib.getLoopPeriod()); + + inputs.position = Rotation2d.fromRadians(armSim.getAngleRads()); + inputs.velocity = RadiansPerSecond.of(armSim.getVelocityRadPerSec()); + + inputs.appliedVolts = new double[constants.armParameters.numMotors()]; + inputs.supplyCurrentAmps = new double[constants.armParameters.numMotors()]; + inputs.torqueCurrentAmps = new double[constants.armParameters.numMotors()]; + inputs.temperatureCelsius = new double[constants.armParameters.numMotors()]; + + Arrays.fill(inputs.appliedVolts, appliedVolts.in(Volts)); + Arrays.fill(inputs.supplyCurrentAmps, armSim.getCurrentDrawAmps()); + Arrays.fill(inputs.torqueCurrentAmps, armSim.getCurrentDrawAmps()); + + inputs.positionGoal = Rotation2d.fromRadians(feedback.getGoal().position); + inputs.positionSetpoint = Rotation2d.fromRadians(feedback.getSetpoint().position); + inputs.positionError = Rotation2d.fromRadians(feedback.getPositionError()); + + inputs.gainSlot = gainSlot; + } + + @Override + public void setVoltageGoal(Voltage voltageGoal) { + isClosedLoop = false; + this.appliedVolts = voltageGoal; + } + + @Override + public void setPositionGoal(Rotation2d rotationGoal) { + isClosedLoop = true; + feedback.setGoal(rotationGoal.getRadians()); + } + + @Override + public boolean atVoltageGoal(Voltage voltageReference) { + return appliedVolts.isNear(voltageReference, Millivolts.of(500)); + } + + @Override + public boolean atPositionGoal(Rotation2d positionReference) { + return Math.abs(positionReference.getRadians() - armSim.getAngleRads()) + < constants.constraints.goalTolerance().get(Radians); + } + + @Override + public void setPosition(Rotation2d position) { + armSim.setState(position.getRadians(), 0); + } + + @Override + public void setGainSlot(GainSlot gainSlot) { + this.gainSlot = gainSlot; + switch (gainSlot) { + case ZERO: + feedback.setPID(constants.slot0Gains.kP().get(), 0.0, constants.slot0Gains.kD().get()); + break; + case ONE: + feedback.setPID(constants.slot1Gains.kP().get(), 0.0, constants.slot1Gains.kD().get()); + break; + case TWO: + feedback.setPID(constants.slot2Gains.kP().get(), 0.0, constants.slot2Gains.kD().get()); + break; + } + } + + @Override + public void updateGains(Gains gains, GainSlot gainSlot) { + feedback.setPID(gains.kP().get(), gains.kI().get(), gains.kD().get()); + feedforward = new ArmFeedforward(gains.kS().get(), gains.kG().get(), gains.kV().get()); + } + + @Override + public void updateConstraints(AngularPositionConstraints constraints) { + feedback.setConstraints( + new Constraints( + constraints.maxVelocity().get().in(RadiansPerSecond), + constraints.maxAcceleration().get().in(RadiansPerSecondPerSecond))); + feedback.setTolerance(constraints.goalTolerance().get().in(Radians)); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIOTalonFX.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIOTalonFX.java new file mode 100644 index 00000000..34c310f2 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIOTalonFX.java @@ -0,0 +1,268 @@ +package edu.wpi.team190.gompeilib.subsystems.arm; + +import static edu.wpi.first.units.Units.*; + +import com.ctre.phoenix6.BaseStatusSignal; +import com.ctre.phoenix6.StatusSignal; +import com.ctre.phoenix6.configs.MotionMagicConfigs; +import com.ctre.phoenix6.configs.TalonFXConfiguration; +import com.ctre.phoenix6.controls.MotionMagicVoltage; +import com.ctre.phoenix6.controls.VoltageOut; +import com.ctre.phoenix6.hardware.TalonFX; +import com.ctre.phoenix6.signals.GravityTypeValue; +import com.ctre.phoenix6.signals.NeutralModeValue; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.units.measure.Angle; +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.units.measure.Current; +import edu.wpi.first.units.measure.Temperature; +import edu.wpi.first.units.measure.Voltage; +import edu.wpi.team190.gompeilib.core.GompeiLib; +import edu.wpi.team190.gompeilib.core.utility.control.Gains; +import edu.wpi.team190.gompeilib.core.utility.control.constraints.AngularPositionConstraints; +import edu.wpi.team190.gompeilib.core.utility.phoenix.GainSlot; +import edu.wpi.team190.gompeilib.core.utility.phoenix.PhoenixUtil; +import java.util.ArrayList; + +public class ArmIOTalonFX implements ArmIO { + protected final TalonFX talonFX; + private final TalonFX[] followTalonFX; + + private final StatusSignal positionRotations; + private final StatusSignal velocityRotationsPerSecond; + private ArrayList> appliedVolts; + private ArrayList> supplyCurrentAmps; + private ArrayList> torqueCurrentAmps; + private ArrayList> temperatureCelsius; + private final StatusSignal positionSetpointRotations; + private final StatusSignal positionErrorRotations; + + private StatusSignal[] statusSignals; + + private final VoltageOut voltageRequest; + private final MotionMagicVoltage positionVoltageRequest; + + private final TalonFXConfiguration config; + + protected final ArmConstants constants; + + public ArmIOTalonFX(ArmConstants constants) { + talonFX = new TalonFX(constants.armCANID, constants.canBus); + followTalonFX = new TalonFX[constants.armParameters.numMotors() - 1]; + + config = new TalonFXConfiguration(); + + config.MotorOutput.NeutralMode = NeutralModeValue.Brake; + config.CurrentLimits.SupplyCurrentLimit = constants.currentLimits.supplyCurrentLimit().in(Amps); + config.CurrentLimits.SupplyCurrentLimitEnable = true; + config.CurrentLimits.StatorCurrentLimit = constants.currentLimits.statorCurrentLimit().in(Amps); + config.CurrentLimits.StatorCurrentLimitEnable = true; + config.Feedback.SensorToMechanismRatio = constants.armParameters.gearRatio(); + config.MotorOutput.Inverted = constants.invertedValue; + + config.Slot0.withKP(constants.slot0Gains.kP().get()) + .withKD(constants.slot0Gains.kD().get()) + .withKS(constants.slot0Gains.kS().get()) + .withKV(constants.slot0Gains.kV().get()) + .withKA(constants.slot0Gains.kA().get()) + .withKG(constants.slot0Gains.kG().get()) + .withGravityType(GravityTypeValue.Arm_Cosine); + + config.Slot1.withKP(constants.slot1Gains.kP().get()) + .withKD(constants.slot1Gains.kD().get()) + .withKS(constants.slot1Gains.kS().get()) + .withKV(constants.slot1Gains.kV().get()) + .withKA(constants.slot1Gains.kA().get()) + .withKG(constants.slot1Gains.kG().get()) + .withGravityType(GravityTypeValue.Arm_Cosine); + + config.Slot2.withKP(constants.slot2Gains.kP().get()) + .withKD(constants.slot2Gains.kD().get()) + .withKS(constants.slot2Gains.kS().get()) + .withKV(constants.slot2Gains.kV().get()) + .withKA(constants.slot2Gains.kA().get()) + .withKG(constants.slot2Gains.kG().get()) + .withGravityType(GravityTypeValue.Arm_Cosine); + + config.MotorOutput.Inverted = constants.invertedValue; + config.ClosedLoopGeneral.ContinuousWrap = constants.armParameters.continuousOutput(); + config.MotionMagic = + new MotionMagicConfigs() + .withMotionMagicAcceleration( + constants.constraints.maxAcceleration().get().in(RotationsPerSecondPerSecond)) + .withMotionMagicCruiseVelocity( + constants.constraints.maxVelocity().get().in(RotationsPerSecond)); + + PhoenixUtil.tryUntilOk(5, () -> talonFX.getConfigurator().apply(config, 0.25)); + + appliedVolts = new ArrayList<>(); + supplyCurrentAmps = new ArrayList<>(); + torqueCurrentAmps = new ArrayList<>(); + temperatureCelsius = new ArrayList<>(); + + positionRotations = talonFX.getPosition(); + velocityRotationsPerSecond = talonFX.getVelocity(); + appliedVolts.add(talonFX.getMotorVoltage()); + supplyCurrentAmps.add(talonFX.getSupplyCurrent()); + torqueCurrentAmps.add(talonFX.getTorqueCurrent()); + temperatureCelsius.add(talonFX.getDeviceTemp()); + + positionSetpointRotations = talonFX.getClosedLoopReference(); + positionErrorRotations = talonFX.getClosedLoopError(); + + for (int i = 0; i < constants.armParameters.numMotors() - 1; i++) { + appliedVolts.add(followTalonFX[i].getMotorVoltage()); + supplyCurrentAmps.add(followTalonFX[i].getSupplyCurrent()); + torqueCurrentAmps.add(followTalonFX[i].getTorqueCurrent()); + temperatureCelsius.add(followTalonFX[i].getDeviceTemp()); + } + + voltageRequest = new VoltageOut(0).withEnableFOC(constants.enableFOC); + positionVoltageRequest = new MotionMagicVoltage(0).withEnableFOC(constants.enableFOC); + + var signalsList = new ArrayList>(); + + signalsList.add(positionRotations); + signalsList.add(velocityRotationsPerSecond); + signalsList.addAll(appliedVolts); + signalsList.addAll(supplyCurrentAmps); + signalsList.addAll(torqueCurrentAmps); + signalsList.addAll(temperatureCelsius); + signalsList.add(positionSetpointRotations); + signalsList.add(positionErrorRotations); + + statusSignals = new StatusSignal[signalsList.size()]; + + for (int i = 0; i < signalsList.size(); i++) { + statusSignals[i] = signalsList.get(i); + } + + BaseStatusSignal.setUpdateFrequencyForAll(1 / GompeiLib.getLoopPeriod(), statusSignals); + + talonFX.optimizeBusUtilization(); + + PhoenixUtil.registerSignals(constants.canBus.isNetworkFD(), statusSignals); + + talonFX.setPosition(0); + + this.constants = constants; + } + + @Override + public void updateInputs(ArmIOInputs inputs) { + + inputs.position = new Rotation2d(positionRotations.getValue()); + inputs.velocity = velocityRotationsPerSecond.getValue(); + + inputs.appliedVolts = new double[constants.armParameters.numMotors()]; + inputs.supplyCurrentAmps = new double[constants.armParameters.numMotors()]; + inputs.torqueCurrentAmps = new double[constants.armParameters.numMotors()]; + inputs.temperatureCelsius = new double[constants.armParameters.numMotors()]; + + for (int i = 0; i < constants.armParameters.numMotors(); i++) { + inputs.appliedVolts[i] = appliedVolts.get(i).getValueAsDouble(); + inputs.supplyCurrentAmps[i] = supplyCurrentAmps.get(i).getValueAsDouble(); + inputs.torqueCurrentAmps[i] = torqueCurrentAmps.get(i).getValueAsDouble(); + inputs.temperatureCelsius[i] = temperatureCelsius.get(i).getValueAsDouble(); + } + + inputs.positionGoal = new Rotation2d(positionVoltageRequest.getPositionMeasure()); + inputs.positionSetpoint = + Rotation2d.fromRotations(positionSetpointRotations.getValueAsDouble()); + inputs.positionError = Rotation2d.fromRotations(positionErrorRotations.getValueAsDouble()); + + inputs.gainSlot = GainSlot.integerToGainSlot(talonFX.getClosedLoopSlot().getValue()); + } + + @Override + public void setVoltageGoal(Voltage voltageGoal) { + talonFX.setControl(voltageRequest.withOutput(voltageGoal)); + } + + @Override + public void setPositionGoal(Rotation2d positionGoal) { + talonFX.setControl(positionVoltageRequest.withPosition(positionGoal.getRotations())); + } + + @Override + public boolean atVoltageGoal(Voltage voltageReference) { + return appliedVolts.get(0).getValue().isNear(voltageReference, Millivolts.of(500)); + } + + @Override + public boolean atPositionGoal(Rotation2d positionReference) { + return Math.abs(positionRotations.getValueAsDouble() - positionReference.getRotations()) + < constants.constraints.goalTolerance().get().in(Rotations); + } + + @Override + public void setPosition(Rotation2d position) { + talonFX.setPosition(position.getRotations()); + } + + @Override + public void setGainSlot(GainSlot gainSlot) { + switch (gainSlot) { + case ONE: + talonFX.setControl(positionVoltageRequest.withSlot(1)); + break; + case TWO: + talonFX.setControl(positionVoltageRequest.withSlot(2)); + break; + default: + talonFX.setControl(positionVoltageRequest.withSlot(0)); + } + } + + @Override + public void updateGains(Gains gains, GainSlot gainSlot) { + switch (gainSlot) { + case ZERO: + config.Slot0.withKP(gains.kP().get()) + .withKD(gains.kD().get()) + .withKS(gains.kS().get()) + .withKV(gains.kV().get()) + .withKA(gains.kA().get()) + .withKG(gains.kG().get()); + break; + case ONE: + config.Slot1.withKP(gains.kP().get()) + .withKD(gains.kD().get()) + .withKS(gains.kS().get()) + .withKV(gains.kV().get()) + .withKA(gains.kA().get()) + .withKG(gains.kG().get()); + break; + case TWO: + default: + config.Slot2.withKP(gains.kP().get()) + .withKD(gains.kD().get()) + .withKS(gains.kS().get()) + .withKV(gains.kV().get()) + .withKA(gains.kA().get()) + .withKG(gains.kG().get()); + break; + } + PhoenixUtil.tryUntilOk(5, () -> talonFX.getConfigurator().apply(config)); + + for (int i = 0; i < constants.armParameters.numMotors() - 1; i++) { + int finalI = i; + PhoenixUtil.tryUntilOk(5, () -> followTalonFX[finalI].getConfigurator().apply(config)); + } + } + + @Override + public void updateConstraints(AngularPositionConstraints constraints) { + config.MotionMagic = + new MotionMagicConfigs() + .withMotionMagicAcceleration( + constraints.maxAcceleration().get(RotationsPerSecondPerSecond)) + .withMotionMagicCruiseVelocity(constraints.maxVelocity().get(RotationsPerSecond)); + PhoenixUtil.tryUntilOk(5, () -> talonFX.getConfigurator().apply(config, 0.25)); + + for (int i = 0; i < constants.armParameters.numMotors() - 1; i++) { + int finalI = i; + PhoenixUtil.tryUntilOk(5, () -> followTalonFX[finalI].getConfigurator().apply(config, 0.25)); + } + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIOTalonFXSim.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIOTalonFXSim.java new file mode 100644 index 00000000..6a2087e5 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIOTalonFXSim.java @@ -0,0 +1,59 @@ +package edu.wpi.team190.gompeilib.subsystems.arm; + +import static edu.wpi.first.units.Units.Radians; +import static edu.wpi.first.units.Units.RadiansPerSecond; + +import com.ctre.phoenix6.sim.TalonFXSimState; +import edu.wpi.first.math.system.plant.LinearSystemId; +import edu.wpi.first.units.measure.Angle; +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.wpilibj.RobotController; +import edu.wpi.first.wpilibj.simulation.SingleJointedArmSim; +import edu.wpi.team190.gompeilib.core.GompeiLib; +import edu.wpi.team190.gompeilib.core.logging.Trace; + +public class ArmIOTalonFXSim extends ArmIOTalonFX { + private final SingleJointedArmSim armSim; + + private final TalonFXSimState armController; + + public ArmIOTalonFXSim(ArmConstants constants) { + super(constants); + armSim = + new SingleJointedArmSim( + LinearSystemId.createSingleJointedArmSystem( + constants.armParameters.motorConfig(), + constants.armParameters.momentOfInertia(), + constants.armParameters.gearRatio()), + constants.armParameters.motorConfig(), + constants.armParameters.gearRatio(), + constants.armParameters.lengthMeters(), + constants.armParameters.minAngle().getRadians(), + constants.armParameters.maxAngle().getRadians(), + true, + constants.armParameters.minAngle().getRadians()); + + armController = super.talonFX.getSimState(); + } + + @Override + @Trace + public void updateInputs(ArmIOInputs inputs) { + armController.setSupplyVoltage(RobotController.getBatteryVoltage()); + double armVoltage = armController.getMotorVoltage(); + + armSim.setInputVoltage(armVoltage); + + armSim.update(GompeiLib.getLoopPeriod()); + + Angle rotorPosition = + Angle.ofBaseUnits(armSim.getAngleRads() * constants.armParameters.gearRatio(), Radians); + AngularVelocity rotorVelocity = + AngularVelocity.ofBaseUnits( + armSim.getVelocityRadPerSec() * constants.armParameters.gearRatio(), RadiansPerSecond); + armController.setRawRotorPosition(rotorPosition); + armController.setRotorVelocity(rotorVelocity); + + super.updateInputs(inputs); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmState.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmState.java new file mode 100644 index 00000000..46c8de97 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmState.java @@ -0,0 +1,7 @@ +package edu.wpi.team190.gompeilib.subsystems.arm; + +public enum ArmState { + IDLE, + OPEN_LOOP_VOLTAGE_CONTROL, + CLOSED_LOOP_POSITION_CONTROL; +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveDrive.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveDrive.java new file mode 100644 index 00000000..47937c3b --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveDrive.java @@ -0,0 +1,491 @@ +// Copyright 2021-2024 FRC 6328 +package edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive; + +import choreo.auto.AutoFactory; +import choreo.trajectory.SwerveSample; +import com.pathplanner.lib.auto.AutoBuilder; +import com.pathplanner.lib.config.PIDConstants; +import com.pathplanner.lib.config.RobotConfig; +import com.pathplanner.lib.controllers.PPHolonomicDriveController; +import edu.wpi.first.math.VecBuilder; +import edu.wpi.first.math.Vector; +import edu.wpi.first.math.controller.PIDController; +import edu.wpi.first.math.filter.LinearFilter; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.geometry.Twist2d; +import edu.wpi.first.math.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.math.numbers.N2; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.first.wpilibj2.command.SubsystemBase; +import edu.wpi.team190.gompeilib.core.GompeiLib; +import edu.wpi.team190.gompeilib.core.io.components.inertial.GyroIO; +import edu.wpi.team190.gompeilib.core.io.components.inertial.GyroIOInputsAutoLogged; +import edu.wpi.team190.gompeilib.core.io.components.inertial.GyroIOPigeon2; +import edu.wpi.team190.gompeilib.core.logging.Trace; +import edu.wpi.team190.gompeilib.core.utility.control.Gains; +import edu.wpi.team190.gompeilib.core.utility.phoenix.PhoenixOdometryThread; +import java.util.List; +import java.util.Optional; +import java.util.Queue; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.IntStream; +import lombok.Getter; +import org.littletonrobotics.junction.Logger; + +public class SwerveDrive extends SubsystemBase { + private final SwerveDriveConstants driveConstants; + + private final GyroIO gyroIO; + private final GyroIOInputsAutoLogged gyroInputs; + private final SwerveModule[] modules; + + private final LinearFilter xFilter; + private final LinearFilter yFilter; + private double filteredX; + private double filteredY; + + private final SwerveDriveKinematics kinematics; + @Getter private Rotation2d rawGyroRotation; + private final SwerveModulePosition[] lastModulePositions; + @Getter private ChassisSpeeds measuredChassisSpeeds; + + @Getter private final AutoFactory autoFactory; + + private final Supplier robotPoseSupplier; + + private final PIDController autoXController; + private final PIDController autoYController; + private final PIDController autoHeadingController; + + private final Optional> yawTimestampQueue; + private final Optional> yawPositionQueue; + + private RobotConfig config; + + public SwerveDrive( + SwerveDriveConstants driveConstants, + GyroIO gyroIO, + SwerveModuleIO flModuleIO, + SwerveModuleIO frModuleIO, + SwerveModuleIO blModuleIO, + SwerveModuleIO brModuleIO, + Supplier robotPoseSupplier, + Consumer resetPoseConsumer) { + this.driveConstants = driveConstants; + this.gyroIO = gyroIO; + gyroInputs = new GyroIOInputsAutoLogged(); + modules = new SwerveModule[4]; // FL, FR, BL, BR + modules[0] = new SwerveModule(driveConstants, flModuleIO, 0); + modules[1] = new SwerveModule(driveConstants, frModuleIO, 1); + modules[2] = new SwerveModule(driveConstants, blModuleIO, 2); + modules[3] = new SwerveModule(driveConstants, brModuleIO, 3); + + xFilter = LinearFilter.movingAverage(10); + yFilter = LinearFilter.movingAverage(10); + filteredX = 0; + filteredY = 0; + + kinematics = this.driveConstants.driveConfig.kinematics(); + rawGyroRotation = new Rotation2d(); + lastModulePositions = // For delta tracking + new SwerveModulePosition[] { + new SwerveModulePosition(), + new SwerveModulePosition(), + new SwerveModulePosition(), + new SwerveModulePosition() + }; + + autoFactory = + new AutoFactory(robotPoseSupplier, resetPoseConsumer, this::choreoDrive, true, this); + + this.robotPoseSupplier = robotPoseSupplier; + + boolean isGryoHighFrequency = gyroIO instanceof GyroIOPigeon2; + + if (isGryoHighFrequency) { + // Start threads (no-op if no signals have been created) + PhoenixOdometryThread.getInstance(driveConstants).start(); + this.yawTimestampQueue = + Optional.of(PhoenixOdometryThread.getInstance(driveConstants).makeTimestampQueue()); + this.yawPositionQueue = + Optional.of( + PhoenixOdometryThread.getInstance(driveConstants).registerSignal(gyroIO.getYaw())); + } else { + this.yawTimestampQueue = Optional.empty(); + this.yawPositionQueue = Optional.empty(); + } + + autoHeadingController = + new PIDController( + driveConstants.autoRotationGains.kP().get(), + 0.0, + driveConstants.autoRotationGains.kD().get(), + GompeiLib.getLoopPeriod()); + autoXController = + new PIDController( + driveConstants.autoTranslationGains.kP().get(), + 0.0, + driveConstants.autoTranslationGains.kD().get(), + GompeiLib.getLoopPeriod()); + autoYController = + new PIDController( + driveConstants.autoTranslationGains.kP().get(), + 0.0, + driveConstants.autoTranslationGains.kD().get(), + GompeiLib.getLoopPeriod()); + + autoHeadingController.enableContinuousInput(-Math.PI, Math.PI); + autoHeadingController.setTolerance(Units.degreesToRadians(1.0)); + + measuredChassisSpeeds = new ChassisSpeeds(); + + try { + config = RobotConfig.fromGUISettings(); + } catch (Exception e) { + System.err.println("Error occurred while loading robot config: " + e.getMessage()); + } + + try { + AutoBuilder.configure( + this.robotPoseSupplier, + resetPoseConsumer, // resetPose + () -> getChassisSpeeds(), // get robotRelativeSpeeds + (speeds, feedforwards) -> { + List> forces = + IntStream.range(0, 4) + .mapToObj( + i -> + VecBuilder.fill( + feedforwards.robotRelativeForcesXNewtons()[i], + feedforwards.robotRelativeForcesYNewtons()[i])) + .toList(); + + runVelocity(speeds); + }, + new PPHolonomicDriveController( + new PIDConstants( + driveConstants.autoTranslationGains.kP().getAsDouble(), + driveConstants.autoTranslationGains.kI().getAsDouble(), + driveConstants.autoTranslationGains.kD().getAsDouble()), + new PIDConstants( + driveConstants.autoRotationGains.kP().getAsDouble(), + driveConstants.autoRotationGains.kI().getAsDouble(), + driveConstants.autoRotationGains.kD().getAsDouble())), + com.pathplanner.lib.config.RobotConfig.fromGUISettings(), + () -> { + var alliance = DriverStation.getAlliance(); + if (alliance.isPresent()) { + return alliance.get() == DriverStation.Alliance.Red; + } + return false; + }, + this); + } catch (Exception e) { + throw new RuntimeException("Failed to load PathPlanner robot config", e); + } + } + + @Trace + public void periodic() { + driveConstants.reentrantLock.lock(); + + if (yawTimestampQueue.isPresent() && yawPositionQueue.isPresent()) { + gyroIO.updateInputs(gyroInputs, yawTimestampQueue.get(), yawPositionQueue.get()); + yawTimestampQueue.get().clear(); + yawPositionQueue.get().clear(); + } else { + gyroIO.updateInputs(gyroInputs); + } + + for (int i = 0; i < 4; i++) { + modules[i].updateInputs(); + } + + driveConstants.reentrantLock.unlock(); + + Logger.processInputs("Drive/Gyro", gyroInputs); + + for (int i = 0; i < 4; i++) { + modules[i].periodic(); + } + + // Stop moving when disabled + if (DriverStation.isDisabled()) { + for (var module : modules) { + module.stop(); + } + + Logger.recordOutput("SwerveStates/Setpoints", new SwerveModuleState[] {}); + Logger.recordOutput("SwerveStates/SetpointsOptimized", new SwerveModuleState[] {}); + } + + Logger.recordOutput("SwerveStates/Measured", getModuleStates()); + Logger.recordOutput("SwerveChassisSpeeds/Measured", measuredChassisSpeeds); + + // Update odometry + double[] sampleTimestamps = + modules[0].getOdometryTimestamps(); // All signals are sampled together + int sampleCount = sampleTimestamps.length; + for (int i = 0; i < sampleCount; i++) { + // Read wheel positions and deltas from each module + SwerveModulePosition[] modulePositions = new SwerveModulePosition[4]; + SwerveModulePosition[] moduleDeltas = new SwerveModulePosition[4]; + for (int moduleIndex = 0; moduleIndex < 4; moduleIndex++) { + modulePositions[moduleIndex] = modules[moduleIndex].getOdometryPositions()[i]; + moduleDeltas[moduleIndex] = + new SwerveModulePosition( + modulePositions[moduleIndex].distanceMeters + - lastModulePositions[moduleIndex].distanceMeters, + modulePositions[moduleIndex].angle); + lastModulePositions[moduleIndex] = modulePositions[moduleIndex]; + } + + // Update gyro angle + if (gyroInputs.connected) { + // Use the real gyro angle + rawGyroRotation = gyroInputs.odometryYawPositions[i]; + } else { + // Use the angle delta from the kinematics and module deltas + Twist2d twist = kinematics.toTwist2d(moduleDeltas); + rawGyroRotation = rawGyroRotation.plus(new Rotation2d(twist.dtheta)); + } + + ChassisSpeeds chassisSpeeds = kinematics.toChassisSpeeds(getModuleStates()); + measuredChassisSpeeds = chassisSpeeds; + Translation2d rawFieldRelativeVelocity = + new Translation2d(chassisSpeeds.vxMetersPerSecond, chassisSpeeds.vyMetersPerSecond) + .rotateBy(getRawGyroRotation()); + + filteredX = xFilter.calculate(rawFieldRelativeVelocity.getX()); + filteredY = yFilter.calculate(rawFieldRelativeVelocity.getY()); + } + } + + /** + * Runs the drive at the desired velocity. + * + * @param speeds Speeds in meters/sec + */ + @Trace + public void runVelocity(ChassisSpeeds speeds) { + // Calculate module setpoints + ChassisSpeeds optimizedSpeeds = ChassisSpeeds.discretize(speeds, GompeiLib.getLoopPeriod()); + SwerveModuleState[] setpointStates = kinematics.toSwerveModuleStates(optimizedSpeeds); + SwerveDriveKinematics.desaturateWheelSpeeds( + setpointStates, driveConstants.driveConfig.maxLinearVelocityMetersPerSecond()); + + // Log unoptimized setpoints and setpoint speeds + Logger.recordOutput("SwerveStates/Setpoints", setpointStates); + Logger.recordOutput("SwerveChassisSpeeds/Setpoints", speeds); + + // Send setpoints to modules + for (int i = 0; i < 4; i++) { + modules[i].runSetpoint(setpointStates[i], new SwerveModuleState()); + } + + // Log optimized setpoints (runSetpoint mutates each state) + Logger.recordOutput("SwerveStates/SetpointsOptimized", setpointStates); + } + + /** + * Runs the drive at the desired velocity and torque. + * + * @param speeds Speeds in meters/sec + */ + @Trace + public void runVelocityTorque(ChassisSpeeds speeds, List> forces) { + if (forces.size() != 4) { + throw new IllegalArgumentException("Forces array must have 4 elements"); + } + // Calculate module setpoints + ChassisSpeeds optimizedSpeeds = ChassisSpeeds.discretize(speeds, GompeiLib.getLoopPeriod()); + SwerveModuleState[] setpointStates = kinematics.toSwerveModuleStates(optimizedSpeeds); + SwerveModuleState[] setpointTorques = new SwerveModuleState[4]; + SwerveDriveKinematics.desaturateWheelSpeeds( + setpointStates, driveConstants.driveConfig.maxLinearVelocityMetersPerSecond()); + + // Send setpoints to modules + for (int i = 0; i < 4; i++) { + Vector wheelDirection = + VecBuilder.fill(setpointStates[i].angle.getCos(), setpointStates[i].angle.getSin()); + setpointTorques[i] = + new SwerveModuleState( + forces.get(i).dot(wheelDirection) + * driveConstants.driveConfig.frontLeft().DriveMotorGearRatio, + setpointStates[i].angle); + + setpointStates[i].optimize(modules[i].getAngle()); + setpointTorques[i].optimize(modules[i].getAngle()); + + modules[i].runSetpoint(setpointStates[i], setpointTorques[i]); + } + + // Log optimized setpoints (runSetpoint mutates each state) + Logger.recordOutput("SwerveStates/SetpointsOptimized", setpointStates); + Logger.recordOutput("SwerveStates/TorquesOptimized", setpointTorques); + } + + /** Runs the drive in a straight line with the specified drive current. */ + @Trace + public void runCharacterization(double amps) { + for (int i = 0; i < 4; i++) { + modules[i].runCharacterization(amps); + } + } + + /** Stops the drive. */ + @Trace + 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. + */ + @Trace + public void stopWithX() { + Rotation2d[] headings = new Rotation2d[4]; + for (int i = 0; i < 4; i++) { + headings[i] = driveConstants.driveConfig.getModuleTranslations()[i].getAngle(); + } + kinematics.resetHeadings(headings); + stop(); + } + + /** Returns the module states (turn angles and drive velocities) for all of the modules. */ + @Trace + private SwerveModuleState[] getModuleStates() { + SwerveModuleState[] states = new SwerveModuleState[4]; + for (int i = 0; i < 4; i++) { + states[i] = modules[i].getState(); + } + return states; + } + + /** Returns the module positions (turn angles and drive positions) for all of the modules. */ + @Trace + public SwerveModulePosition[] getModulePositions() { + SwerveModulePosition[] states = new SwerveModulePosition[4]; + for (int i = 0; i < 4; i++) { + states[i] = modules[i].getPosition(); + } + return states; + } + + /** Returns the measured chassis speeds of the robot. */ + @Trace + private ChassisSpeeds getChassisSpeeds() { + return kinematics.toChassisSpeeds(getModuleStates()); + } + + /** Returns the position of each module in radians. */ + @Trace + public double[] getWheelRadiusCharacterizationPositions() { + double[] values = new double[4]; + for (int i = 0; i < 4; i++) { + values[i] = modules[i].getWheelRadiusCharacterizationPosition(); + } + return values; + } + + /** Returns the average velocity of the modules in rotations/sec (Phoenix native units). */ + @Trace + public double getFFCharacterizationVelocity() { + double output = 0.0; + for (int i = 0; i < 4; i++) { + output += modules[i].getFFCharacterizationVelocity() / 4.0; + } + return output; + } + + /** Returns the maximum linear speed in meters per sec. */ + @Trace + public double getMaxLinearSpeedMetersPerSec() { + return driveConstants.driveConfig.maxLinearVelocityMetersPerSecond(); + } + + /** Returns the maximum angular speed in radians per sec. */ + @Trace + public double getMaxAngularSpeedRadPerSec() { + return getMaxLinearSpeedMetersPerSec() / driveConstants.driveConfig.driveBaseRadius(); + } + + /** Returns the field relative velocity in X and Y. */ + @Trace + public Translation2d getFieldRelativeVelocity() { + return new Translation2d(filteredX, filteredY); + } + + /** Returns the current yaw velocity */ + @Trace + public double getYawVelocity() { + return gyroInputs.yawVelocityRadPerSec; + } + + /** Sets PID gains for modules */ + @Trace + public void setPIDGains(double drive_Kp, double drive_Kd, double turn_Kp, double turn_Kd) { + for (var module : modules) { + module.setPID(drive_Kp, drive_Kd, turn_Kp, turn_Kd); + } + } + + /** Sets FF gains for modules */ + @Trace + public void setFFGains(double kS, double kV) { + for (var module : modules) { + module.setFF(kS, kV); + } + } + + /** Runs a choreo path from swerve samples */ + @Trace + public void choreoDrive(SwerveSample sample) { + Pose2d pose = robotPoseSupplier.get(); + double xFF = sample.vx; + double yFF = sample.vy; + double rotationFF = sample.omega; + + double xFeedback = autoXController.calculate(pose.getX(), sample.x); + double yFeedback = autoYController.calculate(pose.getY(), sample.y); + double rotationFeedback = + autoHeadingController.calculate(pose.getRotation().getRadians(), sample.heading); + + ChassisSpeeds velocity = + ChassisSpeeds.fromFieldRelativeSpeeds( + xFF + xFeedback, + yFF + yFeedback, + rotationFF + rotationFeedback, + Rotation2d.fromRadians(sample.heading)); + + runVelocity(velocity); + Logger.recordOutput("Auto/Setpoint", sample.getPose()); + } + + public void setAutoControllers(Gains translationGains, Gains rotationGains) { + autoXController.setPID(translationGains.kP().get(), 0.0, translationGains.kD().get()); + autoYController.setPID(translationGains.kP().get(), 0.0, translationGains.kD().get()); + autoHeadingController.setPID(rotationGains.kP().get(), 0.0, rotationGains.kD().get()); + } + + /** + * Updates current limits. + * + * @param driveCurrentLimit The drive current limit. + * @param turnCurrentLimit The turn current limit. + */ + @Trace + public void updateCurrentLimits(double driveCurrentLimit, double turnCurrentLimit) { + for (SwerveModule s : modules) { + s.updateCurrentLimits(driveCurrentLimit, turnCurrentLimit); + } + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveDriveConstants.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveDriveConstants.java new file mode 100644 index 00000000..04fb8eee --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveDriveConstants.java @@ -0,0 +1,101 @@ +package edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive; + +import com.ctre.phoenix6.CANBus; +import com.ctre.phoenix6.configs.CANcoderConfiguration; +import com.ctre.phoenix6.configs.TalonFXConfiguration; +import com.ctre.phoenix6.swerve.SwerveModuleConstants; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.kinematics.SwerveDriveKinematics; +import edu.wpi.first.math.system.plant.DCMotor; +import edu.wpi.first.units.AngleUnit; +import edu.wpi.first.units.DistanceUnit; +import edu.wpi.team190.gompeilib.core.utility.control.Gains; +import edu.wpi.team190.gompeilib.core.utility.control.constraints.AngularPositionConstraints; +import edu.wpi.team190.gompeilib.core.utility.control.constraints.LinearConstraints; +import edu.wpi.team190.gompeilib.core.utility.tunable.LoggedTunableMeasure; +import java.util.concurrent.locks.ReentrantLock; +import lombok.Builder; +import lombok.NonNull; + +@Builder(setterPrefix = "with") +public class SwerveDriveConstants { + @Builder.Default public final ReentrantLock reentrantLock = new ReentrantLock(); + + @NonNull public final DriveConfig driveConfig; + + @NonNull public final Gains driveGains; + @NonNull public final Gains turnGains; + + @NonNull public final Gains autoTranslationGains; + @NonNull public final Gains autoRotationGains; + + @NonNull public final AutoAlignConstants autoAlignConstants; + + @NonNull public final Double odometryFrequency; + @NonNull public final Double driverDeadband; + @NonNull public final Double operatorDeadband; + + @Builder(setterPrefix = "with") + public record DriveConfig( + @NonNull CANBus canBus, + @NonNull Integer pigeon2Id, + @NonNull Double maxLinearVelocityMetersPerSecond, + @NonNull Double wheelRadiusMeters, + @NonNull DCMotor driveModel, + @NonNull DCMotor turnModel, + @NonNull + SwerveModuleConstants + frontLeft, + @NonNull + SwerveModuleConstants + frontRight, + @NonNull + SwerveModuleConstants + backLeft, + @NonNull + SwerveModuleConstants + backRight, + @NonNull SwerveModuleConstants.ClosedLoopOutputType driveClosedLoopOutputType, + @NonNull SwerveModuleConstants.ClosedLoopOutputType steerClosedLoopOutputType, + @NonNull Double bumperWidth, + @NonNull Double bumperLength, + @NonNull Double robotMassKilograms, + @NonNull Double trackWidth, + @NonNull Double robotMOI, + @NonNull Double moduleCurrentLimit, + @NonNull Double wheelCOF) { + public Double driveBaseRadius() { + return Math.hypot( + (Math.abs(frontLeft.LocationX) + Math.abs(frontRight.LocationX)) / 2.0, + (Math.abs(frontLeft.LocationY) + Math.abs(backLeft.LocationY)) / 2.0); + } + + public Double maxAngularVelocity() { + return maxLinearVelocityMetersPerSecond / driveBaseRadius(); + } + + public Translation2d[] getModuleTranslations() { + return new Translation2d[] { + new Translation2d(frontLeft.LocationX, frontLeft.LocationY), + new Translation2d(frontRight.LocationX, frontRight.LocationY), + new Translation2d(backLeft.LocationX, backLeft.LocationY), + new Translation2d(backRight.LocationX, backRight.LocationY) + }; + } + + public SwerveDriveKinematics kinematics() { + return new SwerveDriveKinematics(getModuleTranslations()); + } + } + + @Builder(setterPrefix = "with") + public record AutoAlignConstants( + @NonNull Gains xGains, + @NonNull LinearConstraints xConstraints, + @NonNull Gains yGains, + @NonNull LinearConstraints yConstraints, + @NonNull Gains rotationGains, + @NonNull AngularPositionConstraints rotationConstraints, + @NonNull LoggedTunableMeasure linearThreshold, + @NonNull LoggedTunableMeasure angularThreshold) {} +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModule.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModule.java new file mode 100644 index 00000000..955d17e1 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModule.java @@ -0,0 +1,169 @@ +package edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive; + +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.kinematics.SwerveModulePosition; +import edu.wpi.first.math.kinematics.SwerveModuleState; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.wpilibj.Alert; +import edu.wpi.first.wpilibj.Alert.AlertType; +import edu.wpi.team190.gompeilib.core.logging.Trace; +import lombok.Getter; +import org.littletonrobotics.junction.Logger; + +public class SwerveModule { + private final SwerveDriveConstants driveConstants; + private final SwerveModuleIO io; + private final ModuleIOInputsAutoLogged inputs = new ModuleIOInputsAutoLogged(); + private final int index; + + private final Alert driveDisconnectedAlert; + private final Alert turnDisconnectedAlert; + private final Alert turnEncoderDisconnectedAlert; + @Getter private SwerveModulePosition[] odometryPositions = new SwerveModulePosition[] {}; + + public SwerveModule(SwerveDriveConstants driveConstants, SwerveModuleIO io, int index) { + this.driveConstants = driveConstants; + this.io = io; + this.index = index; + driveDisconnectedAlert = + new Alert( + "Disconnected drive motor on module " + Integer.toString(index) + ".", + AlertType.kError); + turnDisconnectedAlert = + new Alert( + "Disconnected turn motor on module " + Integer.toString(index) + ".", AlertType.kError); + turnEncoderDisconnectedAlert = + new Alert( + "Disconnected turn encoder on module " + Integer.toString(index) + ".", + AlertType.kError); + } + + @Trace + public void updateInputs() { + io.updateInputs(inputs); + Logger.processInputs("Drive/Module" + Integer.toString(index), inputs); + } + + @Trace + public void periodic() { + // Calculate positions for odometry + int sampleCount = inputs.odometryTimestamps.length; // All signals are sampled together + odometryPositions = new SwerveModulePosition[sampleCount]; + for (int i = 0; i < sampleCount; i++) { + double positionMeters = + inputs.odometryDrivePositionsRadians[i] * driveConstants.driveConfig.wheelRadiusMeters(); + Rotation2d angle = inputs.odometryTurnPositions[i]; + odometryPositions[i] = new SwerveModulePosition(positionMeters, angle); + } + + // Update alerts + driveDisconnectedAlert.set(!inputs.driveConnected); + turnDisconnectedAlert.set(!inputs.turnConnected); + turnEncoderDisconnectedAlert.set(!inputs.turnEncoderConnected); + } + + /** Runs the module with the specified setpoint state. Mutates the state to optimize it. */ + @Trace + public void runSetpoint(SwerveModuleState state, SwerveModuleState torqueFeedforward) { + // Optimize veloci[ty setpoint + state.optimize(getAngle()); + state.cosineScale(inputs.turnPosition); + + double wheelTorqueNewtonMeters = torqueFeedforward.speedMetersPerSecond; + // Apply setpoints + io.setDriveVelocity( + state.speedMetersPerSecond / driveConstants.driveConfig.wheelRadiusMeters(), + driveConstants + .driveConfig + .driveModel() + .getCurrent( + wheelTorqueNewtonMeters + / driveConstants.driveConfig.frontLeft().DriveMotorGearRatio)); + io.setTurnPosition(state.angle); + } + + /** Runs the module with the specified output while controlling to zero degrees. */ + @Trace + public void runCharacterization(double amps) { + io.setDriveAmps(amps); + io.setTurnPosition(new Rotation2d()); + } + + /** Disables all outputs to motors. */ + @Trace + public void stop() { + io.setDriveAmps(0.0); + io.setTurnAmps(0.0); + } + + /** Returns the current turn angle of the module. */ + @Trace + public Rotation2d getAngle() { + return inputs.turnPosition; + } + + /** Returns the current drive position of the module in meters. */ + @Trace + public double getPositionMeters() { + return inputs.drivePositionRadians * driveConstants.driveConfig.wheelRadiusMeters(); + } + + /** Returns the current drive velocity of the module in meters per second. */ + @Trace + public double getVelocityMetersPerSec() { + return inputs.driveVelocityRadiansPerSecond * driveConstants.driveConfig.wheelRadiusMeters(); + } + + /** Returns the module position (turn angle and drive position). */ + @Trace + public SwerveModulePosition getPosition() { + return new SwerveModulePosition(getPositionMeters(), getAngle()); + } + + /** Returns the module state (turn angle and drive velocity). */ + @Trace + public SwerveModuleState getState() { + return new SwerveModuleState(getVelocityMetersPerSec(), getAngle()); + } + + /** Returns the timestamps of the samples received this cycle. */ + @Trace + public double[] getOdometryTimestamps() { + return inputs.odometryTimestamps; + } + + /** Returns the module position in radians. */ + @Trace + public double getWheelRadiusCharacterizationPosition() { + return inputs.drivePositionRadians; + } + + /** Returns the module velocity in rotations/sec (Phoenix native units). */ + @Trace + public double getFFCharacterizationVelocity() { + return Units.radiansToRotations(inputs.driveVelocityRadiansPerSecond); + } + + /** Sets module PID gains */ + @Trace + public void setPID(double drive_Kp, double drive_Kd, double turn_Kp, double turn_Kd) { + io.setPID(drive_Kp, drive_Kd, turn_Kp, turn_Kd); + } + + /** Sets module FF gains */ + @Trace + public void setFF(double kS, double kV) { + io.setFeedforward(kS, kV); + } + + /** + * Updates current limits. + * + * @param driveCurrentLimit The drive current limit. + * @param turnCurrentLimit The turn current limit. + */ + @Trace + public void updateCurrentLimits(double driveCurrentLimit, double turnCurrentLimit) { + io.updateCurrentLimits(driveCurrentLimit, turnCurrentLimit); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIO.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIO.java new file mode 100644 index 00000000..90c99ce4 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIO.java @@ -0,0 +1,67 @@ +package edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive; + +import edu.wpi.first.math.geometry.Rotation2d; +import org.littletonrobotics.junction.AutoLog; + +public interface SwerveModuleIO { + @AutoLog + public static class ModuleIOInputs { + public double drivePositionRadians = 0.0; + public double driveVelocityRadiansPerSecond = 0.0; + public double driveAppliedVolts = 0.0; + public double driveSupplyCurrentAmps = 0.0; + public double driveTorqueCurrentAmps = 0.0; + public double driveTemperatureCelcius = 0.0; + public double driveVelocitySetpointRadiansPerSecond = 0.0; + public double driveVelocityErrorRadiansPerSecond = 0.0; + + public Rotation2d turnAbsolutePosition = new Rotation2d(); + public Rotation2d turnPosition = new Rotation2d(); + public double turnVelocityRadiansPerSecond = 0.0; + public double turnAppliedVolts = 0.0; + public double turnSupplyCurrentAmps = 0.0; + public double turnTorqueCurrentAmps = 0.0; + public double turnTemperatureCelcius = 0.0; + public Rotation2d turnPositionGoal = new Rotation2d(); + public Rotation2d turnPositionSetpoint = new Rotation2d(); + public Rotation2d turnPositionError = new Rotation2d(); + + public boolean driveConnected = false; + public boolean turnConnected = false; + public boolean turnEncoderConnected = false; + + public double[] odometryTimestamps = new double[] {}; + public double[] odometryDrivePositionsRadians = 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 open loop value. */ + public default void setDriveAmps(double currentAmps) {} + + /** Run the turn motor at the specified open loop value. */ + public default void setTurnAmps(double currentAmps) {} + + /** Run the drive motor at the specified velocity. */ + public default void setDriveVelocity( + double velocityRadiansPerSecond, double currentFeedforward) {} + + /** Run the turn motor to the specified rotation. */ + public default void setTurnPosition(Rotation2d position) {} + + /** Sets the module PID gains */ + public default void setPID(double drive_Kp, double drive_Kd, double turn_Kp, double turn_Kd) {} + + /** Sets the module FF gains */ + public default void setFeedforward(double drive_Ks, double drive_Kv) {} + + /** + * Updates current limits. + * + * @param driveCurrentLimit The drive current limit. + * @param turnCurrentLimit The turn current limit. + */ + public default void updateCurrentLimits(double driveCurrentLimit, double turnCurrentLimit) {} +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIOSim.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIOSim.java new file mode 100644 index 00000000..19bc8c72 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIOSim.java @@ -0,0 +1,149 @@ +package edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive; + +import com.ctre.phoenix6.configs.CANcoderConfiguration; +import com.ctre.phoenix6.configs.TalonFXConfiguration; +import com.ctre.phoenix6.swerve.SwerveModuleConstants; +import edu.wpi.first.math.MathUtil; +import edu.wpi.first.math.controller.PIDController; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.system.plant.LinearSystemId; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.wpilibj.Timer; +import edu.wpi.first.wpilibj.simulation.DCMotorSim; +import edu.wpi.team190.gompeilib.core.GompeiLib; + +/** + * Physics sim implementation of module IO. Simulation is not vendor-specific, but the sim models + * are configured using a set of module constants from Phoenix. + * + *

Simulation is always based on voltage control. + */ +public class SwerveModuleIOSim implements SwerveModuleIO { + private static final double DRIVE_KP = 0.05; + private static final double DRIVE_KD = 0.0; + private static final double DRIVE_KS = 0.0; + private static final double DRIVE_KV_ROT = + 0.91035; // Same units as TunerConstants: (volt * secs) / rotation + private static final double DRIVE_KV = 1.0 / Units.rotationsToRadians(1.0 / DRIVE_KV_ROT); + private static final double TURN_KP = 8.0; + private static final double TURN_KD = 0.0; + + private final DCMotorSim driveSim; + private final DCMotorSim turnSim; + + private boolean driveClosedLoop; + private boolean turnClosedLoop; + + private final PIDController driveController; + private final PIDController turnController; + + private double driveFFVolts; + private double driveAppliedVolts; + private double turnAppliedVolts; + + public SwerveModuleIOSim( + SwerveDriveConstants driveConstants, + SwerveModuleConstants + constants) { + // Create drive and turn sim models + driveSim = + new DCMotorSim( + LinearSystemId.createDCMotorSystem( + driveConstants.driveConfig.driveModel(), + constants.DriveInertia, + constants.DriveMotorGearRatio), + driveConstants.driveConfig.driveModel()); + turnSim = + new DCMotorSim( + LinearSystemId.createDCMotorSystem( + driveConstants.driveConfig.turnModel(), + constants.SteerInertia, + constants.SteerMotorGearRatio), + driveConstants.driveConfig.turnModel()); + + driveClosedLoop = false; + turnClosedLoop = false; + + driveController = new PIDController(DRIVE_KP, 0, DRIVE_KD); + turnController = new PIDController(TURN_KP, 0, TURN_KD); + + driveFFVolts = 0.0; + driveAppliedVolts = 0.0; + turnAppliedVolts = 0.0; + + // Enable wrapping for turn PID + turnController.enableContinuousInput(-Math.PI, Math.PI); + } + + @Override + public void updateInputs(ModuleIOInputs inputs) { + // Run closed-loop control + if (driveClosedLoop) { + driveAppliedVolts = + driveFFVolts + driveController.calculate(driveSim.getAngularVelocityRadPerSec()); + } else { + driveController.reset(); + } + if (turnClosedLoop) { + turnAppliedVolts = turnController.calculate(turnSim.getAngularPositionRad()); + } else { + turnController.reset(); + } + + // Update simulation state + driveSim.setInputVoltage(MathUtil.clamp(driveAppliedVolts, -12.0, 12.0)); + turnSim.setInputVoltage(MathUtil.clamp(turnAppliedVolts, -12.0, 12.0)); + driveSim.update(GompeiLib.getLoopPeriod()); + turnSim.update(GompeiLib.getLoopPeriod()); + + inputs.drivePositionRadians = driveSim.getAngularPositionRad(); + inputs.driveVelocityRadiansPerSecond = driveSim.getAngularVelocityRadPerSec(); + inputs.driveAppliedVolts = driveAppliedVolts; + inputs.driveSupplyCurrentAmps = Math.abs(driveSim.getCurrentDrawAmps()); + inputs.driveVelocitySetpointRadiansPerSecond = driveController.getSetpoint(); + inputs.driveVelocityErrorRadiansPerSecond = driveController.getError(); + + inputs.turnAbsolutePosition = new Rotation2d(turnSim.getAngularPositionRad()); + inputs.turnPosition = new Rotation2d(turnSim.getAngularPositionRad()); + inputs.turnVelocityRadiansPerSecond = turnSim.getAngularVelocityRadPerSec(); + inputs.turnAppliedVolts = turnAppliedVolts; + inputs.turnSupplyCurrentAmps = Math.abs(turnSim.getCurrentDrawAmps()); + inputs.turnPositionGoal = Rotation2d.fromRadians(turnController.getSetpoint()); + inputs.turnPositionSetpoint = Rotation2d.fromRadians(turnController.getSetpoint()); + inputs.turnPositionError = Rotation2d.fromRadians(turnController.getError()); + + inputs.driveConnected = true; + inputs.turnConnected = true; + inputs.turnEncoderConnected = true; + + inputs.odometryTimestamps = new double[] {Timer.getTimestamp()}; + inputs.odometryDrivePositionsRadians = new double[] {inputs.drivePositionRadians}; + inputs.odometryTurnPositions = new Rotation2d[] {inputs.turnPosition}; + } + + @Override + public void setDriveAmps(double currentAmps) { + driveClosedLoop = false; + driveAppliedVolts = currentAmps; + } + + @Override + public void setTurnAmps(double currentAmps) { + turnClosedLoop = false; + turnAppliedVolts = currentAmps; + } + + @Override + public void setDriveVelocity(double velocityRadiansPerSecond, double currentFeedforward) { + driveClosedLoop = true; + driveFFVolts = + DRIVE_KS * Math.signum(velocityRadiansPerSecond) + DRIVE_KV * velocityRadiansPerSecond; + driveController.setSetpoint(velocityRadiansPerSecond); + } + + @Override + public void setTurnPosition(Rotation2d position) { + turnClosedLoop = true; + turnController.setSetpoint(position.getRadians()); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIOTalonFX.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIOTalonFX.java new file mode 100644 index 00000000..55c89eec --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIOTalonFX.java @@ -0,0 +1,330 @@ +package edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive; + +import static edu.wpi.team190.gompeilib.core.utility.phoenix.PhoenixUtil.tryUntilOk; + +import com.ctre.phoenix6.BaseStatusSignal; +import com.ctre.phoenix6.StatusSignal; +import com.ctre.phoenix6.configs.CANcoderConfiguration; +import com.ctre.phoenix6.configs.TalonFXConfiguration; +import com.ctre.phoenix6.controls.*; +import com.ctre.phoenix6.hardware.CANcoder; +import com.ctre.phoenix6.hardware.TalonFX; +import com.ctre.phoenix6.signals.FeedbackSensorSourceValue; +import com.ctre.phoenix6.signals.InvertedValue; +import com.ctre.phoenix6.signals.NeutralModeValue; +import com.ctre.phoenix6.swerve.SwerveModuleConstants; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.units.measure.Angle; +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.units.measure.Current; +import edu.wpi.first.units.measure.Temperature; +import edu.wpi.first.units.measure.Voltage; +import edu.wpi.team190.gompeilib.core.GompeiLib; +import edu.wpi.team190.gompeilib.core.logging.Trace; +import edu.wpi.team190.gompeilib.core.utility.phoenix.PhoenixOdometryThread; +import edu.wpi.team190.gompeilib.core.utility.phoenix.PhoenixUtil; +import java.util.Queue; + +/** + * Module IO implementation for Talon FX drive motor controller, Talon FX turn motor controller, and + * CANcoder. Configured using a set of module constants from Phoenix. + * + *

Device configuration and other behaviors not exposed by TunerConstants can be customized here. + */ +public class SwerveModuleIOTalonFX implements SwerveModuleIO { + protected final TalonFX driveTalonFX; + protected final TalonFX turnTalonFX; + protected final CANcoder cancoder; + + private final TalonFXConfiguration driveConfig; + private final TalonFXConfiguration turnConfig; + private final CANcoderConfiguration cancoderConfig; + + private final StatusSignal drivePositionRotations; + private final StatusSignal driveVelocityRotationsPerSecond; + private final StatusSignal driveAppliedVolts; + private final StatusSignal driveSupplyCurrentAmps; + private final StatusSignal driveTorqueCurrentAmps; + private final StatusSignal driveTemperatureCelcius; + private final StatusSignal driveVelocitySetpointRotationsPerSecond; + private final StatusSignal driveVelocityErrorRotationsPerSecond; + + private final StatusSignal turnAbsolutePositionRotations; + private final StatusSignal turnPositionRotations; + private final StatusSignal turnVelocityRotationsPerSecond; + private final StatusSignal turnAppliedVolts; + private final StatusSignal turnSupplyCurrentAmps; + private final StatusSignal turnTorqueCurrentAmps; + private final StatusSignal turnTemperatureCelcius; + private Rotation2d turnPositionGoal; + private final StatusSignal turnPositionSetpointRotations; + private final StatusSignal turnPositionErrorRotations; + + private final Queue timestampQueue; + private final Queue drivePositionQueue; + private final Queue turnPositionQueue; + + private final TorqueCurrentFOC torqueCurrentRequest; + private final VelocityTorqueCurrentFOC velocityTorqueCurrentRequest; + private final VelocityVoltage velocityVoltageRequest; + private final MotionMagicTorqueCurrentFOC positionTorqueCurrentRequest; + private final MotionMagicVoltage positionVoltageRequest; + + private final SwerveModuleConstants.ClosedLoopOutputType driveClosedLoopOutputType; + private final SwerveModuleConstants.ClosedLoopOutputType turnClosedLoopOutputType; + + public SwerveModuleIOTalonFX( + SwerveDriveConstants driveConstants, + SwerveModuleConstants + constants) { + driveTalonFX = new TalonFX(constants.DriveMotorId, driveConstants.driveConfig.canBus()); + turnTalonFX = new TalonFX(constants.SteerMotorId, driveConstants.driveConfig.canBus()); + cancoder = new CANcoder(constants.EncoderId, driveConstants.driveConfig.canBus()); + + driveConfig = new TalonFXConfiguration(); + driveConfig.MotorOutput.NeutralMode = NeutralModeValue.Brake; + driveConfig.Slot0 = constants.DriveMotorGains; + driveConfig.Feedback.SensorToMechanismRatio = constants.DriveMotorGearRatio; + driveConfig.TorqueCurrent.PeakForwardTorqueCurrent = constants.SlipCurrent; + driveConfig.TorqueCurrent.PeakReverseTorqueCurrent = -constants.SlipCurrent; + driveConfig.ClosedLoopRamps.TorqueClosedLoopRampPeriod = GompeiLib.getLoopPeriod(); + driveConfig.CurrentLimits.StatorCurrentLimit = constants.SlipCurrent; + driveConfig.CurrentLimits.StatorCurrentLimitEnable = true; + driveConfig.MotorOutput.Inverted = + constants.DriveMotorInverted + ? InvertedValue.Clockwise_Positive + : InvertedValue.CounterClockwise_Positive; + tryUntilOk(5, () -> driveTalonFX.getConfigurator().apply(driveConfig, 0.25)); + tryUntilOk(5, () -> driveTalonFX.setPosition(0.0, 0.25)); + + turnConfig = new TalonFXConfiguration(); + turnConfig.MotorOutput.NeutralMode = NeutralModeValue.Brake; + turnConfig.Slot0 = constants.SteerMotorGains; + turnConfig.Feedback.FeedbackRemoteSensorID = constants.EncoderId; + turnConfig.Feedback.FeedbackSensorSource = + switch (constants.FeedbackSource) { + case RemoteCANcoder -> FeedbackSensorSourceValue.RemoteCANcoder; + case FusedCANcoder -> FeedbackSensorSourceValue.FusedCANcoder; + case SyncCANcoder -> FeedbackSensorSourceValue.SyncCANcoder; + default -> + throw new IllegalArgumentException("Unexpected value: " + constants.FeedbackSource); + }; + turnConfig.Feedback.RotorToSensorRatio = constants.SteerMotorGearRatio; + turnConfig.TorqueCurrent.PeakForwardTorqueCurrent = 40.0; + turnConfig.TorqueCurrent.PeakReverseTorqueCurrent = -40.0; + turnConfig.ClosedLoopRamps.TorqueClosedLoopRampPeriod = GompeiLib.getLoopPeriod(); + turnConfig.CurrentLimits.StatorCurrentLimit = 40.0; + turnConfig.CurrentLimits.StatorCurrentLimitEnable = true; + turnConfig.MotionMagic.MotionMagicCruiseVelocity = 100.0 / constants.SteerMotorGearRatio; + turnConfig.MotionMagic.MotionMagicAcceleration = + turnConfig.MotionMagic.MotionMagicCruiseVelocity / 0.100; + turnConfig.MotionMagic.MotionMagicExpo_kV = 0.12 * constants.SteerMotorGearRatio; + turnConfig.MotionMagic.MotionMagicExpo_kA = 0.1; + turnConfig.ClosedLoopGeneral.ContinuousWrap = true; + turnConfig.MotorOutput.Inverted = + constants.SteerMotorInverted + ? InvertedValue.Clockwise_Positive + : InvertedValue.CounterClockwise_Positive; + tryUntilOk(5, () -> turnTalonFX.getConfigurator().apply(turnConfig, 0.25)); + + cancoderConfig = constants.EncoderInitialConfigs; + cancoderConfig.MagnetSensor.MagnetOffset = constants.EncoderOffset; + tryUntilOk(5, () -> cancoder.getConfigurator().apply(cancoderConfig, 0.25)); + + drivePositionRotations = driveTalonFX.getPosition(); + driveVelocityRotationsPerSecond = driveTalonFX.getVelocity(); + driveAppliedVolts = driveTalonFX.getMotorVoltage(); + driveSupplyCurrentAmps = driveTalonFX.getSupplyCurrent(); + driveTorqueCurrentAmps = driveTalonFX.getTorqueCurrent(); + driveTemperatureCelcius = driveTalonFX.getDeviceTemp(); + driveVelocitySetpointRotationsPerSecond = driveTalonFX.getClosedLoopReference(); + driveVelocityErrorRotationsPerSecond = driveTalonFX.getClosedLoopError(); + + turnAbsolutePositionRotations = cancoder.getAbsolutePosition(); + turnPositionRotations = turnTalonFX.getPosition(); + turnVelocityRotationsPerSecond = turnTalonFX.getVelocity(); + turnAppliedVolts = turnTalonFX.getMotorVoltage(); + turnSupplyCurrentAmps = turnTalonFX.getSupplyCurrent(); + turnTorqueCurrentAmps = turnTalonFX.getTorqueCurrent(); + turnTemperatureCelcius = turnTalonFX.getDeviceTemp(); + turnPositionGoal = new Rotation2d(); + turnPositionSetpointRotations = turnTalonFX.getClosedLoopReference(); + turnPositionErrorRotations = turnTalonFX.getClosedLoopError(); + + timestampQueue = PhoenixOdometryThread.getInstance(driveConstants).makeTimestampQueue(); + drivePositionQueue = + PhoenixOdometryThread.getInstance(driveConstants) + .registerSignal(driveTalonFX.getPosition()); + turnPositionQueue = + PhoenixOdometryThread.getInstance(driveConstants).registerSignal(turnTalonFX.getPosition()); + + torqueCurrentRequest = new TorqueCurrentFOC(0.0); + velocityTorqueCurrentRequest = new VelocityTorqueCurrentFOC(0.0); + velocityVoltageRequest = new VelocityVoltage(0.0); + positionTorqueCurrentRequest = new MotionMagicTorqueCurrentFOC(0.0); + positionVoltageRequest = new MotionMagicVoltage(0.0); + + driveClosedLoopOutputType = constants.DriveMotorClosedLoopOutput; + turnClosedLoopOutputType = constants.SteerMotorClosedLoopOutput; + + // Configure periodic frames + BaseStatusSignal.setUpdateFrequencyForAll( + driveConstants.odometryFrequency, + drivePositionRotations, + turnPositionRotations, + turnAbsolutePositionRotations); + BaseStatusSignal.setUpdateFrequencyForAll( + 50.0, + driveVelocityRotationsPerSecond, + driveAppliedVolts, + driveSupplyCurrentAmps, + driveTorqueCurrentAmps, + driveTemperatureCelcius, + driveVelocitySetpointRotationsPerSecond, + driveVelocityErrorRotationsPerSecond, + turnVelocityRotationsPerSecond, + turnAppliedVolts, + turnSupplyCurrentAmps, + turnTorqueCurrentAmps, + turnTemperatureCelcius, + turnPositionSetpointRotations, + turnPositionErrorRotations); + + driveTalonFX.optimizeBusUtilization(); + turnTalonFX.optimizeBusUtilization(); + cancoder.optimizeBusUtilization(); + + PhoenixUtil.registerSignals( + true, + drivePositionRotations, + turnPositionRotations, + turnAbsolutePositionRotations, + driveVelocityRotationsPerSecond, + driveAppliedVolts, + driveSupplyCurrentAmps, + driveTorqueCurrentAmps, + driveTemperatureCelcius, + driveVelocitySetpointRotationsPerSecond, + driveVelocityErrorRotationsPerSecond, + turnVelocityRotationsPerSecond, + turnAppliedVolts, + turnSupplyCurrentAmps, + turnTorqueCurrentAmps, + turnTemperatureCelcius, + turnPositionSetpointRotations, + turnPositionErrorRotations); + } + + @Override + @Trace + public void updateInputs(ModuleIOInputs inputs) { + inputs.drivePositionRadians = + Units.rotationsToRadians(drivePositionRotations.getValueAsDouble()); + inputs.driveVelocityRadiansPerSecond = + Units.rotationsToRadians(driveVelocityRotationsPerSecond.getValueAsDouble()); + inputs.driveAppliedVolts = driveAppliedVolts.getValueAsDouble(); + inputs.driveSupplyCurrentAmps = driveSupplyCurrentAmps.getValueAsDouble(); + inputs.driveTorqueCurrentAmps = driveTorqueCurrentAmps.getValueAsDouble(); + inputs.driveTemperatureCelcius = driveTemperatureCelcius.getValueAsDouble(); + inputs.driveVelocitySetpointRadiansPerSecond = + Units.rotationsToRadians(driveVelocitySetpointRotationsPerSecond.getValueAsDouble()); + inputs.driveVelocityErrorRadiansPerSecond = + Units.rotationsToRadians(driveVelocityErrorRotationsPerSecond.getValueAsDouble()); + + inputs.turnAbsolutePosition = + Rotation2d.fromRotations(turnAbsolutePositionRotations.getValueAsDouble()); + inputs.turnPosition = Rotation2d.fromRotations(turnPositionRotations.getValueAsDouble()); + inputs.turnVelocityRadiansPerSecond = + Units.rotationsToRadians(turnVelocityRotationsPerSecond.getValueAsDouble()); + inputs.turnAppliedVolts = turnAppliedVolts.getValueAsDouble(); + inputs.turnSupplyCurrentAmps = turnSupplyCurrentAmps.getValueAsDouble(); + inputs.turnTorqueCurrentAmps = turnTorqueCurrentAmps.getValueAsDouble(); + inputs.turnTemperatureCelcius = turnTemperatureCelcius.getValueAsDouble(); + inputs.turnPositionGoal = turnPositionGoal; + inputs.turnPositionSetpoint = + Rotation2d.fromRotations(turnPositionSetpointRotations.getValueAsDouble()); + inputs.turnPositionError = + Rotation2d.fromRotations(turnPositionErrorRotations.getValueAsDouble()); + + inputs.odometryTimestamps = + timestampQueue.stream().mapToDouble((Double value) -> value).toArray(); + inputs.odometryDrivePositionsRadians = + drivePositionQueue.stream().mapToDouble(Units::rotationsToRadians).toArray(); + inputs.odometryTurnPositions = + turnPositionQueue.stream().map(Rotation2d::fromRotations).toArray(Rotation2d[]::new); + + timestampQueue.clear(); + drivePositionQueue.clear(); + turnPositionQueue.clear(); + } + + @Override + @Trace + public void setDriveAmps(double currentAmps) { + driveTalonFX.setControl(torqueCurrentRequest.withOutput(currentAmps)); + } + + @Override + @Trace + public void setTurnAmps(double currentAmps) { + turnTalonFX.setControl(torqueCurrentRequest.withOutput(currentAmps)); + } + + @Override + @Trace + public void setDriveVelocity(double velocityRadiansPerSecond, double currentFeedforward) { + if (driveClosedLoopOutputType.equals(SwerveModuleConstants.ClosedLoopOutputType.Voltage)) { + driveTalonFX.setControl( + velocityVoltageRequest + .withVelocity(Units.radiansToRotations(velocityRadiansPerSecond)) + .withFeedForward(currentFeedforward)); + } else if (driveClosedLoopOutputType.equals( + SwerveModuleConstants.ClosedLoopOutputType.TorqueCurrentFOC)) { + driveTalonFX.setControl( + velocityTorqueCurrentRequest + .withVelocity(Units.radiansToRotations(velocityRadiansPerSecond)) + .withFeedForward(currentFeedforward)); + } + } + + @Override + @Trace + public void setTurnPosition(Rotation2d rotation) { + turnPositionGoal = rotation; + if (turnClosedLoopOutputType.equals(SwerveModuleConstants.ClosedLoopOutputType.Voltage)) + turnTalonFX.setControl(positionVoltageRequest.withPosition(rotation.getRotations())); + if (turnClosedLoopOutputType.equals( + SwerveModuleConstants.ClosedLoopOutputType.TorqueCurrentFOC)) + turnTalonFX.setControl(positionTorqueCurrentRequest.withPosition(rotation.getRotations())); + } + + @Override + @Trace + public void setPID(double drive_Kp, double drive_Kd, double turn_Kp, double turn_Kd) { + driveConfig.Slot0.kP = drive_Kp; + driveConfig.Slot0.kD = drive_Kd; + turnConfig.Slot0.kP = turn_Kp; + turnConfig.Slot0.kD = turn_Kd; + tryUntilOk(5, () -> driveTalonFX.getConfigurator().apply(driveConfig, 0.25)); + tryUntilOk(5, () -> turnTalonFX.getConfigurator().apply(turnConfig, 0.25)); + } + + @Override + @Trace + public void setFeedforward(double drive_Ks, double drive_Kv) { + driveConfig.Slot0.kS = drive_Ks; + driveConfig.Slot0.kV = drive_Kv; + tryUntilOk(5, () -> driveTalonFX.getConfigurator().apply(driveConfig, 0.25)); + tryUntilOk(5, () -> turnTalonFX.getConfigurator().apply(turnConfig, 0.25)); + } + + @Override + @Trace + public void updateCurrentLimits(double driveCurrentLimit, double turnCurrentLimit) { + driveConfig.CurrentLimits.StatorCurrentLimit = driveCurrentLimit; + turnConfig.CurrentLimits.StatorCurrentLimit = turnCurrentLimit; + tryUntilOk(5, () -> driveTalonFX.getConfigurator().apply(driveConfig, 0.25)); + tryUntilOk(5, () -> turnTalonFX.getConfigurator().apply(turnConfig, 0.25)); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIOTalonFXSim.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIOTalonFXSim.java new file mode 100644 index 00000000..b112b0af --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIOTalonFXSim.java @@ -0,0 +1,87 @@ +package edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive; + +import com.ctre.phoenix6.configs.CANcoderConfiguration; +import com.ctre.phoenix6.configs.TalonFXConfiguration; +import com.ctre.phoenix6.sim.CANcoderSimState; +import com.ctre.phoenix6.sim.TalonFXSimState; +import com.ctre.phoenix6.swerve.SwerveModuleConstants; +import edu.wpi.first.math.system.plant.LinearSystemId; +import edu.wpi.first.wpilibj.RobotController; +import edu.wpi.first.wpilibj.simulation.DCMotorSim; +import edu.wpi.team190.gompeilib.core.GompeiLib; +import edu.wpi.team190.gompeilib.core.logging.Trace; + +public class SwerveModuleIOTalonFXSim extends SwerveModuleIOTalonFX { + + private final DCMotorSim steerMotorSim; + private final DCMotorSim driveMotorSim; + + private final TalonFXSimState steerController; + private final TalonFXSimState driveController; + private final CANcoderSimState encoderController; + private final double offset; + + public SwerveModuleIOTalonFXSim( + SwerveDriveConstants driveConstants, + SwerveModuleConstants + constants) { + + super(driveConstants, constants); + driveMotorSim = + new DCMotorSim( + LinearSystemId.createDCMotorSystem( + driveConstants.driveConfig.driveModel(), + constants.DriveInertia, + constants.DriveMotorGearRatio), + driveConstants.driveConfig.driveModel()); + steerMotorSim = + new DCMotorSim( + LinearSystemId.createDCMotorSystem( + driveConstants.driveConfig.turnModel(), + constants.SteerInertia, + constants.SteerMotorGearRatio), + driveConstants.driveConfig.turnModel()); + + steerController = super.turnTalonFX.getSimState(); + driveController = super.driveTalonFX.getSimState(); + encoderController = super.cancoder.getSimState(); + + offset = constants.EncoderOffset; + } + + @Override + @Trace + public void updateInputs(ModuleIOInputs inputs) { + driveController.setSupplyVoltage(RobotController.getBatteryVoltage()); + double motorVoltageDrive = driveController.getMotorVoltage(); + + driveMotorSim.setInputVoltage(motorVoltageDrive); + + driveMotorSim.update(GompeiLib.getLoopPeriod()); + + double rotorPositionRotationsDrive = + driveMotorSim.getAngularPositionRotations() * driveMotorSim.getGearing(); + double rotorVelocityRotationsPerSecondDrive = + driveMotorSim.getAngularVelocityRadPerSec() / (Math.PI * 2) * driveMotorSim.getGearing(); + driveController.setRawRotorPosition(rotorPositionRotationsDrive); + driveController.setRotorVelocity(rotorVelocityRotationsPerSecondDrive); + + steerController.setSupplyVoltage(RobotController.getBatteryVoltage()); + double motorVoltageSteer = steerController.getMotorVoltage(); + + steerMotorSim.setInputVoltage(motorVoltageSteer); + + steerMotorSim.update(GompeiLib.getLoopPeriod()); + + double rotorPositionRotationsSteer = + steerMotorSim.getAngularPositionRotations() * steerMotorSim.getGearing(); + double rotorVelocityRotationsPerSecondSteer = + steerMotorSim.getAngularVelocityRadPerSec() / (Math.PI * 2) * steerMotorSim.getGearing(); + steerController.setRawRotorPosition(rotorPositionRotationsSteer); + steerController.setRotorVelocity(rotorVelocityRotationsPerSecondSteer); + + encoderController.setRawPosition(steerMotorSim.getAngularPositionRotations() + offset); + + super.updateInputs(inputs); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/elevator/Elevator.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/elevator/Elevator.java new file mode 100644 index 00000000..68db7016 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/elevator/Elevator.java @@ -0,0 +1,186 @@ +package edu.wpi.team190.gompeilib.subsystems.elevator; + +import static edu.wpi.first.units.Units.*; + +import edu.wpi.first.units.DistanceUnit; +import edu.wpi.first.units.VoltageUnit; +import edu.wpi.first.units.measure.Distance; +import edu.wpi.first.units.measure.Voltage; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; +import edu.wpi.first.wpilibj2.command.Subsystem; +import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine; +import edu.wpi.team190.gompeilib.core.logging.Trace; +import edu.wpi.team190.gompeilib.core.utility.Setpoint; +import edu.wpi.team190.gompeilib.core.utility.control.Gains; +import edu.wpi.team190.gompeilib.core.utility.control.constraints.LinearConstraints; +import edu.wpi.team190.gompeilib.core.utility.phoenix.GainSlot; +import org.littletonrobotics.junction.Logger; + +public class Elevator { + public final ElevatorIO io; + public final ElevatorIOInputsAutoLogged inputs; + + private final String aKitTopic; + + private ElevatorState currentState; + + private Setpoint voltageGoal; + private Setpoint positionGoal; + + private final SysIdRoutine characterizationRoutine; + + public final ElevatorConstants constants; + + public Elevator( + ElevatorConstants constants, + Subsystem subsystem, + int index, + ElevatorIO io, + Setpoint positionGoal, + Setpoint voltageGoal) { + this.io = io; + this.inputs = new ElevatorIOInputsAutoLogged(); + + aKitTopic = subsystem.getName() + "/Elevators" + index; + + currentState = ElevatorState.IDLE; + + this.positionGoal = positionGoal; + this.voltageGoal = voltageGoal; + + characterizationRoutine = + new SysIdRoutine( + new SysIdRoutine.Config( + Volts.of(1).per(Second), + Volts.of(3), + Seconds.of(3), + (state) -> Logger.recordOutput(aKitTopic + "/SysIdState", state.toString())), + new SysIdRoutine.Mechanism(io::setVoltageGoal, null, subsystem)); + + this.constants = constants; + } + + public Elevator(ElevatorConstants constants, Subsystem subsystem, int index, ElevatorIO io) { + this( + constants, + subsystem, + index, + io, + new Setpoint<>( + Meters.of(0), + constants.heightOffsetStep, + constants.elevatorParameters.MIN_HEIGHT(), + constants.elevatorParameters.MAX_HEIGHT()), + new Setpoint<>(Volts.of(0), constants.voltageOffsetStep, Volts.of(-12), Volts.of(12))); + } + + public Elevator( + ElevatorConstants constants, + Subsystem subsystem, + int index, + ElevatorIO io, + Setpoint positionGoal) { + this( + constants, + subsystem, + index, + io, + positionGoal, + new Setpoint<>(Volts.of(0), constants.voltageOffsetStep, Volts.of(-12), Volts.of(12))); + } + + @Trace + public void periodic() { + io.updateInputs(inputs); + Logger.processInputs(aKitTopic, inputs); + + Logger.recordOutput(aKitTopic + "/State", currentState.name()); + Logger.recordOutput(aKitTopic + "/Voltage Goal", voltageGoal.getSetpoint()); + Logger.recordOutput(aKitTopic + "/Position Goal", positionGoal.getSetpoint()); + Logger.recordOutput(aKitTopic + "/Voltage Offset", voltageGoal.getOffset()); + Logger.recordOutput(aKitTopic + "/Position Offset", positionGoal.getOffset()); + Logger.recordOutput(aKitTopic + "/At Voltage Goal", atVoltageGoal()); + Logger.recordOutput(aKitTopic + "/At Position Goal", atPositionGoal()); + + switch (currentState) { + case OPEN_LOOP_VOLTAGE_CONTROL -> io.setVoltageGoal((Voltage) voltageGoal.getNewSetpoint()); + case CLOSED_LOOP_POSITION_CONTROL -> + io.setPositionGoal((Distance) positionGoal.getNewSetpoint()); + } + } + + public Distance getElevatorPosition() { + return inputs.position; + } + + public void setVoltageGoal(Voltage voltageGoal) { + currentState = ElevatorState.OPEN_LOOP_VOLTAGE_CONTROL; + this.voltageGoal.setSetpoint(voltageGoal); + } + + public void setPositionGoal(Distance positionGoal) { + currentState = ElevatorState.CLOSED_LOOP_POSITION_CONTROL; + this.positionGoal.setSetpoint(positionGoal); + } + + public void setVoltageGoal(Setpoint voltageGoal) { + currentState = ElevatorState.OPEN_LOOP_VOLTAGE_CONTROL; + this.voltageGoal = voltageGoal; + } + + public void setPositionGoal(Setpoint positionGoal) { + currentState = ElevatorState.CLOSED_LOOP_POSITION_CONTROL; + this.positionGoal = positionGoal; + } + + public boolean atVoltageGoal(Voltage voltageReference) { + return voltageGoal.getNewSetpoint().isNear(voltageReference, Millivolts.of(500)); + } + + public boolean atPositionGoal(Distance positionReference) { + return positionGoal + .getNewSetpoint() + .isNear(positionReference, constants.constraints.goalTolerance().get()); + } + + public boolean atVoltageGoal() { + return atVoltageGoal((Voltage) voltageGoal.getNewSetpoint()); + } + + public boolean atPositionGoal() { + return atPositionGoal((Distance) positionGoal.getNewSetpoint()); + } + + public void setPosition(Distance position) { + io.setPosition(position); + } + + public void setGainSlot(GainSlot gainSlot) { + io.setGainSlot(gainSlot); + } + + public Command waitUntilAtGoal() { + return Commands.waitUntil(this::atPositionGoal); + } + + public void updateGains(Gains gains, GainSlot slot) { + io.updateGains(gains, slot); + } + + public void updateConstraints(LinearConstraints constraints) { + io.updateConstraints(constraints); + } + + public Command runSysIdRoutine() { + return Commands.sequence( + Commands.runOnce(() -> currentState = ElevatorState.IDLE), + characterizationRoutine.quasistatic(SysIdRoutine.Direction.kForward), + Commands.waitSeconds(1.0), + characterizationRoutine.quasistatic(SysIdRoutine.Direction.kReverse), + Commands.waitSeconds(1.0), + characterizationRoutine.dynamic(SysIdRoutine.Direction.kForward), + Commands.waitSeconds(1.0), + characterizationRoutine.dynamic(SysIdRoutine.Direction.kReverse)); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorConstants.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorConstants.java new file mode 100644 index 00000000..6b2bd590 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorConstants.java @@ -0,0 +1,48 @@ +package edu.wpi.team190.gompeilib.subsystems.elevator; + +import com.ctre.phoenix6.CANBus; +import edu.wpi.first.math.system.plant.DCMotor; +import edu.wpi.first.units.measure.Distance; +import edu.wpi.first.units.measure.Voltage; +import edu.wpi.team190.gompeilib.core.utility.control.Gains; +import edu.wpi.team190.gompeilib.core.utility.control.constraints.LinearConstraints; +import java.util.Set; +import lombok.Builder; +import lombok.NonNull; +import lombok.Singular; + +@Builder(setterPrefix = "with") +public class ElevatorConstants { + @NonNull public final Integer leaderCANID; + @NonNull public final CANBus canBus = new CANBus(); + @NonNull public final Double elevatorGearRatio; + @NonNull public final Double drumRadius; + + @NonNull public final Double elevatorSupplyCurrentLimit; + @NonNull public final Double elevatorStatorCurrentLimit; + + @NonNull public final ElevatorParameters elevatorParameters; + @NonNull public final Gains slot0Gains; + @Builder.Default public final Gains slot1Gains = Gains.builder().build(); + @Builder.Default public final Gains slot2Gains = Gains.builder().build(); + @NonNull public final LinearConstraints constraints; + + @Singular(value = "alignedFollowerCANID") + @NonNull + public final Set alignedFollowerCANIDs; + + @Singular(value = "opposedFollowerCANID") + @NonNull + public final Set opposedFollowerCANIDs; + + @NonNull public final Voltage voltageOffsetStep; + @NonNull public final Distance heightOffsetStep; + + @Builder(setterPrefix = "with") + public record ElevatorParameters( + @NonNull DCMotor ELEVATOR_MOTOR_CONFIG, + @NonNull Double CARRIAGE_MASS_KG, + @NonNull Distance MIN_HEIGHT, + @NonNull Distance MAX_HEIGHT, + @NonNull Integer NUM_MOTORS) {} +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIO.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIO.java new file mode 100644 index 00000000..7078080b --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIO.java @@ -0,0 +1,97 @@ +package edu.wpi.team190.gompeilib.subsystems.elevator; + +import static edu.wpi.first.units.Units.*; + +import edu.wpi.first.units.measure.*; +import edu.wpi.team190.gompeilib.core.utility.control.Gains; +import edu.wpi.team190.gompeilib.core.utility.control.constraints.LinearConstraints; +import edu.wpi.team190.gompeilib.core.utility.phoenix.GainSlot; +import org.littletonrobotics.junction.AutoLog; + +public interface ElevatorIO { + + @AutoLog + public static class ElevatorIOInputs { + public Distance position = Meters.of(0.0); + public LinearVelocity velocity = MetersPerSecond.of(0.0); + public LinearAcceleration acceleration = MetersPerSecondPerSecond.of(0.0); + + public double[] appliedVolts = new double[] {}; + public double[] supplyCurrentAmps = new double[] {}; + public double[] torqueCurrentAmps = new double[] {}; + public double[] temperatureCelsius = new double[] {}; + + public Distance positionGoalMeters = Meters.of(0.0); + public Distance positionSetpointMeters = Meters.of(0.0); + public Distance positionErrorMeters = Meters.of(0.0); + + public GainSlot gainSlot; + } + + /** + * Updates the inputs for the elevator. + * + * @param inputs The inputs to update. + */ + default void updateInputs(ElevatorIOInputs inputs) {} + + /** + * Sets the voltage for the elevator. + * + * @param voltageGoal The voltage of the elevator in volts. + */ + default void setVoltageGoal(Voltage voltageGoal) {} + + /** + * Sets the position goal of the elevator. + * + * @param positionGoal The position goal of the elevator in meters. + */ + default void setPositionGoal(Distance positionGoal) {} + + /** + * Checks if the voltage of the elevator matches the volts argument + * + * @param voltageReference The voltage to check against + * @return True if the voltage matches, false otherwise + */ + default boolean atVoltageGoal(Voltage voltageReference) { + return false; + } + + /** + * Checks if the position of the subsystem matches the positionGoal argument + * + * @param positionReference the position to check against + * @return True if the position matches, false otherwise + */ + default boolean atPositionGoal(Distance positionReference) { + return false; + } + + /** + * Sets the position of the elevator. + * + * @param position The position to set. + */ + default void setPosition(Distance position) {} + + /** + * @param gainSlot The CTRE gain slot to set the elevator to. + */ + default void setGainSlot(GainSlot gainSlot) {} + + /** + * Sets the gains for the elevator. + * + * @param gains the gains to update + */ + default void updateGains(Gains gains, GainSlot gainSlot) {} + + /** + * Sets the constraints for the elevator. + * + * @param constraints the constraints to update + */ + default void updateConstraints(LinearConstraints constraints) {} +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIOSim.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIOSim.java new file mode 100644 index 00000000..472dc64c --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIOSim.java @@ -0,0 +1,163 @@ +package edu.wpi.team190.gompeilib.subsystems.elevator; + +import static edu.wpi.first.units.Units.*; + +import edu.wpi.first.math.MathUtil; +import edu.wpi.first.math.controller.ElevatorFeedforward; +import edu.wpi.first.math.controller.ProfiledPIDController; +import edu.wpi.first.math.system.plant.LinearSystemId; +import edu.wpi.first.math.trajectory.TrapezoidProfile; +import edu.wpi.first.units.measure.*; +import edu.wpi.first.wpilibj.simulation.ElevatorSim; +import edu.wpi.team190.gompeilib.core.GompeiLib; +import edu.wpi.team190.gompeilib.core.utility.control.Gains; +import edu.wpi.team190.gompeilib.core.utility.control.constraints.LinearConstraints; +import edu.wpi.team190.gompeilib.core.utility.phoenix.GainSlot; +import java.util.Arrays; + +public class ElevatorIOSim implements ElevatorIO { + private final ElevatorSim sim; + + private Voltage appliedVolts; + private boolean isClosedLoop; + private GainSlot gainSlot; + + private final ProfiledPIDController feedback; + private ElevatorFeedforward feedforward; + + private final ElevatorConstants constants; + + public ElevatorIOSim(ElevatorConstants constants) { + sim = + new ElevatorSim( + LinearSystemId.createElevatorSystem( + constants.elevatorParameters.ELEVATOR_MOTOR_CONFIG(), + constants.elevatorParameters.CARRIAGE_MASS_KG(), + constants.drumRadius, + constants.elevatorGearRatio), + constants.elevatorParameters.ELEVATOR_MOTOR_CONFIG(), + constants.elevatorParameters.MIN_HEIGHT().in(Meters), + constants.elevatorParameters.MAX_HEIGHT().in(Meters), + true, + constants.elevatorParameters.MIN_HEIGHT().in(Meters)); + + appliedVolts = Volts.of(0.0); + isClosedLoop = true; + gainSlot = GainSlot.ZERO; + + feedback = + new ProfiledPIDController( + constants.slot0Gains.kP().get(), + 0, + constants.slot0Gains.kD().get(), + new TrapezoidProfile.Constraints( + constants.constraints.maxVelocity().get().in(MetersPerSecond), + constants.constraints.maxAcceleration().get().in(MetersPerSecondPerSecond))); + + feedforward = + new ElevatorFeedforward( + constants.slot0Gains.kS().get(), + constants.slot0Gains.kG().get(), + constants.slot0Gains.kV().get(), + constants.slot0Gains.kA().get()); + + this.constants = constants; + } + + @Override + public void updateInputs(ElevatorIOInputs inputs) { + if (isClosedLoop) { + appliedVolts = + Volts.of( + feedback.calculate(sim.getPositionMeters()) + + feedforward.calculate((feedback.getSetpoint().velocity))); + } + + appliedVolts = Volts.of(MathUtil.clamp(appliedVolts.in(Volts), -12, 12)); + + sim.setInputVoltage(appliedVolts.in(Volts)); + sim.update(GompeiLib.getLoopPeriod()); + + inputs.position = Meters.of(sim.getPositionMeters()); + inputs.velocity = MetersPerSecond.of(sim.getVelocityMetersPerSecond()); + inputs.acceleration = + MetersPerSecondPerSecond.of(-1.0); // TODO: Replace with calculation based on velocity + + inputs.appliedVolts = new double[constants.elevatorParameters.NUM_MOTORS()]; + inputs.supplyCurrentAmps = new double[constants.elevatorParameters.NUM_MOTORS()]; + inputs.torqueCurrentAmps = new double[constants.elevatorParameters.NUM_MOTORS()]; + inputs.temperatureCelsius = new double[constants.elevatorParameters.NUM_MOTORS()]; + + Arrays.fill(inputs.appliedVolts, appliedVolts.in(Volts)); + Arrays.fill(inputs.supplyCurrentAmps, sim.getCurrentDrawAmps()); + Arrays.fill(inputs.torqueCurrentAmps, sim.getCurrentDrawAmps()); + Arrays.fill(inputs.temperatureCelsius, 0.0); + + inputs.positionGoalMeters = Meters.of(feedback.getGoal().position); + inputs.positionSetpointMeters = Meters.of(feedback.getSetpoint().position); + inputs.positionErrorMeters = Meters.of(feedback.getPositionError()); + + inputs.gainSlot = gainSlot; + } + + @Override + public void setVoltageGoal(Voltage volts) { + isClosedLoop = false; + appliedVolts = volts; + } + + @Override + public void setPositionGoal(Distance position) { + isClosedLoop = true; + feedback.setGoal(position.in(Meters)); + } + + @Override + public boolean atVoltageGoal(Voltage voltageReference) { + return appliedVolts.isNear(voltageReference, Millivolt.of(500)); + } + + @Override + public boolean atPositionGoal(Distance positionReference) { + return Meters.of(sim.getPositionMeters()) + .isNear(positionReference, constants.constraints.goalTolerance().get()); + } + + @Override + public void setPosition(Distance position) { + sim.setState(position.in(Meters), sim.getVelocityMetersPerSecond()); + } + + @Override + public void setGainSlot(GainSlot gainSlot) { + this.gainSlot = gainSlot; + switch (gainSlot) { + case ZERO: + feedback.setPID(constants.slot0Gains.kP().get(), 0.0, constants.slot0Gains.kD().get()); + break; + case ONE: + feedback.setPID(constants.slot1Gains.kP().get(), 0.0, constants.slot1Gains.kD().get()); + break; + case TWO: + feedback.setPID(constants.slot2Gains.kP().get(), 0.0, constants.slot2Gains.kD().get()); + break; + } + } + + @Override + public void updateGains(Gains gains, GainSlot gainSlot) { + feedback.setPID(gains.kP().get(), gains.kI().get(), gains.kD().get()); + feedforward = + new ElevatorFeedforward( + gains.kS().get(), gains.kG().get(), gains.kV().get(), gains.kA().get()); + } + + @Override + public void updateConstraints(LinearConstraints constraints) { + feedback.setConstraints( + new TrapezoidProfile.Constraints( + constraints.maxVelocity().get().in(MetersPerSecond), + constraints.maxAcceleration().get().in(MetersPerSecondPerSecond))); + feedback.setTolerance(constraints.goalTolerance().get().in(Meters)); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIOTalonFX.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIOTalonFX.java new file mode 100644 index 00000000..47068d82 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIOTalonFX.java @@ -0,0 +1,299 @@ +package edu.wpi.team190.gompeilib.subsystems.elevator; + +import static edu.wpi.first.units.Units.*; + +import com.ctre.phoenix6.BaseStatusSignal; +import com.ctre.phoenix6.StatusSignal; +import com.ctre.phoenix6.configs.TalonFXConfiguration; +import com.ctre.phoenix6.controls.Follower; +import com.ctre.phoenix6.controls.MotionMagicVoltage; +import com.ctre.phoenix6.controls.VoltageOut; +import com.ctre.phoenix6.hardware.TalonFX; +import com.ctre.phoenix6.signals.GravityTypeValue; +import com.ctre.phoenix6.signals.MotorAlignmentValue; +import edu.wpi.first.units.measure.*; +import edu.wpi.team190.gompeilib.core.GompeiLib; +import edu.wpi.team190.gompeilib.core.utility.control.Gains; +import edu.wpi.team190.gompeilib.core.utility.control.constraints.LinearConstraints; +import edu.wpi.team190.gompeilib.core.utility.phoenix.GainSlot; +import edu.wpi.team190.gompeilib.core.utility.phoenix.PhoenixUtil; +import java.util.ArrayList; + +public class ElevatorIOTalonFX implements ElevatorIO { + + // Core hardware components + protected final TalonFX talonFX; + public final TalonFX[] followTalonFX; + + // Configuration + public final TalonFXConfiguration config; + protected final ElevatorConstants constants; + + // Sensor inputs + private StatusSignal positionRotations; + private StatusSignal velocityRotationsPerSecond; + private StatusSignal accelerationRotationsPerSecondPerSecond; + private ArrayList> appliedVolts; + private ArrayList> supplyCurrentAmps; + private ArrayList> torqueCurrentAmps; + private ArrayList> temperatureCelsius; + + public Distance positionGoalMeters; + + private StatusSignal positionSetpointRotations; + private StatusSignal positionErrorRotations; + + private StatusSignal[] statusSignals; + + private MotionMagicVoltage positionVoltageRequest; + private VoltageOut voltageRequest; + + public ElevatorIOTalonFX(ElevatorConstants constants) { + + this.constants = constants; + + // Create lead motor + talonFX = new TalonFX(constants.leaderCANID, constants.canBus); + + // Create follower motor array (define length) + followTalonFX = new TalonFX[constants.elevatorParameters.NUM_MOTORS() - 1]; + + config = new TalonFXConfiguration(); + config.Slot0.withKP(constants.slot0Gains.kP().get()) + .withKD(constants.slot0Gains.kD().get()) + .withKS(constants.slot0Gains.kS().get()) + .withKV(constants.slot0Gains.kV().get()) + .withKA(constants.slot0Gains.kA().get()) + .withKG(constants.slot0Gains.kG().get()) + .withGravityType(GravityTypeValue.Elevator_Static); + + config.Slot1.withKP(constants.slot1Gains.kP().get()) + .withKD(constants.slot1Gains.kD().get()) + .withKS(constants.slot1Gains.kS().get()) + .withKV(constants.slot1Gains.kV().get()) + .withKA(constants.slot1Gains.kA().get()) + .withKG(constants.slot1Gains.kG().get()) + .withGravityType(GravityTypeValue.Elevator_Static); + + config.Slot2.withKP(constants.slot2Gains.kP().get()) + .withKD(constants.slot2Gains.kD().get()) + .withKS(constants.slot2Gains.kS().get()) + .withKV(constants.slot2Gains.kV().get()) + .withKA(constants.slot2Gains.kA().get()) + .withKG(constants.slot2Gains.kG().get()) + .withGravityType(GravityTypeValue.Elevator_Static); + + config.CurrentLimits.withSupplyCurrentLimit(constants.elevatorSupplyCurrentLimit) + .withSupplyCurrentLimitEnable(true) + .withStatorCurrentLimit(constants.elevatorStatorCurrentLimit) + .withStatorCurrentLimitEnable(true); + + config.Feedback.SensorToMechanismRatio = + constants.elevatorGearRatio / (2 * Math.PI * constants.drumRadius); + + config.SoftwareLimitSwitch.withForwardSoftLimitThreshold( + constants.elevatorParameters.MAX_HEIGHT().in(Meters)) + .withForwardSoftLimitEnable(true) + .withReverseSoftLimitThreshold(constants.elevatorParameters.MIN_HEIGHT().in(Meters)) + .withReverseSoftLimitEnable(true); + + config.MotionMagic.withMotionMagicAcceleration( + constants.constraints.maxAcceleration().get().in(MetersPerSecondPerSecond)) + .withMotionMagicCruiseVelocity( + constants.constraints.maxVelocity().get().in(MetersPerSecond)); + + PhoenixUtil.tryUntilOk(5, () -> talonFX.getConfigurator().apply(config)); + + final int[] indexHolder = {0}; // mutable index for array insertion + + constants.alignedFollowerCANIDs.forEach( + id -> { + TalonFX follower = new TalonFX(id, talonFX.getNetwork()); + followTalonFX[indexHolder[0]++] = follower; + + PhoenixUtil.tryUntilOk(5, () -> follower.getConfigurator().apply(config, 0.25)); + + follower.setControl(new Follower(talonFX.getDeviceID(), MotorAlignmentValue.Aligned)); + }); + + constants.opposedFollowerCANIDs.forEach( + id -> { + TalonFX follower = new TalonFX(id, talonFX.getNetwork()); + followTalonFX[indexHolder[0]++] = follower; + + PhoenixUtil.tryUntilOk(5, () -> follower.getConfigurator().apply(config, 0.25)); + + follower.setControl(new Follower(talonFX.getDeviceID(), MotorAlignmentValue.Opposed)); + }); + + appliedVolts = new ArrayList<>(); + supplyCurrentAmps = new ArrayList<>(); + torqueCurrentAmps = new ArrayList<>(); + temperatureCelsius = new ArrayList<>(); + + positionRotations = talonFX.getPosition(); + velocityRotationsPerSecond = talonFX.getVelocity(); + accelerationRotationsPerSecondPerSecond = talonFX.getAcceleration(); + appliedVolts.add(talonFX.getMotorVoltage()); + supplyCurrentAmps.add(talonFX.getSupplyCurrent()); + torqueCurrentAmps.add(talonFX.getTorqueCurrent()); + temperatureCelsius.add(talonFX.getDeviceTemp()); + positionGoalMeters = Meters.of(0.0); + positionSetpointRotations = talonFX.getClosedLoopReference(); + positionErrorRotations = talonFX.getClosedLoopError(); + + for (TalonFX follower : followTalonFX) { + appliedVolts.add(follower.getMotorVoltage()); + supplyCurrentAmps.add(follower.getSupplyCurrent()); + torqueCurrentAmps.add(follower.getTorqueCurrent()); + temperatureCelsius.add(follower.getDeviceTemp()); + } + + var signalsList = new ArrayList>(); + + signalsList.add(positionRotations); + signalsList.add(velocityRotationsPerSecond); + signalsList.add(accelerationRotationsPerSecondPerSecond); + signalsList.add(positionSetpointRotations); + signalsList.add(positionErrorRotations); + signalsList.addAll(appliedVolts); + signalsList.addAll(supplyCurrentAmps); + signalsList.addAll(torqueCurrentAmps); + signalsList.addAll(temperatureCelsius); + + statusSignals = new StatusSignal[signalsList.size()]; + + for (int i = 0; i < signalsList.size(); i++) { + statusSignals[i] = signalsList.get(i); + } + + BaseStatusSignal.setUpdateFrequencyForAll(1 / GompeiLib.getLoopPeriod(), statusSignals); + + talonFX.optimizeBusUtilization(); + for (TalonFX follower : followTalonFX) { + follower.optimizeBusUtilization(); + } + + positionVoltageRequest = new MotionMagicVoltage(0.0); + voltageRequest = new VoltageOut(0.0); + + PhoenixUtil.registerSignals(constants.canBus.isNetworkFD(), statusSignals); + } + + @Override + public void updateInputs(ElevatorIOInputs inputs) { + + // CTRE status signals are natively in rotations, but setting sensor to mechanism ratio + // including the circumference of the drum allows us to transform into meters directly from + // status signal object + inputs.position = Meters.of(positionRotations.getValueAsDouble()); + inputs.velocity = MetersPerSecond.of(velocityRotationsPerSecond.getValueAsDouble()); + inputs.acceleration = + MetersPerSecondPerSecond.of(accelerationRotationsPerSecondPerSecond.getValueAsDouble()); + + inputs.appliedVolts = new double[appliedVolts.size()]; + inputs.supplyCurrentAmps = new double[supplyCurrentAmps.size()]; + inputs.torqueCurrentAmps = new double[torqueCurrentAmps.size()]; + inputs.temperatureCelsius = new double[temperatureCelsius.size()]; + + for (int i = 0; i <= followTalonFX.length; i++) { + inputs.appliedVolts[i] = appliedVolts.get(i).getValueAsDouble(); + inputs.supplyCurrentAmps[i] = supplyCurrentAmps.get(i).getValueAsDouble(); + inputs.torqueCurrentAmps[i] = torqueCurrentAmps.get(i).getValueAsDouble(); + inputs.temperatureCelsius[i] = temperatureCelsius.get(i).getValueAsDouble(); + } + inputs.positionGoalMeters = positionGoalMeters; + inputs.positionSetpointMeters = Meters.of(positionSetpointRotations.getValueAsDouble()); + inputs.positionErrorMeters = Meters.of(positionErrorRotations.getValueAsDouble()); + + inputs.gainSlot = GainSlot.integerToGainSlot(talonFX.getClosedLoopSlot().getValue()); + } + + @Override + public void setVoltageGoal(Voltage voltageGoal) { + talonFX.setControl(voltageRequest.withOutput(voltageGoal).withEnableFOC(true)); + } + + @Override + public void setPositionGoal(Distance positionGoal) { + positionGoalMeters = positionGoal; + talonFX.setControl(positionVoltageRequest.withPosition(positionGoal.in(Meters))); + } + + @Override + public boolean atVoltageGoal(Voltage voltageReference) { + return appliedVolts.get(0).getValue().isNear(voltageReference, Millivolts.of(500)); + } + + @Override + public boolean atPositionGoal(Distance positionReference) { + return Math.abs(positionRotations.getValueAsDouble() - positionReference.in(Meters)) + <= constants.constraints.goalTolerance().get(Meters); + } + + @Override + public void setPosition(Distance position) { + talonFX.setPosition(position.in(Meters)); + } + + @Override + public void setGainSlot(GainSlot slot) { + switch (slot) { + case ONE: + talonFX.setControl(positionVoltageRequest.withSlot(1)); + break; + case TWO: + talonFX.setControl(positionVoltageRequest.withSlot(2)); + break; + default: + talonFX.setControl(positionVoltageRequest.withSlot(0)); + break; + } + } + + @Override + public void updateGains(Gains gains, GainSlot gainSlot) { + switch (gainSlot) { + case ZERO: + config.Slot0.withKP(gains.kP().get()) + .withKD(gains.kD().get()) + .withKS(gains.kS().get()) + .withKV(gains.kV().get()) + .withKA(gains.kA().get()) + .withKG(gains.kG().get()); + break; + case ONE: + config.Slot1.withKP(gains.kP().get()) + .withKD(gains.kD().get()) + .withKS(gains.kS().get()) + .withKV(gains.kV().get()) + .withKA(gains.kA().get()) + .withKG(gains.kG().get()); + break; + case TWO: + default: + config.Slot2.withKP(gains.kP().get()) + .withKD(gains.kD().get()) + .withKS(gains.kS().get()) + .withKV(gains.kV().get()) + .withKA(gains.kA().get()) + .withKG(gains.kG().get()); + break; + } + PhoenixUtil.tryUntilOk(5, () -> talonFX.getConfigurator().apply(config, 0.25)); + for (TalonFX follower : followTalonFX) { + PhoenixUtil.tryUntilOk(5, () -> follower.getConfigurator().apply(config, 0.25)); + } + } + + @Override + public void updateConstraints(LinearConstraints constraints) { + config.MotionMagic.withMotionMagicAcceleration( + constraints.maxAcceleration().get().in(MetersPerSecondPerSecond)) + .withMotionMagicCruiseVelocity(constraints.maxVelocity().get().in(MetersPerSecond)); + PhoenixUtil.tryUntilOk(5, () -> talonFX.getConfigurator().apply(config, 0.25)); + for (TalonFX follower : followTalonFX) { + PhoenixUtil.tryUntilOk(5, () -> follower.getConfigurator().apply(config, 0.25)); + } + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIOTalonFXSim.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIOTalonFXSim.java new file mode 100644 index 00000000..0b52d670 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIOTalonFXSim.java @@ -0,0 +1,63 @@ +package edu.wpi.team190.gompeilib.subsystems.elevator; + +import static edu.wpi.first.units.Units.*; +import static edu.wpi.first.units.Units.Meters; + +import com.ctre.phoenix6.sim.TalonFXSimState; +import edu.wpi.first.math.system.plant.LinearSystemId; +import edu.wpi.first.units.measure.Angle; +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.wpilibj.RobotController; +import edu.wpi.first.wpilibj.simulation.ElevatorSim; +import edu.wpi.team190.gompeilib.core.GompeiLib; +import edu.wpi.team190.gompeilib.core.logging.Trace; + +public class ElevatorIOTalonFXSim extends ElevatorIOTalonFX { + private final ElevatorSim elevatorSim; + + private final TalonFXSimState elevatorController; + + public ElevatorIOTalonFXSim(ElevatorConstants constants) { + super(constants); + elevatorSim = + new ElevatorSim( + LinearSystemId.createElevatorSystem( + constants.elevatorParameters.ELEVATOR_MOTOR_CONFIG(), + constants.elevatorParameters.CARRIAGE_MASS_KG(), + constants.drumRadius, + constants.elevatorGearRatio), + constants.elevatorParameters.ELEVATOR_MOTOR_CONFIG(), + constants.elevatorParameters.MIN_HEIGHT().in(Meters), + constants.elevatorParameters.MAX_HEIGHT().in(Meters), + true, + constants.elevatorParameters.MIN_HEIGHT().in(Meters)); + + elevatorController = super.talonFX.getSimState(); + } + + @Override + @Trace + public void updateInputs(ElevatorIOInputs inputs) { + elevatorController.setSupplyVoltage(RobotController.getBatteryVoltage()); + double elevatorVoltage = elevatorController.getMotorVoltage(); + + elevatorSim.setInputVoltage(elevatorVoltage); + + elevatorSim.update(GompeiLib.getLoopPeriod()); + + Angle rotorPosition = + Angle.ofBaseUnits( + elevatorSim.getPositionMeters() * constants.elevatorGearRatio * constants.drumRadius, + Radians); + AngularVelocity rotorVelocity = + AngularVelocity.ofBaseUnits( + elevatorSim.getVelocityMetersPerSecond() + * constants.elevatorGearRatio + * constants.drumRadius, + RadiansPerSecond); + elevatorController.setRawRotorPosition(rotorPosition); + elevatorController.setRotorVelocity(rotorVelocity); + + super.updateInputs(inputs); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorState.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorState.java new file mode 100644 index 00000000..b09c07e2 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorState.java @@ -0,0 +1,7 @@ +package edu.wpi.team190.gompeilib.subsystems.elevator; + +public enum ElevatorState { + IDLE, + OPEN_LOOP_VOLTAGE_CONTROL, + CLOSED_LOOP_POSITION_CONTROL; +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheel.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheel.java new file mode 100644 index 00000000..591dd735 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheel.java @@ -0,0 +1,265 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.flywheel; + +import static edu.wpi.first.units.Units.*; + +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.units.*; +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.units.measure.Current; +import edu.wpi.first.units.measure.Voltage; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; +import edu.wpi.first.wpilibj2.command.Subsystem; +import edu.wpi.team190.gompeilib.core.utility.Setpoint; +import edu.wpi.team190.gompeilib.core.utility.control.Gains; +import edu.wpi.team190.gompeilib.core.utility.control.constraints.AngularVelocityConstraints; +import edu.wpi.team190.gompeilib.core.utility.phoenix.GainSlot; +import edu.wpi.team190.gompeilib.core.utility.sysid.CustomSysIdRoutine; +import edu.wpi.team190.gompeilib.core.utility.sysid.CustomUnits; +import java.util.function.Supplier; +import lombok.Getter; +import org.littletonrobotics.junction.Logger; + +public class GenericFlywheel { + private final GenericFlywheelIO io; + private final GenericFlywheelIOInputsAutoLogged inputs; + + private final String aKitTopic; + + @Getter private GenericFlywheelState currentState; + + @Getter private Setpoint velocityGoal; + @Getter private Setpoint voltageGoal; + @Getter private Current currentGoal; + + private final CustomSysIdRoutine voltageCharacterizationRoutine; + private final CustomSysIdRoutine torqueCharacterizationRoutine; + + public GenericFlywheel( + GenericFlywheelIO io, + Subsystem subsystem, + GenericFlywheelConstants constants, + String name, + Setpoint velocityGoal, + Setpoint voltageGoal) { + this.io = io; + inputs = new GenericFlywheelIOInputsAutoLogged(); + + aKitTopic = subsystem.getName() + "/" + "Flywheel" + name; + + currentState = GenericFlywheelState.IDLE; + + this.velocityGoal = velocityGoal; + this.voltageGoal = voltageGoal; + currentGoal = Amps.of(0.0); + + voltageCharacterizationRoutine = + new CustomSysIdRoutine<>( + new CustomSysIdRoutine.Config( + CustomUnits.voltsPerSecond.ofNative(0.5), + Volts.of(8), + Seconds.of(24), + (state) -> + Logger.recordOutput(aKitTopic + "/Voltage SysID State", state.toString()), + Volts), + new CustomSysIdRoutine.Mechanism<>( + (volts) -> io.setVoltageGoal(Volts.of(volts.in(Volts))), subsystem), + Volts.mutable(0)); + + torqueCharacterizationRoutine = + new CustomSysIdRoutine<>( + new CustomSysIdRoutine.Config( + CustomUnits.ampsPerSecond.ofNative(0.5), + Amps.of(3.5), + Seconds.of(10), + (state) -> + Logger.recordOutput( + aKitTopic + "/Torque Current SysID State", state.toString()), + Amp), + new CustomSysIdRoutine.Mechanism<>( + (amps) -> io.setCurrentGoal(Amps.of(amps.in(Amps))), subsystem), + Amp.mutable(0)); + } + + public GenericFlywheel( + GenericFlywheelIO io, Subsystem subsystem, GenericFlywheelConstants constants, String name) { + this( + io, + subsystem, + constants, + name, + new Setpoint<>( + RadiansPerSecond.of(0), + constants.velocityOffsetStep, + RadiansPerSecond.of(-constants.gearRatio * constants.motorConfig.freeSpeedRadPerSec), + RadiansPerSecond.of(constants.gearRatio * constants.motorConfig.freeSpeedRadPerSec)), + new Setpoint<>(Volts.of(0), constants.voltageOffsetStep, Volts.of(-12), Volts.of(12))); + } + + public GenericFlywheel( + GenericFlywheelIO io, + Subsystem subsystem, + GenericFlywheelConstants constants, + String name, + Setpoint velocityGoal) { + this( + io, + subsystem, + constants, + name, + velocityGoal, + new Setpoint<>(Volts.of(0), constants.voltageOffsetStep, Volts.of(-12), Volts.of(12))); + } + + public void periodic() { + io.updateInputs(inputs); + Logger.processInputs(aKitTopic, inputs); + + Logger.recordOutput(aKitTopic + "/State", currentState.name()); + Logger.recordOutput(aKitTopic + "/Velocity Goal", velocityGoal.getSetpoint()); + Logger.recordOutput(aKitTopic + "/Voltage Goal", voltageGoal.getSetpoint()); + Logger.recordOutput(aKitTopic + "/Voltage Offset", voltageGoal.getOffset()); + Logger.recordOutput(aKitTopic + "/Velocity Offset", velocityGoal.getOffset()); + Logger.recordOutput(aKitTopic + "/Current Goal", currentGoal); + Logger.recordOutput(aKitTopic + "/At Velocity Goal", atVelocityGoal()); + Logger.recordOutput(aKitTopic + "/At Voltage Goal", atVoltageGoal()); + Logger.recordOutput(aKitTopic + "/At Current Goal", atCurrentGoal()); + + switch (currentState) { + case VELOCITY_VOLTAGE_CONTROL: + io.setVelocityGoal((AngularVelocity) velocityGoal.getNewSetpoint()); + break; + case VELOCITY_TORQUE_CONTROL: + io.setVelocityGoal((AngularVelocity) velocityGoal.getNewSetpoint(), currentGoal); + break; + case VOLTAGE_CONTROL: + io.setVoltageGoal((Voltage) voltageGoal.getNewSetpoint()); + break; + case STOP: + io.setNeutralControl(); + break; + case IDLE: + break; + } + } + + public AngularVelocity getFlywheelVelocity() { + return inputs.velocity; + } + + public Rotation2d getFlywheelPosition() { + return inputs.position; + } + + public void setVoltageGoal(Voltage voltageGoal) { + currentState = GenericFlywheelState.VOLTAGE_CONTROL; + this.voltageGoal.setSetpoint(voltageGoal); + } + + public void setVoltageGoal(Setpoint voltageGoal) { + currentState = GenericFlywheelState.VOLTAGE_CONTROL; + this.voltageGoal = voltageGoal; + } + + public void setVelocityGoal(AngularVelocity velocityGoal) { + currentState = GenericFlywheelState.VELOCITY_VOLTAGE_CONTROL; + this.velocityGoal.setSetpoint(velocityGoal); + } + + public void setVelocityGoal(Setpoint velocityGoal) { + currentState = GenericFlywheelState.VELOCITY_VOLTAGE_CONTROL; + this.velocityGoal = velocityGoal; + } + + public void setVelocityGoal(Supplier velocityGoal) { + currentState = GenericFlywheelState.VELOCITY_VOLTAGE_CONTROL; + this.velocityGoal.setSetpoint(velocityGoal.get()); + } + + public void setVelocityGoal(AngularVelocity velocityGoal, Current currentGoal) { + currentState = GenericFlywheelState.VELOCITY_TORQUE_CONTROL; + this.velocityGoal.setSetpoint(velocityGoal); + this.currentGoal = currentGoal; + } + + public void setVelocityGoal(Setpoint velocityGoal, Current currentGoal) { + currentState = GenericFlywheelState.VELOCITY_TORQUE_CONTROL; + this.velocityGoal = velocityGoal; + this.currentGoal = currentGoal; + } + + public void setVelocityGoal( + Supplier velocityGoal, Supplier currentGoal) { + currentState = GenericFlywheelState.VELOCITY_TORQUE_CONTROL; + this.velocityGoal.setSetpoint(velocityGoal.get()); + this.currentGoal = currentGoal.get(); + } + + public void stop() { + currentState = GenericFlywheelState.STOP; + } + + public boolean atVoltageGoal(Voltage voltageReference) { + return io.atVoltageGoal(voltageReference); + } + + public boolean atVelocityGoal(AngularVelocity velocityReference) { + return io.atVelocityGoal(velocityReference); + } + + public boolean atCurrentGoal(Current currentReference) { + return io.atCurrentGoal(currentReference); + } + + public boolean atVoltageGoal() { + return atVoltageGoal((Voltage) voltageGoal.getNewSetpoint()); + } + + public boolean atVelocityGoal() { + return atVelocityGoal((AngularVelocity) velocityGoal.getNewSetpoint()); + } + + public boolean atCurrentGoal() { + return atCurrentGoal(currentGoal); + } + + public Command waitUntilAtGoal() { + return Commands.waitUntil(this::atVelocityGoal); + } + + public void updateGains(Gains gains, GainSlot gainSlot) { + io.updateGains(gains, gainSlot); + } + + public void updateConstraints(AngularVelocityConstraints constraints) { + io.updateConstraints(constraints); + } + + public Command sysIdRoutineVoltage() { + return Commands.sequence( + Commands.runOnce(() -> currentState = GenericFlywheelState.IDLE), + voltageCharacterizationRoutine + .dynamic(CustomSysIdRoutine.Direction.kForward) + .withTimeout(10), + Commands.waitSeconds(6.0), + voltageCharacterizationRoutine + .dynamic(CustomSysIdRoutine.Direction.kReverse) + .withTimeout(10), + Commands.waitSeconds(6.0), + voltageCharacterizationRoutine.quasistatic(CustomSysIdRoutine.Direction.kForward), + Commands.waitSeconds(6.0), + voltageCharacterizationRoutine.quasistatic(CustomSysIdRoutine.Direction.kReverse)); + } + + public Command sysIdRoutineTorque() { + return Commands.sequence( + Commands.runOnce(() -> currentState = GenericFlywheelState.IDLE), + torqueCharacterizationRoutine.dynamic(CustomSysIdRoutine.Direction.kForward), + Commands.waitSeconds(5.0), + torqueCharacterizationRoutine.dynamic(CustomSysIdRoutine.Direction.kReverse), + Commands.waitSeconds(5.0), + torqueCharacterizationRoutine.quasistatic(CustomSysIdRoutine.Direction.kForward), + Commands.waitSeconds(5.0), + torqueCharacterizationRoutine.quasistatic(CustomSysIdRoutine.Direction.kReverse)); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelConstants.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelConstants.java new file mode 100644 index 00000000..79623905 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelConstants.java @@ -0,0 +1,46 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.flywheel; + +import com.ctre.phoenix6.CANBus; +import com.ctre.phoenix6.signals.InvertedValue; +import edu.wpi.first.math.system.plant.DCMotor; +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.units.measure.Voltage; +import edu.wpi.team190.gompeilib.core.utility.control.CurrentLimits; +import edu.wpi.team190.gompeilib.core.utility.control.Gains; +import edu.wpi.team190.gompeilib.core.utility.control.constraints.AngularVelocityConstraints; +import java.util.Set; +import lombok.Builder; +import lombok.NonNull; +import lombok.Singular; + +@Builder(setterPrefix = "with") +public class GenericFlywheelConstants { + + @NonNull public final Integer leaderCANID; + @NonNull public final InvertedValue leaderInversion; + + @NonNull public final CANBus canBus; + @NonNull public final Boolean enableFOC; + + @NonNull public final CurrentLimits currentLimit; + @NonNull public final Double momentOfInertia; + @NonNull public final Double gearRatio; + + @NonNull public final DCMotor motorConfig; + + @NonNull public final Gains voltageGains; + @NonNull public final Gains torqueGains; + @NonNull public final AngularVelocityConstraints constraints; + + @Singular(value = "alignedFollowerCANID") + @NonNull + public final Set alignedFollowerCANIDs; + + @Singular(value = "opposedFollowerCANID") + @NonNull + public final Set opposedFollowerCANIDs; + + @NonNull public final AngularVelocity velocityOffsetStep; + + @NonNull public final Voltage voltageOffsetStep; +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIO.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIO.java new file mode 100644 index 00000000..c59924ad --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIO.java @@ -0,0 +1,58 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.flywheel; + +import static edu.wpi.first.units.Units.RadiansPerSecond; + +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.units.measure.*; +import edu.wpi.team190.gompeilib.core.utility.control.Gains; +import edu.wpi.team190.gompeilib.core.utility.control.constraints.AngularVelocityConstraints; +import edu.wpi.team190.gompeilib.core.utility.phoenix.GainSlot; +import org.littletonrobotics.junction.AutoLog; + +public interface GenericFlywheelIO { + + @AutoLog + public static class GenericFlywheelIOInputs { + public Rotation2d position = new Rotation2d(); + public AngularVelocity velocity = RadiansPerSecond.of(0.0); + + public double[] appliedVolts = new double[] {}; + public double[] supplyCurrentAmps = new double[] {}; + public double[] torqueCurrentAmps = new double[] {}; + public double[] temperatureCelsius = new double[] {}; + + public AngularVelocity velocityGoal = RadiansPerSecond.of(0.0); + public AngularVelocity velocitySetpoint = RadiansPerSecond.of(0.0); + public AngularVelocity velocityError = RadiansPerSecond.of(0.0); + + public GainSlot gainSlot; + } + + default void updateInputs(GenericFlywheelIOInputs inputs) {} + + default void setVoltageGoal(Voltage voltageGoal) {} + + default void setCurrentGoal(Current currentGoal) {} + + default void setVelocityGoal(AngularVelocity velocityGoal) {} + + default void setVelocityGoal(AngularVelocity velocityGoal, Current currentFeedforward) {} + + default void setNeutralControl() {} + + default boolean atVoltageGoal(Voltage voltageReference) { + return false; + } + + default boolean atCurrentGoal(Current currentReference) { + return false; + } + + default boolean atVelocityGoal(AngularVelocity velocityReference) { + return false; + } + + default void updateGains(Gains gains, GainSlot gainSlot) {} + + default void updateConstraints(AngularVelocityConstraints constraints) {} +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIOSim.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIOSim.java new file mode 100644 index 00000000..c20b00a5 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIOSim.java @@ -0,0 +1,142 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.flywheel; + +import static edu.wpi.first.units.Units.*; + +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.system.plant.LinearSystemId; +import edu.wpi.first.units.measure.*; +import edu.wpi.first.wpilibj.simulation.FlywheelSim; +import edu.wpi.team190.gompeilib.core.GompeiLib; +import edu.wpi.team190.gompeilib.core.utility.control.Gains; +import edu.wpi.team190.gompeilib.core.utility.control.LinearProfile; +import edu.wpi.team190.gompeilib.core.utility.control.constraints.AngularVelocityConstraints; +import edu.wpi.team190.gompeilib.core.utility.phoenix.GainSlot; +import java.util.Arrays; + +public class GenericFlywheelIOSim implements GenericFlywheelIO { + + private final FlywheelSim motorSim; + + private Voltage appliedVolts; + private boolean isClosedLoop; + private GainSlot gainSlot; + + private final PIDController feedback; + private final SimpleMotorFeedforward feedforward; + private final LinearProfile profile; + + GenericFlywheelConstants constants; + + private Angle accumulatedPosition; + + public GenericFlywheelIOSim(GenericFlywheelConstants constants) { + motorSim = + new FlywheelSim( + LinearSystemId.createFlywheelSystem( + constants.motorConfig, constants.momentOfInertia, constants.gearRatio), + constants.motorConfig); + + appliedVolts = Volts.of(0.0); + isClosedLoop = false; + + feedback = + new PIDController( + constants.voltageGains.kP().get(), 0.0, constants.voltageGains.kD().get()); + feedback.setTolerance(constants.constraints.goalTolerance().get().in(RadiansPerSecond)); + feedforward = + new SimpleMotorFeedforward( + constants.voltageGains.kS().get(), constants.voltageGains.kV().get()); + profile = + new LinearProfile( + constants.constraints.maxAcceleration().get().in(RadiansPerSecondPerSecond), + constants.constraints.maxVelocity().get().in(RadiansPerSecond), + 1 / GompeiLib.getLoopPeriod()); + + this.constants = constants; + + accumulatedPosition = Radians.of(0.0); + } + + @Override + public void updateInputs(GenericFlywheelIOInputs inputs) { + if (isClosedLoop) + appliedVolts = + Volts.of( + feedback.calculate(motorSim.getAngularVelocityRadPerSec()) + + feedforward.calculate(feedback.getSetpoint())); + + appliedVolts = Volts.of(MathUtil.clamp(appliedVolts.in(Volts), -12.0, 12.0)); + motorSim.setInputVoltage(appliedVolts.in(Volts)); + motorSim.update(1.0 / GompeiLib.getLoopPeriod()); + + accumulatedPosition = + accumulatedPosition.plus( + motorSim.getAngularVelocity().times(Seconds.of(GompeiLib.getLoopPeriod()))); + + inputs.position = Rotation2d.fromRadians(accumulatedPosition.in(Radians)); + inputs.velocity = motorSim.getAngularVelocity(); + + Arrays.fill(inputs.appliedVolts, appliedVolts.in(Volts)); + Arrays.fill(inputs.supplyCurrentAmps, motorSim.getCurrentDrawAmps()); + Arrays.fill(inputs.torqueCurrentAmps, motorSim.getCurrentDrawAmps()); + + inputs.velocityGoal = RadiansPerSecond.of(profile.getGoal()); + inputs.velocitySetpoint = RadiansPerSecond.of(feedback.getSetpoint()); + inputs.velocityError = RadiansPerSecond.of(feedback.getError()); + + inputs.gainSlot = gainSlot; + } + + @Override + public void setVoltageGoal(Voltage voltageGoal) { + isClosedLoop = false; + appliedVolts = voltageGoal; + } + + @Override + public void setVelocityGoal(AngularVelocity velocityGoal) { + isClosedLoop = true; + profile.setGoal(velocityGoal.in(RadiansPerSecond), motorSim.getAngularVelocityRadPerSec()); + appliedVolts = + Volts.of( + feedback.calculate(motorSim.getAngularVelocityRadPerSec(), profile.calculateSetpoint()) + + feedforward.calculate(feedback.getSetpoint())); + } + + @Override + public void setNeutralControl() { + appliedVolts = Volts.of(0.0); + profile.reset(); + } + + @Override + public boolean atVoltageGoal(Voltage voltageReference) { + return appliedVolts.isNear(voltageReference, Millivolts.of(500)); + } + + @Override + public boolean atVelocityGoal(AngularVelocity velocityReference) { + return motorSim + .getAngularVelocity() + .isNear(velocityReference, constants.constraints.goalTolerance().get(RadiansPerSecond)); + } + + @Override + public void updateGains(Gains gains, GainSlot gainSlot) { + this.gainSlot = gainSlot; + feedback.setPID(gains.kP().get(), gains.kI().get(), gains.kD().get()); + feedforward.setKs(gains.kS().get()); + feedforward.setKv(gains.kV().get()); + feedforward.setKa(gains.kA().get()); + } + + @Override + public void updateConstraints(AngularVelocityConstraints constraints) { + profile.setMaxAcceleration(constraints.maxAcceleration().get().in(RadiansPerSecondPerSecond)); + profile.setMaxVelocity(constraints.maxVelocity().get().in(RadiansPerSecond)); + feedback.setTolerance(constraints.goalTolerance().get().in(RadiansPerSecond)); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIOTalonFX.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIOTalonFX.java new file mode 100644 index 00000000..18e97f1a --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIOTalonFX.java @@ -0,0 +1,290 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.flywheel; + +import static edu.wpi.first.units.Units.*; + +import com.ctre.phoenix6.BaseStatusSignal; +import com.ctre.phoenix6.StatusSignal; +import com.ctre.phoenix6.configs.MotionMagicConfigs; +import com.ctre.phoenix6.configs.TalonFXConfiguration; +import com.ctre.phoenix6.controls.*; +import com.ctre.phoenix6.hardware.TalonFX; +import com.ctre.phoenix6.signals.MotorAlignmentValue; +import com.ctre.phoenix6.signals.NeutralModeValue; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.units.measure.*; +import edu.wpi.team190.gompeilib.core.GompeiLib; +import edu.wpi.team190.gompeilib.core.utility.control.Gains; +import edu.wpi.team190.gompeilib.core.utility.control.constraints.AngularVelocityConstraints; +import edu.wpi.team190.gompeilib.core.utility.phoenix.GainSlot; +import edu.wpi.team190.gompeilib.core.utility.phoenix.PhoenixUtil; +import java.util.ArrayList; + +public class GenericFlywheelIOTalonFX implements GenericFlywheelIO { + protected final TalonFX talonFX; + private final TalonFX[] followerTalonFX; + + private final StatusSignal positionRotations; + private final StatusSignal velocityRotationsPerSecond; + private final ArrayList> appliedVolts; + private final ArrayList> supplyCurrentAmps; + private final ArrayList> torqueCurrentAmps; + private final ArrayList> temperatureCelsius; + private AngularVelocity velocityGoal; + private final StatusSignal velocitySetpointRotationsPerSecond; + private final StatusSignal velocityErrorRotationsPerSecond; + + private StatusSignal[] statusSignals; + + private final TalonFXConfiguration talonFXConfiguration; + + private final NeutralOut neutralControlRequest; + private final VoltageOut voltageControlRequest; + private final TorqueCurrentFOC torqueCurrentFOCRequest; + private final VelocityVoltage velocityControlRequest; + private final VelocityTorqueCurrentFOC velocityTorqueCurrentRequest; + + protected GenericFlywheelConstants constants; + + public GenericFlywheelIOTalonFX(GenericFlywheelConstants constants) { + talonFX = new TalonFX(constants.leaderCANID, constants.canBus); + followerTalonFX = + new TalonFX + [constants.alignedFollowerCANIDs.size() + constants.opposedFollowerCANIDs.size()]; + + talonFXConfiguration = new TalonFXConfiguration(); + + talonFXConfiguration.MotorOutput.withInverted(constants.leaderInversion); + + talonFXConfiguration + .CurrentLimits + .withSupplyCurrentLimit(constants.currentLimit.supplyCurrentLimit()) + .withSupplyCurrentLimitEnable(true) + .withStatorCurrentLimit(constants.currentLimit.statorCurrentLimit()) + .withStatorCurrentLimitEnable(true); + talonFXConfiguration.MotorOutput.withNeutralMode(NeutralModeValue.Coast); + talonFXConfiguration + .Slot0 + .withKP(constants.voltageGains.kP().getAsDouble()) + .withKD(constants.voltageGains.kD().getAsDouble()) + .withKS(constants.voltageGains.kS().getAsDouble()) + .withKV(constants.voltageGains.kV().getAsDouble()) + .withKA(constants.voltageGains.kA().getAsDouble()); + + talonFXConfiguration + .Slot1 + .withKP(constants.torqueGains.kP().getAsDouble()) + .withKD(constants.torqueGains.kD().getAsDouble()) + .withKS(constants.torqueGains.kS().getAsDouble()) + .withKV(constants.torqueGains.kV().getAsDouble()) + .withKA(constants.torqueGains.kA().getAsDouble()); + + talonFXConfiguration.Feedback.SensorToMechanismRatio = constants.gearRatio; + + talonFXConfiguration.MotionMagic = + new MotionMagicConfigs() + .withMotionMagicAcceleration( + (AngularAcceleration) constants.constraints.maxAcceleration().get()) + .withMotionMagicCruiseVelocity( + (AngularVelocity) constants.constraints.maxVelocity().get()); + + PhoenixUtil.tryUntilOk(5, () -> talonFX.getConfigurator().apply(talonFXConfiguration, 0.25)); + + final int[] indexHolder = {0}; // mutable index for array insertion + + constants.alignedFollowerCANIDs.forEach( + id -> { + TalonFX follower = new TalonFX(id, talonFX.getNetwork()); + followerTalonFX[indexHolder[0]++] = follower; + + PhoenixUtil.tryUntilOk( + 5, () -> follower.getConfigurator().apply(talonFXConfiguration, 0.25)); + + follower.setControl(new Follower(talonFX.getDeviceID(), MotorAlignmentValue.Aligned)); + }); + + constants.opposedFollowerCANIDs.forEach( + id -> { + TalonFX follower = new TalonFX(id, talonFX.getNetwork()); + followerTalonFX[indexHolder[0]++] = follower; + + PhoenixUtil.tryUntilOk( + 5, () -> follower.getConfigurator().apply(talonFXConfiguration, 0.25)); + + follower.setControl(new Follower(talonFX.getDeviceID(), MotorAlignmentValue.Opposed)); + }); + + positionRotations = talonFX.getPosition(); + velocityRotationsPerSecond = talonFX.getVelocity(); + + appliedVolts = new ArrayList<>(); + supplyCurrentAmps = new ArrayList<>(); + torqueCurrentAmps = new ArrayList<>(); + temperatureCelsius = new ArrayList<>(); + + appliedVolts.add(talonFX.getMotorVoltage()); + supplyCurrentAmps.add(talonFX.getSupplyCurrent()); + torqueCurrentAmps.add(talonFX.getTorqueCurrent()); + temperatureCelsius.add(talonFX.getDeviceTemp()); + + for (TalonFX follower : followerTalonFX) { + appliedVolts.add(follower.getMotorVoltage()); + supplyCurrentAmps.add(follower.getSupplyCurrent()); + torqueCurrentAmps.add(follower.getTorqueCurrent()); + temperatureCelsius.add(follower.getDeviceTemp()); + } + + velocityGoal = RotationsPerSecond.of(0.0); + + velocitySetpointRotationsPerSecond = talonFX.getClosedLoopReference(); + velocityErrorRotationsPerSecond = talonFX.getClosedLoopError(); + + var signalsList = new ArrayList>(); + + signalsList.add(positionRotations); + signalsList.add(velocityRotationsPerSecond); + signalsList.addAll(appliedVolts); + signalsList.addAll(supplyCurrentAmps); + signalsList.addAll(torqueCurrentAmps); + signalsList.addAll(temperatureCelsius); + + statusSignals = new StatusSignal[signalsList.size()]; + + for (int i = 0; i < signalsList.size(); i++) { + statusSignals[i] = signalsList.get(i); + } + + BaseStatusSignal.setUpdateFrequencyForAll(1 / GompeiLib.getLoopPeriod(), statusSignals); + + PhoenixUtil.registerSignals(constants.canBus.isNetworkFD(), statusSignals); + + talonFX.optimizeBusUtilization(); + for (TalonFX follower : followerTalonFX) { + follower.optimizeBusUtilization(); + } + + neutralControlRequest = new NeutralOut(); + voltageControlRequest = new VoltageOut(0.0); + torqueCurrentFOCRequest = new TorqueCurrentFOC(0.0); + + velocityControlRequest = new VelocityVoltage(0).withSlot(0); + velocityTorqueCurrentRequest = new VelocityTorqueCurrentFOC(0.0).withSlot(1); + + this.constants = constants; + } + + @Override + public void updateInputs(GenericFlywheelIOInputs inputs) { + inputs.position = Rotation2d.fromRotations(positionRotations.getValueAsDouble()); + inputs.velocity = velocityRotationsPerSecond.getValue(); + + inputs.appliedVolts = new double[appliedVolts.size()]; + inputs.supplyCurrentAmps = new double[supplyCurrentAmps.size()]; + inputs.torqueCurrentAmps = new double[torqueCurrentAmps.size()]; + inputs.temperatureCelsius = new double[temperatureCelsius.size()]; + + for (int i = 0; i <= followerTalonFX.length; i++) { + inputs.appliedVolts[i] = appliedVolts.get(i).getValueAsDouble(); + inputs.supplyCurrentAmps[i] = supplyCurrentAmps.get(i).getValueAsDouble(); + inputs.torqueCurrentAmps[i] = torqueCurrentAmps.get(i).getValueAsDouble(); + inputs.temperatureCelsius[i] = temperatureCelsius.get(i).getValueAsDouble(); + } + + inputs.velocityGoal = velocityGoal; + inputs.velocitySetpoint = + RotationsPerSecond.of(velocitySetpointRotationsPerSecond.getValueAsDouble()); + inputs.velocityError = RotationsPerSecond.of(velocityRotationsPerSecond.getValueAsDouble()); + + inputs.gainSlot = GainSlot.integerToGainSlot(talonFX.getClosedLoopSlot().getValue()); + } + + @Override + public void setVoltageGoal(Voltage voltageGoal) { + talonFX.setControl( + voltageControlRequest.withOutput(voltageGoal).withEnableFOC(constants.enableFOC)); + } + + @Override + public void setCurrentGoal(Current currentGoal) { + talonFX.setControl(torqueCurrentFOCRequest.withOutput(currentGoal)); + } + + @Override + public void setNeutralControl() { + talonFX.setControl(neutralControlRequest); + } + + @Override + public void setVelocityGoal(AngularVelocity velocityGoal) { + this.velocityGoal = velocityGoal; + talonFX.setControl( + velocityControlRequest.withVelocity(this.velocityGoal).withEnableFOC(constants.enableFOC)); + } + + @Override + public void setVelocityGoal(AngularVelocity velocityGoal, Current currentFeedforward) { + this.velocityGoal = velocityGoal; + talonFX.setControl( + velocityTorqueCurrentRequest + .withVelocity(this.velocityGoal) + .withFeedForward(currentFeedforward)); + } + + @Override + public boolean atVoltageGoal(Voltage voltageReference) { + return appliedVolts.get(0).isNear(voltageReference, Millivolts.of(500)); + } + + @Override + public boolean atCurrentGoal(Current currentReference) { + return torqueCurrentAmps.get(0).isNear(currentReference, Milliamps.of(500)); + } + + @Override + public boolean atVelocityGoal(AngularVelocity velocityReference) { + return velocityRotationsPerSecond.isNear( + velocityReference, + RotationsPerSecond.of(constants.constraints.goalTolerance().get(RotationsPerSecond))); + } + + @Override + public void updateGains(Gains gains, GainSlot gainSlot) { + switch (gainSlot) { + case ONE -> + talonFXConfiguration + .Slot1 + .withKP(gains.kP().get()) + .withKI(gains.kI().get()) + .withKD(gains.kD().get()) + .withKS(gains.kS().get()) + .withKV(gains.kV().get()) + .withKA(gains.kA().get()) + .withKG(gains.kG().get()); + default -> + talonFXConfiguration + .Slot0 + .withKP(gains.kP().get()) + .withKI(gains.kI().get()) + .withKD(gains.kD().get()) + .withKS(gains.kS().get()) + .withKV(gains.kV().get()) + .withKA(gains.kA().get()) + .withKG(gains.kG().get()); + } + PhoenixUtil.tryUntilOk(5, () -> talonFX.getConfigurator().apply(talonFXConfiguration)); + for (TalonFX follower : followerTalonFX) { + PhoenixUtil.tryUntilOk(5, () -> follower.getConfigurator().apply(talonFXConfiguration)); + } + } + + @Override + public void updateConstraints(AngularVelocityConstraints constraints) { + talonFXConfiguration + .MotionMagic + .withMotionMagicAcceleration(constraints.maxAcceleration().get(RotationsPerSecondPerSecond)) + .withMotionMagicCruiseVelocity(constraints.maxVelocity().get(RotationsPerSecond)); + PhoenixUtil.tryUntilOk(5, () -> talonFX.getConfigurator().apply(talonFXConfiguration)); + for (TalonFX follower : followerTalonFX) { + PhoenixUtil.tryUntilOk(5, () -> follower.getConfigurator().apply(talonFXConfiguration)); + } + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIOTalonFXSim.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIOTalonFXSim.java new file mode 100644 index 00000000..05fcdcd0 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIOTalonFXSim.java @@ -0,0 +1,53 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.flywheel; + +import static edu.wpi.first.units.Units.RadiansPerSecond; +import static edu.wpi.first.units.Units.RadiansPerSecondPerSecond; + +import com.ctre.phoenix6.sim.TalonFXSimState; +import edu.wpi.first.math.system.plant.LinearSystemId; +import edu.wpi.first.units.measure.AngularAcceleration; +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.wpilibj.RobotController; +import edu.wpi.first.wpilibj.simulation.FlywheelSim; +import edu.wpi.team190.gompeilib.core.GompeiLib; +import edu.wpi.team190.gompeilib.core.logging.Trace; + +public class GenericFlywheelIOTalonFXSim extends GenericFlywheelIOTalonFX { + private final FlywheelSim flywheelSim; + + private final TalonFXSimState flywheelController; + + public GenericFlywheelIOTalonFXSim(GenericFlywheelConstants constants) { + super(constants); + flywheelSim = + new FlywheelSim( + LinearSystemId.createFlywheelSystem( + constants.motorConfig, constants.momentOfInertia, constants.gearRatio), + constants.motorConfig); + + flywheelController = super.talonFX.getSimState(); + } + + @Override + @Trace + public void updateInputs(GenericFlywheelIOInputs inputs) { + flywheelController.setSupplyVoltage(RobotController.getBatteryVoltage()); + double flywheelVoltage = flywheelController.getMotorVoltage(); + + flywheelSim.setInputVoltage(flywheelVoltage); + + flywheelSim.update(GompeiLib.getLoopPeriod()); + + AngularVelocity rotorVelocity = + AngularVelocity.ofBaseUnits( + flywheelSim.getAngularVelocityRadPerSec() * constants.gearRatio, RadiansPerSecond); + AngularAcceleration rotorAcceleration = + AngularAcceleration.ofBaseUnits( + flywheelSim.getAngularAccelerationRadPerSecSq() * constants.gearRatio, + RadiansPerSecondPerSecond); + flywheelController.setRotorVelocity(rotorVelocity); + flywheelController.setRotorAcceleration(rotorAcceleration); + + super.updateInputs(inputs); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelState.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelState.java new file mode 100644 index 00000000..05de77f1 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelState.java @@ -0,0 +1,9 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.flywheel; + +public enum GenericFlywheelState { + VELOCITY_VOLTAGE_CONTROL, + VELOCITY_TORQUE_CONTROL, + VOLTAGE_CONTROL, + IDLE, + STOP +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRoller.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRoller.java new file mode 100644 index 00000000..ef1d434c --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRoller.java @@ -0,0 +1,74 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.roller; + +import static edu.wpi.first.units.Units.Volts; + +import edu.wpi.first.units.VoltageUnit; +import edu.wpi.first.units.measure.Voltage; +import edu.wpi.first.wpilibj2.command.Subsystem; +import edu.wpi.team190.gompeilib.core.logging.Trace; +import edu.wpi.team190.gompeilib.core.utility.Setpoint; +import lombok.Getter; +import org.littletonrobotics.junction.Logger; + +public class GenericRoller { + private final GenericRollerIO io; + private final GenericRollerIOInputsAutoLogged inputs; + + private final String aKitTopic; + + @Getter private Setpoint voltageGoal; + + public GenericRoller( + GenericRollerIO io, + Subsystem subsystem, + GenericRollerConstants constants, + String name, + Setpoint voltageGoal) { + this.io = io; + inputs = new GenericRollerIOInputsAutoLogged(); + aKitTopic = subsystem.getName() + "/Roller" + name; + this.voltageGoal = voltageGoal; + } + + public GenericRoller( + GenericRollerIO io, Subsystem subsystem, GenericRollerConstants constants, String name) { + this( + io, + subsystem, + constants, + name, + new Setpoint<>(Volts.of(0), constants.voltageOffsetStep, Volts.of(-12), Volts.of(12))); + } + + @Trace + public void periodic() { + io.updateInputs(inputs); + Logger.processInputs(aKitTopic, inputs); + + Logger.recordOutput(aKitTopic + "/Voltage Goal", voltageGoal.getSetpoint()); + Logger.recordOutput(aKitTopic + "/Voltage Offset", voltageGoal.getOffset()); + Logger.recordOutput(aKitTopic + "/At Voltage Goal", atVoltageGoal()); + + io.setVoltageGoal((Voltage) voltageGoal.getNewSetpoint()); + } + + public boolean atVoltageGoal(Voltage voltageReference) { + return io.atVoltageGoal(voltageReference); + } + + public boolean atVoltageGoal() { + return atVoltageGoal((Voltage) voltageGoal.getNewSetpoint()); + } + + public void setVoltageGoal(Voltage voltage) { + voltageGoal.setSetpoint(voltage); + } + + public void setVoltageGoal(Setpoint voltage) { + voltageGoal = voltage; + } + + public double[] getTorqueCurrent() { + return inputs.torqueCurrentAmps; + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerConstants.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerConstants.java new file mode 100644 index 00000000..921eb7c3 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerConstants.java @@ -0,0 +1,38 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.roller; + +import com.ctre.phoenix6.CANBus; +import com.ctre.phoenix6.signals.InvertedValue; +import com.ctre.phoenix6.signals.NeutralModeValue; +import edu.wpi.first.math.system.plant.DCMotor; +import edu.wpi.first.units.measure.MomentOfInertia; +import edu.wpi.first.units.measure.Voltage; +import edu.wpi.team190.gompeilib.core.utility.control.CurrentLimits; +import java.util.Set; +import lombok.Builder; +import lombok.NonNull; +import lombok.Singular; + +@Builder(setterPrefix = "with") +public class GenericRollerConstants { + @NonNull public final Integer leaderCANID; + @NonNull public final InvertedValue leaderInvertedValue; + + @Singular(value = "alignedFollowerCANID") + @NonNull + public final Set alignedFollowerCANIDs; + + @Singular(value = "opposedFollowerCANID") + @NonNull + public final Set opposedFollowerCANIDs; + + @NonNull public final CurrentLimits currentLimits; + @NonNull public final DCMotor rollerGearbox; + @NonNull public final Double rollerMotorGearRatio; + @NonNull public final MomentOfInertia momentOfInertia; + @NonNull public final NeutralModeValue neutralMode; + @NonNull public final CANBus canBus; + + @NonNull public final Boolean enableFOC; + + @NonNull public final Voltage voltageOffsetStep; +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIO.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIO.java new file mode 100644 index 00000000..a0cb8079 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIO.java @@ -0,0 +1,29 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.roller; + +import static edu.wpi.first.units.Units.*; + +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.units.measure.Voltage; +import org.littletonrobotics.junction.AutoLog; + +public interface GenericRollerIO { + @AutoLog + public static class GenericRollerIOInputs { + public Rotation2d position = new Rotation2d(); + public AngularVelocity velocity = RadiansPerSecond.of(0.0); + + public double[] appliedVolts = new double[] {}; + public double[] supplyCurrentAmps = new double[] {}; + public double[] torqueCurrentAmps = new double[] {}; + public double[] temperatureCelsius = new double[] {}; + } + + default void updateInputs(GenericRollerIOInputs inputs) {} + + default void setVoltageGoal(Voltage volts) {} + + default boolean atVoltageGoal(Voltage voltageReference) { + return false; + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIOSim.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIOSim.java new file mode 100644 index 00000000..1bc99178 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIOSim.java @@ -0,0 +1,62 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.roller; + +import static edu.wpi.first.units.Units.*; + +import edu.wpi.first.math.MathUtil; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.system.plant.LinearSystemId; +import edu.wpi.first.units.measure.Angle; +import edu.wpi.first.units.measure.Voltage; +import edu.wpi.first.wpilibj.simulation.DCMotorSim; +import edu.wpi.team190.gompeilib.core.GompeiLib; +import java.util.Arrays; + +public class GenericRollerIOSim implements GenericRollerIO { + private final DCMotorSim motorSim; + + private Voltage appliedVolts; + + private Angle accumulatedPosition; + + public GenericRollerIOSim(GenericRollerConstants constants) { + motorSim = + new DCMotorSim( + LinearSystemId.createDCMotorSystem( + constants.rollerGearbox, + constants.momentOfInertia.baseUnitMagnitude(), + constants.rollerMotorGearRatio), + constants.rollerGearbox); + + appliedVolts = Volts.of(0.0); + + accumulatedPosition = Radians.of(0.0); + } + + @Override + public void updateInputs(GenericRollerIOInputs inputs) { + appliedVolts = Volts.of(MathUtil.clamp(appliedVolts.in(Volts), -12.0, 12.0)); + motorSim.setInputVoltage(appliedVolts.in(Volts)); + motorSim.update(GompeiLib.getLoopPeriod()); + + accumulatedPosition = + Radians.of( + accumulatedPosition.in(Radians) + + (motorSim.getAngularVelocityRadPerSec() * GompeiLib.getLoopPeriod())); + + inputs.position = Rotation2d.fromRadians(accumulatedPosition.in(Radians)); + inputs.velocity = motorSim.getAngularVelocity(); + Arrays.fill(inputs.appliedVolts, appliedVolts.in(Volts)); + Arrays.fill(inputs.supplyCurrentAmps, motorSim.getCurrentDrawAmps()); + Arrays.fill(inputs.torqueCurrentAmps, motorSim.getCurrentDrawAmps()); + } + + @Override + public void setVoltageGoal(Voltage voltageGoal) { + appliedVolts = voltageGoal; + } + + @Override + public boolean atVoltageGoal(Voltage voltageReference) { + return appliedVolts.isNear(voltageReference, Millivolts.of(500)); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIOTalonFX.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIOTalonFX.java new file mode 100644 index 00000000..9378c524 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIOTalonFX.java @@ -0,0 +1,160 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.roller; + +import static edu.wpi.first.units.Units.Millivolts; + +import com.ctre.phoenix6.BaseStatusSignal; +import com.ctre.phoenix6.StatusSignal; +import com.ctre.phoenix6.configs.TalonFXConfiguration; +import com.ctre.phoenix6.controls.Follower; +import com.ctre.phoenix6.controls.VoltageOut; +import com.ctre.phoenix6.hardware.TalonFX; +import com.ctre.phoenix6.signals.MotorAlignmentValue; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.units.measure.*; +import edu.wpi.team190.gompeilib.core.GompeiLib; +import edu.wpi.team190.gompeilib.core.utility.phoenix.PhoenixUtil; +import java.util.ArrayList; + +public class GenericRollerIOTalonFX implements GenericRollerIO { + protected final TalonFX talonFX; + private final TalonFX[] followerTalonFX; + + private final StatusSignal positionRotations; + private final StatusSignal velocityRotationsPerSecond; + private final ArrayList> appliedVolts; + private final ArrayList> supplyCurrentAmps; + private final ArrayList> torqueCurrentAmps; + private final ArrayList> temperatureCelsius; + + private StatusSignal[] statusSignals; + + private final TalonFXConfiguration talonFXConfiguration; + + private final VoltageOut voltageRequest; + + protected GenericRollerConstants constants; + + public GenericRollerIOTalonFX(GenericRollerConstants constants) { + talonFX = new TalonFX(constants.leaderCANID, constants.canBus); + followerTalonFX = + new TalonFX + [constants.alignedFollowerCANIDs.size() + constants.opposedFollowerCANIDs.size()]; + + talonFXConfiguration = new TalonFXConfiguration(); + + talonFXConfiguration.MotorOutput.withInverted(constants.leaderInvertedValue); + talonFXConfiguration.MotorOutput.NeutralMode = constants.neutralMode; + + talonFXConfiguration + .CurrentLimits + .withSupplyCurrentLimit(constants.currentLimits.supplyCurrentLimit()) + .withSupplyCurrentLimitEnable(true) + .withStatorCurrentLimit(constants.currentLimits.statorCurrentLimit()) + .withStatorCurrentLimitEnable(true); + talonFXConfiguration.OpenLoopRamps.VoltageOpenLoopRampPeriod = 0.25; + + talonFXConfiguration.Feedback.SensorToMechanismRatio = constants.rollerMotorGearRatio; + + PhoenixUtil.tryUntilOk(5, () -> talonFX.getConfigurator().apply(talonFXConfiguration, 0.25)); + + final int[] indexHolder = {0}; // mutable index for array insertion + + constants.alignedFollowerCANIDs.forEach( + id -> { + TalonFX follower = new TalonFX(id, talonFX.getNetwork()); + followerTalonFX[indexHolder[0]++] = follower; + + PhoenixUtil.tryUntilOk( + 5, () -> follower.getConfigurator().apply(talonFXConfiguration, 0.25)); + + follower.setControl(new Follower(talonFX.getDeviceID(), MotorAlignmentValue.Aligned)); + }); + + constants.opposedFollowerCANIDs.forEach( + id -> { + TalonFX follower = new TalonFX(id, talonFX.getNetwork()); + followerTalonFX[indexHolder[0]++] = follower; + + PhoenixUtil.tryUntilOk( + 5, () -> follower.getConfigurator().apply(talonFXConfiguration, 0.25)); + + follower.setControl(new Follower(talonFX.getDeviceID(), MotorAlignmentValue.Opposed)); + }); + + positionRotations = talonFX.getPosition(); + velocityRotationsPerSecond = talonFX.getVelocity(); + + appliedVolts = new ArrayList<>(); + supplyCurrentAmps = new ArrayList<>(); + torqueCurrentAmps = new ArrayList<>(); + temperatureCelsius = new ArrayList<>(); + + appliedVolts.add(talonFX.getMotorVoltage()); + supplyCurrentAmps.add(talonFX.getSupplyCurrent()); + torqueCurrentAmps.add(talonFX.getTorqueCurrent()); + temperatureCelsius.add(talonFX.getDeviceTemp()); + + for (TalonFX follower : followerTalonFX) { + appliedVolts.add(follower.getMotorVoltage()); + supplyCurrentAmps.add(follower.getSupplyCurrent()); + torqueCurrentAmps.add(follower.getTorqueCurrent()); + temperatureCelsius.add(follower.getDeviceTemp()); + } + + var signalsList = new ArrayList>(); + + signalsList.add(positionRotations); + signalsList.add(velocityRotationsPerSecond); + signalsList.addAll(appliedVolts); + signalsList.addAll(supplyCurrentAmps); + signalsList.addAll(torqueCurrentAmps); + signalsList.addAll(temperatureCelsius); + + statusSignals = new StatusSignal[signalsList.size()]; + + for (int i = 0; i < signalsList.size(); i++) { + statusSignals[i] = signalsList.get(i); + } + + BaseStatusSignal.setUpdateFrequencyForAll(1 / GompeiLib.getLoopPeriod(), statusSignals); + + PhoenixUtil.registerSignals(constants.canBus.isNetworkFD(), statusSignals); + + talonFX.optimizeBusUtilization(); + for (TalonFX follower : followerTalonFX) { + follower.optimizeBusUtilization(); + } + + voltageRequest = new VoltageOut(0.0).withEnableFOC(constants.enableFOC); + + this.constants = constants; + } + + @Override + public void updateInputs(GenericRollerIOInputs inputs) { + inputs.position = Rotation2d.fromRotations(positionRotations.getValueAsDouble()); + inputs.velocity = velocityRotationsPerSecond.getValue(); + + inputs.appliedVolts = new double[appliedVolts.size()]; + inputs.supplyCurrentAmps = new double[supplyCurrentAmps.size()]; + inputs.torqueCurrentAmps = new double[torqueCurrentAmps.size()]; + inputs.temperatureCelsius = new double[temperatureCelsius.size()]; + + for (int i = 0; i <= followerTalonFX.length; i++) { + inputs.appliedVolts[i] = appliedVolts.get(i).getValueAsDouble(); + inputs.supplyCurrentAmps[i] = supplyCurrentAmps.get(i).getValueAsDouble(); + inputs.torqueCurrentAmps[i] = torqueCurrentAmps.get(i).getValueAsDouble(); + inputs.temperatureCelsius[i] = temperatureCelsius.get(i).getValueAsDouble(); + } + } + + @Override + public void setVoltageGoal(Voltage voltageGoal) { + talonFX.setControl(voltageRequest.withOutput(voltageGoal)); + } + + @Override + public boolean atVoltageGoal(Voltage referenceVoltage) { + return appliedVolts.get(0).isNear(referenceVoltage, Millivolts.of(500)); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIOTalonFXSim.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIOTalonFXSim.java new file mode 100644 index 00000000..b9d327d6 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIOTalonFXSim.java @@ -0,0 +1,55 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.roller; + +import static edu.wpi.first.units.Units.Radians; +import static edu.wpi.first.units.Units.RadiansPerSecond; + +import com.ctre.phoenix6.sim.TalonFXSimState; +import edu.wpi.first.math.system.plant.LinearSystemId; +import edu.wpi.first.units.measure.Angle; +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.wpilibj.RobotController; +import edu.wpi.first.wpilibj.simulation.DCMotorSim; +import edu.wpi.team190.gompeilib.core.GompeiLib; +import edu.wpi.team190.gompeilib.core.logging.Trace; + +public class GenericRollerIOTalonFXSim extends GenericRollerIOTalonFX { + private final DCMotorSim rollerSim; + + private final TalonFXSimState rollerController; + + public GenericRollerIOTalonFXSim(GenericRollerConstants constants) { + super(constants); + rollerSim = + new DCMotorSim( + LinearSystemId.createDCMotorSystem( + constants.rollerGearbox, + constants.momentOfInertia.baseUnitMagnitude(), + constants.rollerMotorGearRatio), + constants.rollerGearbox); + + rollerController = super.talonFX.getSimState(); + } + + @Override + @Trace + public void updateInputs(GenericRollerIOInputs inputs) { + rollerController.setSupplyVoltage(RobotController.getBatteryVoltage()); + double rollerVoltage = rollerController.getMotorVoltage(); + + rollerSim.setInputVoltage(rollerVoltage); + + rollerSim.update(GompeiLib.getLoopPeriod()); + + Angle rotorPosition = + Angle.ofBaseUnits( + rollerSim.getAngularPositionRad() * constants.rollerMotorGearRatio, Radians); + AngularVelocity rotorVelocity = + AngularVelocity.ofBaseUnits( + rollerSim.getAngularVelocityRadPerSec() * constants.rollerMotorGearRatio, + RadiansPerSecond); + rollerController.setRawRotorPosition(rotorPosition); + rollerController.setRotorVelocity(rotorVelocity); + + super.updateInputs(inputs); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/Vision.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/Vision.java new file mode 100644 index 00000000..fb491ae1 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/Vision.java @@ -0,0 +1,50 @@ +package edu.wpi.team190.gompeilib.subsystems.vision; + +import edu.wpi.first.apriltag.AprilTag; +import edu.wpi.first.apriltag.AprilTagFieldLayout; +import edu.wpi.first.networktables.NetworkTable; +import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.team190.gompeilib.core.utility.VirtualSubsystem; +import edu.wpi.team190.gompeilib.subsystems.vision.camera.Camera; +import java.util.function.Supplier; +import lombok.Getter; + +/** + * A class to contain all the {@link Camera Camera}s for a robot and methods to interact with them. + * A vision object publishes the data about the field to the robot, and runs the periodic method for + * all of its cameras. + */ +public class Vision extends VirtualSubsystem { + @Getter private final Camera[] cameras; + @Getter private final Supplier fieldLayoutSupplier; + + public Vision(Supplier fieldLayoutSupplier, Camera... cameras) { + this.cameras = cameras; + this.fieldLayoutSupplier = fieldLayoutSupplier; + + NetworkTable fieldTable = NetworkTableInstance.getDefault().getTable("field"); + + for (AprilTag tag : fieldLayoutSupplier.get().getTags()) { + fieldTable + .getDoubleArrayTopic("tag_" + tag.ID) + .publish() + .set( + new double[] { + tag.pose.getX(), + tag.pose.getY(), + tag.pose.getZ(), + tag.pose.getRotation().getQuaternion().getW(), + tag.pose.getRotation().getQuaternion().getX(), + tag.pose.getRotation().getQuaternion().getY(), + tag.pose.getRotation().getQuaternion().getZ() + }); + } + } + + @Override + public void periodic() { + for (Camera camera : cameras) { + camera.periodic(); + } + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/VisionConstants.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/VisionConstants.java new file mode 100644 index 00000000..16deba0c --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/VisionConstants.java @@ -0,0 +1,59 @@ +package edu.wpi.team190.gompeilib.subsystems.vision; + +import edu.wpi.first.math.Matrix; +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Transform3d; +import edu.wpi.first.math.numbers.N1; +import edu.wpi.first.math.numbers.N3; +import edu.wpi.first.math.numbers.N5; +import edu.wpi.team190.gompeilib.subsystems.vision.camera.CameraType; +import lombok.Builder; + +public class VisionConstants { + public static final double AMBIGUITY_THRESHOLD = 0.4; + public static final double XY_STDEV_DISTANCE_EXPONENT = 1.2; + public static final double XY_STDEV_TAG_COUNT_EXPONENT = 2.0; + + @Builder + public record StaticLimelightConfig( + String key, + CameraType cameraType, + double horizontalFOV, + double verticalFOV, + double megatagXYStdev, + double metatagThetaStdev, + double megatag2XYStdev, + Transform3d robotToCameraTransform, + Boolean enableRewind) {} + + @Builder + public record MovingLimelightConfig( + String key, + CameraType cameraType, + double horizontalFOV, + double verticalFOV, + double megatagXYStdev, + double metatagThetaStdev, + double megatag2XYStdev, + Transform3d robotToRotationAxisTransform, + Transform3d rotationAxisToLensTransform, + Boolean enableRewind) {} + + @Builder + public record GompeiVisionConfig( + String key, + String hardwareID, + CameraType cameraType, + double exposure, + double gain, + int width, + int height, + Matrix cameraMatrix, + Matrix distortionCoefficients, + double horizontalFOV, + double verticalFOV, + double singletagXYStdev, + double thetaStdev, + double multitagXYStdev, + Pose3d robotRelativePose) {} +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/Camera.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/Camera.java new file mode 100644 index 00000000..7745b76b --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/Camera.java @@ -0,0 +1,58 @@ +package edu.wpi.team190.gompeilib.subsystems.vision.camera; + +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.team190.gompeilib.core.logging.Trace; +import edu.wpi.team190.gompeilib.subsystems.vision.data.VisionMultiTxTyObservation; +import edu.wpi.team190.gompeilib.subsystems.vision.data.VisionPoseObservation; +import edu.wpi.team190.gompeilib.subsystems.vision.data.VisionSingleTxTyObservation; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import lombok.Getter; + +public abstract class Camera { + @Getter private final String name; + List>> poseObservers; + List>> multiTxTyObservers; + List>> singleTxTyObservers; + + @Getter List poseObservationList; + @Getter List multiTxTyObservationList; + @Getter List singleTxTyObservationList; + + @Getter protected Pose3d currentCameraPose; + + public Camera( + String name, + List>> poseObservers, + List>> multiTxTyObservers, + List>> singleTxTyObservers) { + this.name = name; + this.poseObservers = poseObservers; + this.multiTxTyObservers = multiTxTyObservers; + this.singleTxTyObservers = singleTxTyObservers; + + poseObservationList = new ArrayList<>(); + multiTxTyObservationList = new ArrayList<>(); + singleTxTyObservationList = new ArrayList<>(); + } + + @Trace + public void periodic() {} + + public void setCameraPose(Pose3d cameraPose) { + currentCameraPose = cameraPose; + } + + public void sendObservers() { + for (Consumer> observer : poseObservers) { + observer.accept(poseObservationList); + } + for (Consumer> observer : multiTxTyObservers) { + observer.accept(multiTxTyObservationList); + } + for (Consumer> observer : singleTxTyObservers) { + observer.accept(singleTxTyObservationList); + } + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraGompeiVision.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraGompeiVision.java new file mode 100644 index 00000000..69573856 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraGompeiVision.java @@ -0,0 +1,244 @@ +package edu.wpi.team190.gompeilib.subsystems.vision.camera; + +import edu.wpi.first.apriltag.AprilTagFieldLayout; +import edu.wpi.first.math.VecBuilder; +import edu.wpi.first.math.geometry.*; +import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.team190.gompeilib.core.utility.GeometryUtil; +import edu.wpi.team190.gompeilib.subsystems.vision.VisionConstants; +import edu.wpi.team190.gompeilib.subsystems.vision.VisionConstants.GompeiVisionConfig; +import edu.wpi.team190.gompeilib.subsystems.vision.data.VisionMultiTxTyObservation; +import edu.wpi.team190.gompeilib.subsystems.vision.data.VisionPoseObservation; +import edu.wpi.team190.gompeilib.subsystems.vision.io.CameraIOGompeiVision; +import edu.wpi.team190.gompeilib.subsystems.vision.io.GompeiVisionIOInputsAutoLogged; +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Supplier; +import lombok.Getter; +import lombok.experimental.ExtensionMethod; +import org.littletonrobotics.junction.Logger; + +@ExtensionMethod(GeometryUtil.class) +public class CameraGompeiVision extends Camera { + private final GompeiVisionIOInputsAutoLogged inputs; + private final CameraIOGompeiVision io; + + private final GompeiVisionConfig config; + private final Supplier aprilTagFieldLayoutSupplier; + private final double fieldBorderMarginMeters; + private final Supplier currentRobotPoseSupplier; + + @Getter private final String name; + + @Getter private final List allTagPoses; + @Getter private Pose2d robotPose; + + public CameraGompeiVision( + CameraIOGompeiVision io, + GompeiVisionConfig config, + Supplier aprilTagFieldLayoutSupplier, + double fieldBorderMarginMeters, + Supplier currentRobotPoseSupplier, + List>> poseObservers, + List>> txtyObservers) { + super(config.key(), poseObservers, txtyObservers, new ArrayList<>()); + + inputs = new GompeiVisionIOInputsAutoLogged(); + this.io = io; + + this.config = config; + this.aprilTagFieldLayoutSupplier = aprilTagFieldLayoutSupplier; + this.fieldBorderMarginMeters = fieldBorderMarginMeters; + this.currentRobotPoseSupplier = currentRobotPoseSupplier; + + this.name = this.config.key(); + + allTagPoses = new ArrayList<>(); + robotPose = Pose2d.kZero; + + currentCameraPose = config.robotRelativePose(); + } + + @Override + public void periodic() { + poseObservationList.clear(); + multiTxTyObservationList.clear(); + + io.updateInputs(inputs); + Logger.processInputs("Vision/Cameras/" + this.name, inputs); + + allTagPoses.clear(); + robotPose = Pose2d.kZero; + + for (int i = 0; i < inputs.frames.length; i++) { + double timestamp = inputs.timestamps[i]; + int totalTargets; + double averageDistance = 0.0; + double[] values = inputs.frames[i]; + + if (values.length == 0 || values[0] == 0) { + continue; + } + + Pose3d cameraPose = null; + Pose2d robotPose = null; + + switch ((int) values[0]) { + case 1: + // One pose + cameraPose = + new Pose3d( + values[2], + values[3], + values[4], + new Rotation3d(new Quaternion(values[5], values[6], values[7], values[8]))); + robotPose = + cameraPose + .toPose2d() + .transformBy(currentCameraPose.toPose2d().toTransform2d().inverse()); + break; + + case 2: + // Multiple poses + double error0 = values[1]; + double error1 = values[9]; + Pose3d cameraPose0 = + new Pose3d( + values[2], + values[3], + values[4], + new Rotation3d(new Quaternion(values[5], values[6], values[7], values[8]))); + + Pose3d cameraPose1 = + new Pose3d( + values[10], + values[11], + values[12], + new Rotation3d(new Quaternion(values[13], values[14], values[15], values[16]))); + Transform2d cameraToRobot = currentCameraPose.toPose2d().toTransform2d().inverse(); + Pose2d robotPose0 = cameraPose0.toPose2d().transformBy(cameraToRobot); + Pose2d robotPose1 = cameraPose1.toPose2d().transformBy(cameraToRobot); + + if (error0 < error1 * VisionConstants.AMBIGUITY_THRESHOLD + || error1 < error0 * VisionConstants.AMBIGUITY_THRESHOLD) { + Rotation2d currentRotation = currentRobotPoseSupplier.get().getRotation(); + Rotation2d visionRotation0 = robotPose0.getRotation(); + Rotation2d visionRotation1 = robotPose1.getRotation(); + if (Math.abs(currentRotation.minus(visionRotation0).getRadians()) + < Math.abs(currentRotation.minus(visionRotation1).getRadians())) { + robotPose = robotPose0; + cameraPose = cameraPose0; + } else { + robotPose = robotPose1; + cameraPose = cameraPose1; + } + } + + break; + default: + DriverStation.reportWarning("FAILED TO CAPTURE FRAMES", false); + continue; + } + + if (cameraPose == null || robotPose == null) { + continue; + } + + // Exit if robot pose is off the field + double fieldLength = aprilTagFieldLayoutSupplier.get().getFieldLength(); + double fieldWidth = aprilTagFieldLayoutSupplier.get().getFieldWidth(); + + if (robotPose.getX() < -fieldBorderMarginMeters + || robotPose.getX() > fieldLength + fieldBorderMarginMeters + || robotPose.getY() < -fieldBorderMarginMeters + || robotPose.getY() > fieldWidth + fieldBorderMarginMeters) { + continue; + } + + // Get tag poses and update last detection times + List tagPoses = new ArrayList<>(); + for (int j = (values[0] == 1 ? 9 : 17); j < values.length; j += 10) { + int tagId = (int) values[j]; + Optional tagPose = aprilTagFieldLayoutSupplier.get().getTagPose(tagId); + tagPose.ifPresent(tagPoses::add); + } + + // Prepare arrays + totalTargets = tagPoses.size(); + Set tagIds = new HashSet<>(); + + if (!tagPoses.isEmpty()) { + // Calculate average distance + double totalDistance = 0.0; + for (Pose3d tagPose : tagPoses) { + totalDistance += tagPose.getTranslation().getDistance(currentCameraPose.getTranslation()); + } + averageDistance = totalDistance / tagPoses.size(); + + // --- Parse tag angle + distance data --- + int tagEstimationDataEndIndex = + switch ((int) values[0]) { + case 1 -> 8; + case 2 -> 16; + default -> 0; + }; + + int indexCounter = 0; + + // Step through each 10-value chunk safely + for (int index = tagEstimationDataEndIndex + 1; index + 9 < values.length; index += 10) { + int tagId = (int) values[index]; + double[] tx = new double[4]; + double[] ty = new double[4]; + + // Read 4 corner pairs + for (int j = 0; j < 4; j++) { + tx[i] = values[index + 1 + (2 * i)]; + ty[i] = values[index + 1 + (2 * i) + 1]; + } + + double distance = values[index + 9]; + + // Store data + if (indexCounter < totalTargets) { + indexCounter++; + multiTxTyObservationList.add( + new VisionMultiTxTyObservation(tagId, tx, ty, distance, timestamp, cameraPose)); + tagIds.add(tagId); + } else { + System.out.println("[WARN] More tag data than expected: indexCounter=" + indexCounter); + } + } + + // Optional debug check + if ((values.length - (tagEstimationDataEndIndex + 1)) % 10 != 0) { + System.out.println( + "[WARN] Observation array not multiple of 10! Length=" + + values.length + + " start=" + + (tagEstimationDataEndIndex + 1)); + } + } + double xyStdevCoeff; + double thetaStdev; + + if (totalTargets > 1) { + xyStdevCoeff = config.multitagXYStdev(); + thetaStdev = + config.thetaStdev() * Math.pow(averageDistance, 1.2) / Math.pow(totalTargets, 2.0); + } else { + xyStdevCoeff = config.singletagXYStdev(); + thetaStdev = Double.POSITIVE_INFINITY; + } + + // Add observation to list + double xyStdDev = xyStdevCoeff * Math.pow(averageDistance, 1.2) / Math.pow(totalTargets, 2.0); + + poseObservationList.add( + new VisionPoseObservation( + robotPose, tagIds, timestamp, VecBuilder.fill(xyStdDev, xyStdDev, thetaStdev))); + } + + super.sendObservers(); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraMovingLimelight.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraMovingLimelight.java new file mode 100644 index 00000000..11ffb7fd --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraMovingLimelight.java @@ -0,0 +1,247 @@ +package edu.wpi.team190.gompeilib.subsystems.vision.camera; + +import static edu.wpi.first.units.Units.Degrees; + +import edu.wpi.first.math.VecBuilder; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Transform2d; +import edu.wpi.first.math.kinematics.ChassisSpeeds; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.networktables.DoubleArrayPublisher; +import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.first.wpilibj.Timer; +import edu.wpi.team190.gompeilib.core.utility.LimelightHelpers; +import edu.wpi.team190.gompeilib.subsystems.vision.VisionConstants; +import edu.wpi.team190.gompeilib.subsystems.vision.VisionConstants.MovingLimelightConfig; +import edu.wpi.team190.gompeilib.subsystems.vision.data.VisionPoseObservation; +import edu.wpi.team190.gompeilib.subsystems.vision.data.VisionSingleTxTyObservation; +import edu.wpi.team190.gompeilib.subsystems.vision.io.CameraIO; +import edu.wpi.team190.gompeilib.subsystems.vision.io.CameraIOLimelight; +import edu.wpi.team190.gompeilib.subsystems.vision.io.LimelightIOInputsAutoLogged; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.LongSupplier; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import lombok.Getter; +import org.littletonrobotics.junction.Logger; + +public class CameraMovingLimelight extends Camera { + private final LimelightIOInputsAutoLogged inputs; + private final CameraIOLimelight io; + + private final MovingLimelightConfig config; + @Getter private final String name; + + private final Supplier headingSupplier; + private final Supplier rotationAxisSupplier; + private final Supplier chassisSpeedsSupplier; + private final LongSupplier timestampSupplier; + private final DoubleArrayPublisher headingPublisher; + + @Getter private final List allTagPoses; + + private boolean wasEnabled; + private double enabledTimestamp; + + public CameraMovingLimelight( + CameraIOLimelight io, + MovingLimelightConfig config, + Supplier headingSupplier, + Supplier rotationAxisSupplier, + Supplier chassisSpeedsSupplier, + LongSupplier timestampSupplier, + List>> poseObservers, + List>> singleTxTyObservers) { + super(config.key(), poseObservers, new ArrayList<>(), singleTxTyObservers); + + inputs = new LimelightIOInputsAutoLogged(); + this.io = io; + + this.config = config; + this.name = "limelight-" + this.config.key(); + + this.headingSupplier = headingSupplier; + this.rotationAxisSupplier = rotationAxisSupplier; + this.chassisSpeedsSupplier = chassisSpeedsSupplier; + this.timestampSupplier = timestampSupplier; + this.headingPublisher = + NetworkTableInstance.getDefault() + .getTable(this.name) + .getDoubleArrayTopic("robot_orientation_set") + .publish(); + + allTagPoses = new ArrayList<>(); + + currentCameraPose = Pose3d.kZero.transformBy(config.robotToRotationAxisTransform()); + + LimelightHelpers.setCameraPose_RobotSpace( + name, + currentCameraPose.getX(), + -currentCameraPose.getY(), + currentCameraPose.getZ(), + currentCameraPose.getRotation().getMeasureX().in(Degrees), + currentCameraPose.getRotation().getMeasureY().in(Degrees), + currentCameraPose.getRotation().getMeasureZ().in(Degrees)); + + LimelightHelpers.SetIMUAssistAlpha(name, 0.0067); + LimelightHelpers.setRewindEnabled(name, config.enableRewind()); + + LimelightHelpers.SetIMUMode(name, 1); + LimelightHelpers.SetThrottle(name, 190); + + wasEnabled = false; + enabledTimestamp = Timer.getFPGATimestamp(); + } + + @Override + public void periodic() { + poseObservationList.clear(); + multiTxTyObservationList.clear(); + singleTxTyObservationList.clear(); + + currentCameraPose = + Pose3d.kZero + .transformBy(config.robotToRotationAxisTransform()) + .rotateAround( + currentCameraPose.getTranslation(), new Rotation3d(rotationAxisSupplier.get())) + .transformBy(config.rotationAxisToLensTransform()); + + if (DriverStation.isEnabled()) { + if (!wasEnabled) { + enabledTimestamp = Timer.getFPGATimestamp(); + wasEnabled = true; + LimelightHelpers.SetIMUMode(name, 0); + LimelightHelpers.SetThrottle(name, 0); + } + + if (Timer.getFPGATimestamp() - enabledTimestamp >= 165 && config.enableRewind()) { + LimelightHelpers.triggerRewindCapture(name, 165); + enabledTimestamp = Timer.getFPGATimestamp(); + } + } + + if (DriverStation.isDisabled()) { + if (wasEnabled) { + if (config.enableRewind()) { + LimelightHelpers.triggerRewindCapture(name, Timer.getFPGATimestamp() - enabledTimestamp); + } + wasEnabled = false; + LimelightHelpers.SetIMUMode(name, 1); + LimelightHelpers.SetThrottle(name, 190); + } + } + + headingPublisher.set( + new double[] { + -Units.radiansToDegrees(config.robotToRotationAxisTransform().getRotation().getZ()) + + rotationAxisSupplier.get().getDegrees() + + headingSupplier.get().getDegrees(), + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + }, + timestampSupplier.getAsLong()); + + io.updateInputs(inputs); + Logger.processInputs("Vision/Cameras/" + this.name, inputs); + + allTagPoses.clear(); + + double xyStdDev = config.megatagXYStdev(); + double thetaStdev = config.metatagThetaStdev(); + + if (inputs.mt1PoseEstimate.tagCount() != 0) { + xyStdDev = + config.megatagXYStdev() + * Math.pow( + inputs.mt1PoseEstimate.avgTagDist(), VisionConstants.XY_STDEV_DISTANCE_EXPONENT) + / Math.pow( + inputs.mt1PoseEstimate.tagCount(), VisionConstants.XY_STDEV_TAG_COUNT_EXPONENT); + thetaStdev = + inputs.mt1PoseEstimate.tagCount() > 1 + && Math.abs(chassisSpeedsSupplier.get().vxMetersPerSecond) <= 0.15 + && Math.abs(chassisSpeedsSupplier.get().vyMetersPerSecond) <= 0.15 + && Math.abs(chassisSpeedsSupplier.get().omegaRadiansPerSecond) <= 0.05 + && Arrays.stream(inputs.mt1PoseEstimate.rawFiducials()) + .mapToDouble(CameraIO.RawFiducial::ambiguity) + .average() + .orElse(Double.MAX_VALUE) + < VisionConstants.AMBIGUITY_THRESHOLD + ? config.metatagThetaStdev() + * Math.pow( + inputs.mt1PoseEstimate.avgTagDist(), + VisionConstants.XY_STDEV_DISTANCE_EXPONENT) + / Math.pow( + inputs.mt1PoseEstimate.tagCount(), + VisionConstants.XY_STDEV_TAG_COUNT_EXPONENT) + : Double.POSITIVE_INFINITY; + Pose2d tagPose = inputs.mt1PoseEstimate.pose(); + tagPose = + tagPose.rotateAround(tagPose.getTranslation(), rotationAxisSupplier.get().unaryMinus()); + Pose2d correctedRobotPose = + tagPose.transformBy( + new Transform2d( + config.rotationAxisToLensTransform().getTranslation().toTranslation2d(), + new Rotation2d())); + Logger.recordOutput("Vision/Cameras/" + this.name + "/MT1Pose", correctedRobotPose); + poseObservationList.add( + new VisionPoseObservation( + correctedRobotPose, + Arrays.stream(inputs.mt1PoseEstimate.rawFiducials()) + .map(CameraIO.RawFiducial::id) + .collect(Collectors.toSet()), + inputs.mt1PoseEstimate.timestampSeconds(), + VecBuilder.fill(xyStdDev, xyStdDev, thetaStdev))); + } + + if (inputs.mt2PoseEstimate.tagCount() != 0) { + xyStdDev = + config.megatag2XYStdev() + * Math.pow( + inputs.mt2PoseEstimate.avgTagDist(), VisionConstants.XY_STDEV_DISTANCE_EXPONENT) + / Math.pow( + inputs.mt2PoseEstimate.tagCount(), VisionConstants.XY_STDEV_TAG_COUNT_EXPONENT); + thetaStdev = Double.POSITIVE_INFINITY; + Pose2d tagPose = inputs.mt2PoseEstimate.pose(); + Pose2d limelightBelievedCameraPose = + new Pose3d().transformBy(config.robotToRotationAxisTransform()).toPose2d(); + Pose2d trueCameraPose = currentCameraPose.toPose2d(); + Transform2d correction = new Transform2d(limelightBelievedCameraPose, trueCameraPose); + Pose2d correctedRobotPose = tagPose.transformBy(correction.inverse()); + Logger.recordOutput("Vision/" + name + "/MT2Pose", correctedRobotPose); + poseObservationList.add( + new VisionPoseObservation( + correctedRobotPose, + Arrays.stream(inputs.mt2PoseEstimate.rawFiducials()) + .map(CameraIO.RawFiducial::id) + .collect(Collectors.toSet()), + inputs.mt2PoseEstimate.timestampSeconds(), + VecBuilder.fill(xyStdDev, xyStdDev, thetaStdev))); + } + + if (inputs.rawFiducials.length != 0) { + Arrays.stream(inputs.rawFiducials) + .forEach( + fiducial -> + singleTxTyObservationList.add( + new VisionSingleTxTyObservation( + fiducial.id(), + fiducial.txnc(), + fiducial.tync(), + fiducial.distToCamera(), + inputs.mt1PoseEstimate.timestampSeconds(), + currentCameraPose))); + } + + super.sendObservers(); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraStaticLimelight.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraStaticLimelight.java new file mode 100644 index 00000000..461ac8b8 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraStaticLimelight.java @@ -0,0 +1,220 @@ +package edu.wpi.team190.gompeilib.subsystems.vision.camera; + +import static edu.wpi.first.units.Units.Degrees; + +import edu.wpi.first.math.VecBuilder; +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.kinematics.ChassisSpeeds; +import edu.wpi.first.networktables.DoubleArrayPublisher; +import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.first.wpilibj.Timer; +import edu.wpi.team190.gompeilib.core.GompeiLib; +import edu.wpi.team190.gompeilib.core.utility.LimelightHelpers; +import edu.wpi.team190.gompeilib.subsystems.vision.VisionConstants; +import edu.wpi.team190.gompeilib.subsystems.vision.VisionConstants.StaticLimelightConfig; +import edu.wpi.team190.gompeilib.subsystems.vision.data.VisionPoseObservation; +import edu.wpi.team190.gompeilib.subsystems.vision.data.VisionSingleTxTyObservation; +import edu.wpi.team190.gompeilib.subsystems.vision.io.CameraIO; +import edu.wpi.team190.gompeilib.subsystems.vision.io.CameraIOLimelight; +import edu.wpi.team190.gompeilib.subsystems.vision.io.LimelightIOInputsAutoLogged; +import java.util.*; +import java.util.function.Consumer; +import java.util.function.LongSupplier; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import lombok.Getter; +import org.littletonrobotics.junction.Logger; + +public class CameraStaticLimelight extends Camera { + private final LimelightIOInputsAutoLogged inputs; + private final CameraIOLimelight io; + + private final StaticLimelightConfig config; + @Getter private final String name; + + private final Supplier headingSupplier; + private final Supplier chassisSpeedsSupplier; + private final LongSupplier timestampSupplier; + private final DoubleArrayPublisher headingPublisher; + + @Getter private final List allTagPoses; + + private boolean wasEnabled; + private double enabledTimestamp; + + public CameraStaticLimelight( + CameraIOLimelight io, + StaticLimelightConfig config, + Supplier headingSupplier, + Supplier chassisSpeedsSupplier, + LongSupplier timestampSupplier, + List>> poseObservers, + List>> singleTxTyObservers) { + super(config.key(), poseObservers, new ArrayList<>(), singleTxTyObservers); + + inputs = new LimelightIOInputsAutoLogged(); + this.io = io; + + this.config = config; + this.name = "limelight-" + this.config.key(); + + this.headingSupplier = headingSupplier; + this.chassisSpeedsSupplier = chassisSpeedsSupplier; + this.timestampSupplier = timestampSupplier; + this.headingPublisher = + NetworkTableInstance.getDefault() + .getTable(this.name) + .getDoubleArrayTopic("robot_orientation_set") + .publish(); + + allTagPoses = new ArrayList<>(); + + currentCameraPose = + new Pose3d( + config.robotToCameraTransform().getTranslation(), + config.robotToCameraTransform().getRotation()); + + LimelightHelpers.setCameraPose_RobotSpace( + name, + currentCameraPose.getX(), + -currentCameraPose.getY(), + currentCameraPose.getZ(), + currentCameraPose.getRotation().getMeasureX().in(Degrees), + currentCameraPose.getRotation().getMeasureY().in(Degrees), + currentCameraPose.getRotation().getMeasureZ().in(Degrees)); + + LimelightHelpers.SetIMUAssistAlpha(name, 0.0067); + LimelightHelpers.setRewindEnabled(name, config.enableRewind()); + + LimelightHelpers.SetIMUMode(name, 1); + if (GompeiLib.isTuning()) { + LimelightHelpers.SetThrottle(name, 0); + } else { + LimelightHelpers.SetIMUMode(name, 190); + } + + wasEnabled = false; + enabledTimestamp = Timer.getFPGATimestamp(); + } + + @Override + public void periodic() { + poseObservationList.clear(); + multiTxTyObservationList.clear(); + singleTxTyObservationList.clear(); + + if (DriverStation.isEnabled()) { + if (!wasEnabled) { + enabledTimestamp = Timer.getFPGATimestamp(); + wasEnabled = true; + LimelightHelpers.SetIMUMode(name, 0); + LimelightHelpers.SetThrottle(name, 0); + } + + if (Timer.getFPGATimestamp() - enabledTimestamp >= 165 && config.enableRewind()) { + LimelightHelpers.triggerRewindCapture(name, 165); + enabledTimestamp = Timer.getFPGATimestamp(); + } + } + + if (DriverStation.isDisabled()) { + if (wasEnabled) { + if (config.enableRewind()) { + LimelightHelpers.triggerRewindCapture(name, Timer.getFPGATimestamp() - enabledTimestamp); + } + wasEnabled = false; + LimelightHelpers.SetIMUMode(name, 1); + if (GompeiLib.isTuning()) { + LimelightHelpers.SetThrottle(name, 0); + } else { + LimelightHelpers.SetIMUMode(name, 190); + } + } + } + + headingPublisher.set( + new double[] {headingSupplier.get().getDegrees(), 0.0, 0.0, 0.0, 0.0, 0.0}, + timestampSupplier.getAsLong()); + + io.updateInputs(inputs); + Logger.processInputs("Vision/Cameras/" + this.name, inputs); + + allTagPoses.clear(); + + double xyStdDev = config.megatagXYStdev(); + double thetaStdev = config.metatagThetaStdev(); + + if (inputs.mt1PoseEstimate.tagCount() != 0) { + xyStdDev = + config.megatagXYStdev() + * Math.pow( + inputs.mt1PoseEstimate.avgTagDist(), VisionConstants.XY_STDEV_DISTANCE_EXPONENT) + / Math.pow( + inputs.mt1PoseEstimate.tagCount(), VisionConstants.XY_STDEV_TAG_COUNT_EXPONENT); + thetaStdev = + inputs.mt1PoseEstimate.tagCount() > 1 + && Math.abs(chassisSpeedsSupplier.get().vxMetersPerSecond) <= 0.15 + && Math.abs(chassisSpeedsSupplier.get().vyMetersPerSecond) <= 0.15 + && Math.abs(chassisSpeedsSupplier.get().omegaRadiansPerSecond) <= 0.05 + && Arrays.stream(inputs.mt1PoseEstimate.rawFiducials()) + .mapToDouble(CameraIO.RawFiducial::ambiguity) + .average() + .orElse(Double.MAX_VALUE) + < VisionConstants.AMBIGUITY_THRESHOLD + ? config.metatagThetaStdev() + * Math.pow( + inputs.mt1PoseEstimate.avgTagDist(), + VisionConstants.XY_STDEV_DISTANCE_EXPONENT) + / Math.pow( + inputs.mt1PoseEstimate.tagCount(), + VisionConstants.XY_STDEV_TAG_COUNT_EXPONENT) + : Double.POSITIVE_INFINITY; + + poseObservationList.add( + new VisionPoseObservation( + inputs.mt1PoseEstimate.pose(), + Arrays.stream(inputs.mt1PoseEstimate.rawFiducials()) + .map(CameraIO.RawFiducial::id) + .collect(Collectors.toSet()), + inputs.mt1PoseEstimate.timestampSeconds(), + VecBuilder.fill(xyStdDev, xyStdDev, thetaStdev))); + } + + if (inputs.mt2PoseEstimate.tagCount() != 0) { + xyStdDev = + config.megatag2XYStdev() + * Math.pow( + inputs.mt2PoseEstimate.avgTagDist(), VisionConstants.XY_STDEV_DISTANCE_EXPONENT) + / Math.pow( + inputs.mt2PoseEstimate.tagCount(), VisionConstants.XY_STDEV_TAG_COUNT_EXPONENT); + thetaStdev = Double.POSITIVE_INFINITY; + + poseObservationList.add( + new VisionPoseObservation( + inputs.mt2PoseEstimate.pose(), + Arrays.stream(inputs.mt2PoseEstimate.rawFiducials()) + .map(CameraIO.RawFiducial::id) + .collect(Collectors.toSet()), + inputs.mt2PoseEstimate.timestampSeconds(), + VecBuilder.fill(xyStdDev, xyStdDev, thetaStdev))); + } + + if (inputs.rawFiducials.length != 0) { + Arrays.stream(inputs.rawFiducials) + .forEach( + fiducial -> + singleTxTyObservationList.add( + new VisionSingleTxTyObservation( + fiducial.id(), + fiducial.txnc(), + fiducial.tync(), + fiducial.distToCamera(), + inputs.mt1PoseEstimate.timestampSeconds(), + currentCameraPose))); + } + + super.sendObservers(); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraType.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraType.java new file mode 100644 index 00000000..9970f11d --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraType.java @@ -0,0 +1,120 @@ +package edu.wpi.team190.gompeilib.subsystems.vision.camera; + +import edu.wpi.first.math.util.Units; + +public enum CameraType { + LIMELIGHT_2_PLUS( + Limelight2PlusConstants.HORIZONTAL_FOV, + Limelight2PlusConstants.VERTICAL_FOV, + Limelight2PlusConstants.MEGATAG_XY_STANDARD_DEVIATION_COEFFICIENT, + Limelight2PlusConstants.MEGATAG_2_XY_STANDARD_DEVIATION_COEFFICIENT, + Limelight2PlusConstants.MEGATAG_THETA_STANDARD_DEVIATION_COEFFICIENT), + LIMELIGHT_3( + Limelight3Constants.HORIZONTAL_FOV, + Limelight3Constants.VERTICAL_FOV, + Limelight3Constants.MEGATAG_2_XY_STANDARD_DEVIATION_COEFFICIENT, + Limelight3Constants.MEGATAG_XY_STANDARD_DEVIATION_COEFFICIENT, + Limelight3Constants.MEGATAG_THETA_STANDARD_DEVIATION_COEFFICIENT), + LIMELIGHT_3G( + Limelight3GConstants.HORIZONTAL_FOV, + Limelight3GConstants.VERTICAL_FOV, + Limelight3GConstants.MEGATAG_2_XY_STANDARD_DEVIATION_COEFFICIENT, + Limelight3GConstants.MEGATAG_XY_STANDARD_DEVIATION_COEFFICIENT, + Limelight3GConstants.MEGATAG_THETA_STANDARD_DEVIATION_COEFFICIENT), + LIMELIGHT_4( + Limelight4Constants.HORIZONTAL_FOV, + Limelight4Constants.VERTICAL_FOV, + Limelight4Constants.MEGATAG_2_XY_STANDARD_DEVIATION_COEFFICIENT, + Limelight4Constants.MEGATAG_XY_STANDARD_DEVIATION_COEFFICIENT, + Limelight4Constants.MEGATAG_THETA_STANDARD_DEVIATION_COEFFICIENT), + THRIFTYCAM( + ThriftyCamConstants.HORIZONTAL_FOV, + ThriftyCamConstants.VERTICAL_FOV, + ThriftyCamConstants.SINGLETAG_XY_STANDARD_DEVIATION_COEFFICIENT, + ThriftyCamConstants.MULTITAG_XY_STANDARD_DEVIATION_COEFFICIENT, + ThriftyCamConstants.THETA_STANDARD_DEVIATION_COEFFICIENT), + DEFAULT(); + + public final double horizontalFOV; + public final double verticalFOV; + public final double primaryXYStandardDeviationCoefficient; + public final double secondaryXYStandardDeviationCoefficient; + public final double primaryThetaStandardDeviationCoefficient; + + private CameraType( + double horizontalFOV, + double verticalFOV, + double primaryXYStandardDeviationCoefficient, + double secondaryXYStandardDeviationCoefficient, + double primaryThetaStandardDeviationCoefficient) { + this.horizontalFOV = horizontalFOV; + this.verticalFOV = verticalFOV; + this.primaryXYStandardDeviationCoefficient = primaryXYStandardDeviationCoefficient; + this.secondaryXYStandardDeviationCoefficient = secondaryXYStandardDeviationCoefficient; + this.primaryThetaStandardDeviationCoefficient = primaryThetaStandardDeviationCoefficient; + } + + private CameraType( + double horizontalFOV, + double verticalFOV, + double xyStandardDeviationCoefficient, + double thetaStandardDeviationCoefficient) { + this.horizontalFOV = horizontalFOV; + this.verticalFOV = verticalFOV; + this.primaryXYStandardDeviationCoefficient = xyStandardDeviationCoefficient; + this.secondaryXYStandardDeviationCoefficient = xyStandardDeviationCoefficient; + this.primaryThetaStandardDeviationCoefficient = thetaStandardDeviationCoefficient; + } + + private CameraType() { + this.horizontalFOV = 0.0; + this.verticalFOV = 0.0; + this.primaryXYStandardDeviationCoefficient = 0.0; + this.secondaryXYStandardDeviationCoefficient = 0.0; + this.primaryThetaStandardDeviationCoefficient = 0.0; + } + + public static final double BLINK_TIME = 0.067; + + private static class Limelight2PlusConstants { + private static final double HORIZONTAL_FOV = Units.degreesToRadians(62.5); + private static final double VERTICAL_FOV = Units.degreesToRadians(48.9); + private static final double MEGATAG_XY_STANDARD_DEVIATION_COEFFICIENT = 0.1; + private static final double MEGATAG_THETA_STANDARD_DEVIATION_COEFFICIENT = 0.1; + private static final double MEGATAG_2_XY_STANDARD_DEVIATION_COEFFICIENT = 0.1; + } + + private static class Limelight3Constants { + private static final double HORIZONTAL_FOV = Units.degreesToRadians(62.5); + private static final double VERTICAL_FOV = Units.degreesToRadians(48.9); + private static final double MEGATAG_XY_STANDARD_DEVIATION_COEFFICIENT = 0.1; + private static final double MEGATAG_THETA_STANDARD_DEVIATION_COEFFICIENT = 0.1; + private static final double MEGATAG_2_XY_STANDARD_DEVIATION_COEFFICIENT = 0.1; + } + + private static class Limelight3GConstants { + private static final double HORIZONTAL_FOV = Units.degreesToRadians(82.0); + private static final double VERTICAL_FOV = Units.degreesToRadians(46.2); + private static final double MEGATAG_XY_STANDARD_DEVIATION_COEFFICIENT = 0.05; + private static final double MEGATAG_THETA_STANDARD_DEVIATION_COEFFICIENT = 0.1; + private static final double MEGATAG_2_XY_STANDARD_DEVIATION_COEFFICIENT = 0.00015; + } + + private static class Limelight4Constants { + private static final double HORIZONTAL_FOV = Units.degreesToRadians(82.0); + private static final double VERTICAL_FOV = Units.degreesToRadians(46.2); + private static final double MEGATAG_XY_STANDARD_DEVIATION_COEFFICIENT = 0.05; + private static final double MEGATAG_THETA_STANDARD_DEVIATION_COEFFICIENT = 10; + private static final double MEGATAG_2_XY_STANDARD_DEVIATION_COEFFICIENT = 0.00015; + } + + private static class ThriftyCamConstants { + private static final double HORIZONTAL_FOV = Units.degreesToRadians(82.0); + private static final double VERTICAL_FOV = Units.degreesToRadians(46.2); + private static final double SINGLETAG_XY_STANDARD_DEVIATION_COEFFICIENT = 0.05; + private static final double THETA_STANDARD_DEVIATION_COEFFICIENT = 0.1; + private static final double MULTITAG_XY_STANDARD_DEVIATION_COEFFICIENT = 0.00015; + private static final int WIDTH = 1600; + private static final int HEIGHT = 1304; + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/data/VisionMultiTxTyObservation.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/data/VisionMultiTxTyObservation.java new file mode 100644 index 00000000..dc0db831 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/data/VisionMultiTxTyObservation.java @@ -0,0 +1,6 @@ +package edu.wpi.team190.gompeilib.subsystems.vision.data; + +import edu.wpi.first.math.geometry.Pose3d; + +public record VisionMultiTxTyObservation( + int tagId, double[] tx, double[] ty, double distance, double timestamp, Pose3d cameraPose) {} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/data/VisionPoseObservation.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/data/VisionPoseObservation.java new file mode 100644 index 00000000..5aa31e2d --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/data/VisionPoseObservation.java @@ -0,0 +1,10 @@ +package edu.wpi.team190.gompeilib.subsystems.vision.data; + +import edu.wpi.first.math.Matrix; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.numbers.N1; +import edu.wpi.first.math.numbers.N3; +import java.util.Set; + +public record VisionPoseObservation( + Pose2d pose, Set tagIds, double timestamp, Matrix stddevs) {} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/data/VisionSingleTxTyObservation.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/data/VisionSingleTxTyObservation.java new file mode 100644 index 00000000..a4dcb206 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/data/VisionSingleTxTyObservation.java @@ -0,0 +1,6 @@ +package edu.wpi.team190.gompeilib.subsystems.vision.data; + +import edu.wpi.first.math.geometry.Pose3d; + +public record VisionSingleTxTyObservation( + int tagId, double tx, double ty, double distance, double timestamp, Pose3d cameraPose) {} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/io/CameraIO.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/io/CameraIO.java new file mode 100644 index 00000000..00253872 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/io/CameraIO.java @@ -0,0 +1,110 @@ +package edu.wpi.team190.gompeilib.subsystems.vision.io; + +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.team190.gompeilib.core.utility.LimelightHelpers; +import edu.wpi.team190.gompeilib.subsystems.vision.camera.Camera; +import java.util.Arrays; +import org.littletonrobotics.junction.AutoLog; + +/** + * An interface for the hardware implementation of a {@link Camera Camera}. Contains the methods + * necessary for providing information to a camera, like its pipeline, and getting information out + * of a camera, like its individual robot pose estimate. + */ +public interface CameraIO { + + @AutoLog + public static class GompeiVisionIOInputs { + public double[] timestamps = new double[] {}; + public double[][] frames = new double[][] {}; + public double captureFPS = 0; + public double processingFPS = 0; + } + + @AutoLog + public static class LimelightIOInputs { + public PoseEstimate mt1PoseEstimate = new PoseEstimate(); + public PoseEstimate mt2PoseEstimate = new PoseEstimate(); + public RawFiducial[] rawFiducials = {}; + } + + public default void updateInputs(GompeiVisionIOInputs inputs) {} + + public default void updateInputs(LimelightIOInputs inputs) {} + + public default String getName() { + return ""; + } + + public record PoseEstimate( + Pose2d pose, + double timestampSeconds, + double latency, + int tagCount, + double tagSpan, + double avgTagDist, + double avgTagArea, + RawFiducial[] rawFiducials, + boolean isMegaTag2) { + public PoseEstimate() { + this(Pose2d.kZero, 0, 0, 0, 0, 0, 0, new RawFiducial[] {}, false); + } + + public PoseEstimate(LimelightHelpers.PoseEstimate poseEstimate) { + this( + poseEstimate.pose, + poseEstimate.timestampSeconds, + poseEstimate.latency, + poseEstimate.tagCount, + poseEstimate.tagSpan, + poseEstimate.avgTagDist, + poseEstimate.avgTagArea, + Arrays.stream(poseEstimate.rawFiducials) + .map(RawFiducial::new) + .toArray(RawFiducial[]::new), + poseEstimate.isMegaTag2); + } + } + + public record RawFiducial( + int id, + double txnc, + double tync, + double ta, + double distToCamera, + double distToRobot, + double ambiguity) { + public RawFiducial() { + this(0, 0, 0, 0, 0, 0, 0); + } + + public RawFiducial(LimelightHelpers.RawFiducial rawFiducial) { + this( + rawFiducial.id, + rawFiducial.txnc, + rawFiducial.tync, + rawFiducial.ta, + rawFiducial.distToCamera, + rawFiducial.distToRobot, + rawFiducial.ambiguity); + } + } + + public record RawDetection( + int classId, + double txnc, + double tync, + double ta, + double corner0_X, + double corner0_Y, + double corner1_X, + double corner1_Y, + double corner2_X, + double corner2_Y, + double corner3_X, + double corner3_Y) { + public RawDetection() { + this(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + } + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/io/CameraIOGompeiVision.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/io/CameraIOGompeiVision.java new file mode 100644 index 00000000..f661f1dc --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/io/CameraIOGompeiVision.java @@ -0,0 +1,73 @@ +package edu.wpi.team190.gompeilib.subsystems.vision.io; + +import edu.wpi.first.math.util.Units; +import edu.wpi.first.networktables.*; +import edu.wpi.team190.gompeilib.subsystems.vision.VisionConstants; +import java.util.*; +import lombok.Getter; + +public class CameraIOGompeiVision implements CameraIO { + @Getter private final String name; + + private final DoubleArraySubscriber observationSubscriber; + private final IntegerSubscriber captureFPSAprilTagSubscriber; + private final IntegerSubscriber processingFPSAprilTagSubscriber; + + public CameraIOGompeiVision(VisionConstants.GompeiVisionConfig config) { + + this.name = config.key(); + + NetworkTable configTable = + NetworkTableInstance.getDefault() + .getTable("cameras") + .getSubTable(this.name) + .getSubTable("config"); + NetworkTable outputTable = + NetworkTableInstance.getDefault() + .getTable("cameras") + .getSubTable(this.name) + .getSubTable("output"); + + String deviceId = config.key(); + + configTable.getStringTopic("role").publish().set(name); + configTable.getStringTopic("hardware_id").publish().set(deviceId); + configTable.getDoubleArrayTopic("camera_matrix").publish().set(config.cameraMatrix().getData()); + configTable + .getDoubleArrayTopic("distortion_coefficients") + .publish() + .set(config.distortionCoefficients().getData()); + configTable.getDoubleTopic("exposure").publish().set(config.exposure()); + configTable.getDoubleTopic("gain").publish().set(config.gain()); + configTable.getIntegerTopic("width").publish().set(config.width()); + configTable.getIntegerTopic("height").publish().set(config.height()); + configTable.getDoubleTopic("fiducial_size_m").publish().set(Units.inchesToMeters(6.50)); + configTable.getBooleanTopic("setup_mode").publish().set(false); + + this.observationSubscriber = + outputTable + .getDoubleArrayTopic("observations") + .subscribe( + new double[] {}, + PubSubOption.keepDuplicates(true), + PubSubOption.sendAll(true), + PubSubOption.pollStorage(5), + PubSubOption.periodic(0.01)); + this.captureFPSAprilTagSubscriber = outputTable.getIntegerTopic("capture_fps").subscribe(0); + this.processingFPSAprilTagSubscriber = + outputTable.getIntegerTopic("processing_fps").subscribe(0); + } + + @Override + public void updateInputs(GompeiVisionIOInputs inputs) { + var aprilTagQueue = observationSubscriber.readQueue(); + inputs.timestamps = new double[aprilTagQueue.length]; + inputs.frames = new double[aprilTagQueue.length][]; + for (int i = 0; i < aprilTagQueue.length; i++) { + inputs.timestamps[i] = aprilTagQueue[i].timestamp / 1000000.0; + inputs.frames[i] = aprilTagQueue[i].value; + } + inputs.captureFPS = captureFPSAprilTagSubscriber.get(); + inputs.processingFPS = processingFPSAprilTagSubscriber.get(); + } +} diff --git a/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/io/CameraIOLimelight.java b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/io/CameraIOLimelight.java new file mode 100644 index 00000000..577166b5 --- /dev/null +++ b/lib/src/main/java/edu/wpi/team190/gompeilib/subsystems/vision/io/CameraIOLimelight.java @@ -0,0 +1,29 @@ +package edu.wpi.team190.gompeilib.subsystems.vision.io; + +import edu.wpi.team190.gompeilib.core.utility.LimelightHelpers; +import edu.wpi.team190.gompeilib.subsystems.vision.VisionConstants; +import java.util.Arrays; +import lombok.Getter; + +public class CameraIOLimelight implements CameraIO { + @Getter private final String name; + + public CameraIOLimelight(VisionConstants.StaticLimelightConfig config) { + this.name = "limelight-" + config.key(); + } + + public CameraIOLimelight(VisionConstants.MovingLimelightConfig config) { + this.name = "limelight-" + config.key(); + } + + @Override + public void updateInputs(LimelightIOInputs inputs) { + inputs.mt1PoseEstimate = new PoseEstimate(LimelightHelpers.getBotPoseEstimate_wpiBlue(name)); + inputs.mt2PoseEstimate = + new PoseEstimate(LimelightHelpers.getBotPoseEstimate_wpiBlue_MegaTag2(name)); + inputs.rawFiducials = + Arrays.stream(LimelightHelpers.getRawFiducials(name)) + .map(RawFiducial::new) + .toArray(RawFiducial[]::new); + } +} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/GompeiLibTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/GompeiLibTest.java new file mode 100644 index 00000000..9b73566d --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/GompeiLibTest.java @@ -0,0 +1,140 @@ +package edu.wpi.team190.gompeilib.core; + +import static org.junit.jupiter.api.Assertions.*; + +import edu.wpi.team190.gompeilib.core.robot.RobotMode; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; +import lombok.Builder; +import org.junit.jupiter.api.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.FieldSource; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class GompeiLibTest { + + private static final List TEST_PARAMETERS = + List.of( + GompeiLibTestParameters.builder() + .withMode(RobotMode.REAL) + .withIsTuning(true) + .withLoopPeriodSecs(0.02) + .build(), + GompeiLibTestParameters.builder() + .withMode(RobotMode.REAL) + .withIsTuning(false) + .withLoopPeriodSecs(0.02) + .build(), + GompeiLibTestParameters.builder() + .withMode(RobotMode.SIM) + .withIsTuning(true) + .withLoopPeriodSecs(0.02) + .build(), + GompeiLibTestParameters.builder() + .withMode(RobotMode.SIM) + .withIsTuning(false) + .withLoopPeriodSecs(0.02) + .build(), + GompeiLibTestParameters.builder() + .withMode(RobotMode.REPLAY) + .withIsTuning(true) + .withLoopPeriodSecs(0.02) + .build(), + GompeiLibTestParameters.builder() + .withMode(RobotMode.REPLAY) + .withIsTuning(false) + .withLoopPeriodSecs(0.02) + .build()); + + @BeforeEach + void resetGompeiLib() { + GompeiLib.deinit(); + } + + @Test + @Order(1) + void testConstruction() { + new GompeiLib(); + } + + @Test + @Order(2) + void testInitializationWhenNotInitialized() throws Exception { + Method checkInitializedMethod = GompeiLib.class.getDeclaredMethod("checkInitialized"); + checkInitializedMethod.setAccessible(true); + InvocationTargetException ex = + assertThrows( + java.lang.reflect.InvocationTargetException.class, + () -> checkInitializedMethod.invoke(null)); + + // Make sure the cause is IllegalStateException + assertInstanceOf(IllegalStateException.class, ex.getCause()); + } + + @Test + @Order(3) + void testInitializationWhenAlreadyInitialized() { + // Initialize once + GompeiLib.init(RobotMode.SIM, true, 0.02); + + // Capture System.err output + ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + PrintStream originalErr = System.err; + System.setErr(new PrintStream(errContent)); + + // Call init again -> should hit the branch + GompeiLib.init(RobotMode.SIM, true, 0.02); + + // Restore System.err + System.setErr(originalErr); + + // Check that the correct message was printed + String output = errContent.toString(); + assertTrue(output.contains("GompeiLib has already been initialized!")); + } + + @Test + @Order(4) + void testDeinitializationFlow() { + GompeiLib.init(RobotMode.SIM, true, 0.02); + GompeiLib.deinit(); + + // The getters should throw IllegalStateException now because we are deinitialized + assertThrows(IllegalStateException.class, GompeiLib::getMode); + } + + @Test + @Order(5) + void testDoubleDeinitWarning() { + GompeiLib.deinit(); // Ensure it is already deinitialized + + ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + PrintStream originalErr = System.err; + System.setErr(new PrintStream(errContent)); + + try { + GompeiLib.deinit(); // Call it while the stream is redirected + } finally { + System.setErr(originalErr); // Always restore in a finally block for safety + } + + assertTrue(errContent.toString().contains("GompeiLib has already been deinitialized!")); + } + + @ParameterizedTest + @FieldSource("TEST_PARAMETERS") + @Order(6) + void testGompeiLib(GompeiLibTestParameters params) { + GompeiLib.init(params.mode, params.isTuning, params.loopPeriodSecs); + + assertEquals(GompeiLib.getMode(), params.mode); + assertEquals(GompeiLib.isTuning(), params.isTuning); + assertEquals(GompeiLib.getLoopPeriod(), params.loopPeriodSecs); + } + + @Builder(setterPrefix = "with") + private record GompeiLibTestParameters(RobotMode mode, boolean isTuning, double loopPeriodSecs) {} +} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/io/inertial/GyroIOPigeon2Test.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/io/inertial/GyroIOPigeon2Test.java new file mode 100644 index 00000000..9f0d2970 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/io/inertial/GyroIOPigeon2Test.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.core.io.inertial; + +public class GyroIOPigeon2Test {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/io/inertial/GyroIOTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/io/inertial/GyroIOTest.java new file mode 100644 index 00000000..8e86bd9f --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/io/inertial/GyroIOTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.core.io.inertial; + +public class GyroIOTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/logging/TraceTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/logging/TraceTest.java new file mode 100644 index 00000000..422e2fa4 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/logging/TraceTest.java @@ -0,0 +1,61 @@ +package edu.wpi.team190.gompeilib.core.logging; + +import static org.junit.jupiter.api.Assertions.*; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; +import org.junit.jupiter.api.Test; + +class TraceTest { + + @Trace + void annotatedMethod() {} + + void nonAnnotatedMethod() {} + + @Test + void testRetentionPolicyIsRuntime() { + Retention retention = Trace.class.getAnnotation(Retention.class); + assertNotNull(retention); + assertEquals(RetentionPolicy.RUNTIME, retention.value()); + } + + @Test + void testTargetIsMethod() { + Target target = Trace.class.getAnnotation(Target.class); + assertNotNull(target); + + boolean supportsMethod = false; + for (ElementType type : target.value()) { + if (type == ElementType.METHOD) { + supportsMethod = true; + break; + } + } + + assertTrue(supportsMethod); + } + + @Test + void testAnnotationPresentOnMethod() throws Exception { + Method method = TraceTest.class.getDeclaredMethod("annotatedMethod"); + + assertTrue(method.isAnnotationPresent(Trace.class)); + } + + @Test + void testAnnotationAbsentWhenNotDeclared() throws Exception { + Method method = TraceTest.class.getDeclaredMethod("nonAnnotatedMethod"); + + assertFalse(method.isAnnotationPresent(Trace.class)); + } + + @Test + void testAnnotationMetadataAccessible() { + assertNotNull(Trace.class.getAnnotation(Retention.class)); + assertNotNull(Trace.class.getAnnotation(Target.class)); + } +} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/robot/RobotContainerTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/robot/RobotContainerTest.java new file mode 100644 index 00000000..75cfb2c4 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/robot/RobotContainerTest.java @@ -0,0 +1,32 @@ +package edu.wpi.team190.gompeilib.core.robot; + +import static org.junit.jupiter.api.Assertions.*; + +import edu.wpi.first.wpilibj2.command.Command; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class RobotContainerTest { + + /** Minimal concrete implementation of RobotContainer for testing. */ + static class TestContainer implements RobotContainer {} + + @Test + void testGetAutonomousCommand() { + RobotContainer container = Mockito.mock(TestContainer.class, Mockito.CALLS_REAL_METHODS); + + // Call the default method + Command cmd = container.getAutonomousCommand(); + + // Ensure it returns something (mocked default Commands.none() safe) + assertNotNull(cmd); + } + + @Test + void testRobotPeriodic() { + RobotContainer container = Mockito.mock(TestContainer.class, Mockito.CALLS_REAL_METHODS); + + // Call the default method + container.robotPeriodic(); // just runs, should not throw + } +} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/robot/RobotModeTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/robot/RobotModeTest.java new file mode 100644 index 00000000..33bd6679 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/robot/RobotModeTest.java @@ -0,0 +1,20 @@ +package edu.wpi.team190.gompeilib.core.robot; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class RobotModeTest { + + @Test + void testEnumValues() { + // Ensure all enum constants exist + RobotMode[] modes = RobotMode.values(); + + assertEquals(3, modes.length, "There should be exactly 3 modes"); + + assertEquals(RobotMode.REAL, RobotMode.valueOf("REAL")); + assertEquals(RobotMode.SIM, RobotMode.valueOf("SIM")); + assertEquals(RobotMode.REPLAY, RobotMode.valueOf("REPLAY")); + } +} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/robot/RobotStateTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/robot/RobotStateTest.java new file mode 100644 index 00000000..f0bef720 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/robot/RobotStateTest.java @@ -0,0 +1,19 @@ +package edu.wpi.team190.gompeilib.core.robot; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class RobotStateTest { + + /** Minimal concrete implementation of RobotState for testing. */ + static class TestState implements RobotState {} + + @Test + void testPeriodic() { + // Use Mockito to safely call default method + RobotState state = Mockito.mock(TestState.class, Mockito.CALLS_REAL_METHODS); + + // Call the default method + state.periodic(); // just runs, should not throw + } +} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/state/localization/EstimationRegionTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/state/localization/EstimationRegionTest.java new file mode 100644 index 00000000..44996481 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/state/localization/EstimationRegionTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.core.state.localization; + +public class EstimationRegionTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/state/localization/FieldZoneTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/state/localization/FieldZoneTest.java new file mode 100644 index 00000000..100f0ad7 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/state/localization/FieldZoneTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.core.state.localization; + +public class FieldZoneTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/state/localization/LocalizationConstantsTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/state/localization/LocalizationConstantsTest.java new file mode 100644 index 00000000..94aad0fb --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/state/localization/LocalizationConstantsTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.core.state.localization; + +public class LocalizationConstantsTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/state/localization/LocalizationTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/state/localization/LocalizationTest.java new file mode 100644 index 00000000..d8ed5250 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/state/localization/LocalizationTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.core.state.localization; + +public class LocalizationTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/GeometryUtilTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/GeometryUtilTest.java new file mode 100644 index 00000000..15b13d97 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/GeometryUtilTest.java @@ -0,0 +1,227 @@ +package edu.wpi.team190.gompeilib.core.utility; + +import static org.junit.jupiter.api.Assertions.*; + +import edu.wpi.first.math.Pair; +import edu.wpi.first.math.geometry.*; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.FieldSource; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class GeometryUtilTest { + public static Translation2d[] TRANSLATION2D_CASES; + public static Pair[] XY_PAIR_CASES; + public static Rotation2d[] ROTATION2D_CASES; + public static Pose2d[] POSE2D_CASES; + + static { + TRANSLATION2D_CASES = + new Translation2d[] { + Translation2d.kZero, new Translation2d(190.0, 190.0), new Translation2d(42.0, 42.0) + }; + XY_PAIR_CASES = + new Pair[] {new Pair<>(0.0, 0.0), new Pair<>(190.0, 190.0), new Pair<>(42.0, 42.0)}; + ROTATION2D_CASES = + new Rotation2d[] { + Rotation2d.kZero, Rotation2d.fromDegrees(190.0), Rotation2d.fromDegrees(42.0) + }; + POSE2D_CASES = + new Pose2d[] { + Pose2d.kZero, + new Pose2d(190.0, 190.0, Rotation2d.fromDegrees(190.0)), + new Pose2d(42.0, 42.0, Rotation2d.fromDegrees(42.0)) + }; + } + + @Test + @Order(1) + void testConstruction() { + new GeometryUtil(); + } + + @ParameterizedTest + @FieldSource("TRANSLATION2D_CASES") + @Order(2) + void testToTransform2dGivenTranslation(Translation2d translation) { + assertEquals( + GeometryUtil.toTransform2d(translation), + new Transform2d(translation.getX(), translation.getY(), new Rotation2d())); + } + + @ParameterizedTest + @FieldSource("XY_PAIR_CASES") + @Order(3) + void testToTransform2dGivenDoubles(Pair xyPair) { + double x = xyPair.getFirst(); + double y = xyPair.getSecond(); + + assertEquals(GeometryUtil.toTransform2d(x, y), new Transform2d(x, y, new Rotation2d())); + } + + @ParameterizedTest + @FieldSource("ROTATION2D_CASES") + @Order(4) + void testToTransform2dGivenRotation2d(Rotation2d rotation) { + assertEquals( + GeometryUtil.toTransform2d(rotation), new Transform2d(new Translation2d(), rotation)); + } + + @ParameterizedTest + @FieldSource("POSE2D_CASES") + @Order(5) + void testToTransform2dGivenPose2d(Pose2d pose) { + assertEquals( + GeometryUtil.toTransform2d(pose), + new Transform2d(pose.getX(), pose.getY(), pose.getRotation())); + } + + @Test + @Order(6) + void testSinglePose2dsAreZero() { + Pose2d zeroedPose2d = Pose2d.kZero; + Pose2d nonZeroedPose2d1 = new Pose2d(1.0, 0.0, Rotation2d.fromDegrees(0.0)); + Pose2d nonZeroedPose2d2 = new Pose2d(0.0, 1.0, Rotation2d.fromDegrees(0.0)); + Pose2d nonZeroedPose2d3 = new Pose2d(0.0, 0.0, Rotation2d.fromDegrees(1.0)); + + assertTrue(GeometryUtil.isZero(zeroedPose2d)); + assertFalse(GeometryUtil.isZero(nonZeroedPose2d1)); + assertFalse(GeometryUtil.isZero(nonZeroedPose2d2)); + assertFalse(GeometryUtil.isZero(nonZeroedPose2d3)); + } + + @Test + @Order(7) + void testAnyPose2dsAreZero() { + Pose2d[] cases = new Pose2d[] {POSE2D_CASES[0], POSE2D_CASES[1], POSE2D_CASES[2]}; + assertTrue(GeometryUtil.isZero(cases)); + } + + @Test + @Order(8) + void testAnyPose2dsAreNotZero() { + Pose2d[] cases = new Pose2d[] {POSE2D_CASES[1], POSE2D_CASES[2]}; + assertFalse(GeometryUtil.isZero(cases)); + } + + @Test + @Order(9) + void testSingleTranslation2dsAreZero() { + Translation2d zeroedTranslation2d = Translation2d.kZero; + Translation2d nonZeroedTranslation2d1 = new Translation2d(1.0, 0.0); + Translation2d nonZeroedTranslation2d2 = new Translation2d(0.0, 1.0); + + assertTrue(GeometryUtil.isZero(zeroedTranslation2d)); + assertFalse(GeometryUtil.isZero(nonZeroedTranslation2d1)); + assertFalse(GeometryUtil.isZero(nonZeroedTranslation2d2)); + } + + @Test + @Order(10) + void testSingleRotation2dsAreZero() { + Rotation2d zeroedRotation2d = Rotation2d.kZero; + Rotation2d nonZeroedTranslation2d1 = Rotation2d.fromDegrees(190.0); + Rotation2d nonZeroedTranslation2d2 = Rotation2d.fromDegrees(42.0); + + assertTrue(GeometryUtil.isZero(zeroedRotation2d)); + assertFalse(GeometryUtil.isZero(nonZeroedTranslation2d1)); + assertFalse(GeometryUtil.isZero(nonZeroedTranslation2d2)); + } + + @Test + @Order(11) + void testSinglePose2dsAreNAN() { + Pose2d zeroedPose2d = Pose2d.kZero; + Pose2d nanPose2d1 = new Pose2d(Double.NaN, 0.0, Rotation2d.fromDegrees(0.0)); + Pose2d nanPose2d2 = new Pose2d(0.0, Double.NaN, Rotation2d.fromDegrees(0.0)); + Pose2d nanPose2d3 = new Pose2d(0.0, 0.0, Rotation2d.fromDegrees(Double.NaN)); + + assertFalse(GeometryUtil.isNaN(zeroedPose2d)); + assertTrue(GeometryUtil.isNaN(nanPose2d1)); + assertTrue(GeometryUtil.isNaN(nanPose2d2)); + assertTrue(GeometryUtil.isNaN(nanPose2d3)); + } + + @Test + @Order(12) + void testAnyPose2dsAreNAN() { + Pose2d nanPose = new Pose2d(Double.NaN, 190.0, Rotation2d.fromDegrees(190.0)); + Pose2d[] cases = new Pose2d[] {nanPose, POSE2D_CASES[0], POSE2D_CASES[1], POSE2D_CASES[2]}; + assertTrue(GeometryUtil.isNaN(cases)); + } + + @Test + @Order(13) + void testAnyPose2dsAreNotNAN() { + Pose2d[] cases = new Pose2d[] {POSE2D_CASES[0], POSE2D_CASES[1], POSE2D_CASES[2]}; + assertFalse(GeometryUtil.isNaN(cases)); + } + + @Test + @Order(14) + void testRectangleDoesNotContainsPose() { + Rectangle2d[] rectangles = { + new Rectangle2d(new Pose2d(0, 0, Rotation2d.kZero), 1, 1), + new Rectangle2d(new Pose2d(0, 0, Rotation2d.kZero), 1, 1), + new Rectangle2d(new Pose2d(0, 0, Rotation2d.kZero), 2, 2) + }; + Pose2d pose = new Pose2d(-5, -5, Rotation2d.kZero); + assertFalse(GeometryUtil.contains(rectangles, pose)); + } + + @Test + @Order(15) + void testRectangleContainsPose() { + Rectangle2d[] rectangles = { + new Rectangle2d(new Pose2d(0, 0, Rotation2d.kZero), 1, 1), + new Rectangle2d(new Pose2d(0, 0, Rotation2d.kZero), 2, 2), + new Rectangle2d(new Pose2d(0, 0, Rotation2d.kZero), 3, 3) + }; + Pose2d pose = new Pose2d(1.5, 1.5, Rotation2d.kZero); + assertTrue(GeometryUtil.contains(rectangles, pose)); + } + + @Test + @Order(16) + void testRectangleDoesNotContainsTranslation() { + Rectangle2d[] rectangles = { + new Rectangle2d(new Pose2d(0, 0, Rotation2d.kZero), 1, 1), + new Rectangle2d(new Pose2d(0, 0, Rotation2d.kZero), 1, 1), + new Rectangle2d(new Pose2d(0, 0, Rotation2d.kZero), 2, 2) + }; + Translation2d translation = new Translation2d(-5, -5); + assertFalse(GeometryUtil.contains(rectangles, translation)); + } + + @Test + @Order(17) + void testRectangleContainsTranslation() { + Rectangle2d[] rectangles = { + new Rectangle2d(new Pose2d(0, 0, Rotation2d.kZero), 1, 1), + new Rectangle2d(new Pose2d(0, 0, Rotation2d.kZero), 2, 2), + new Rectangle2d(new Pose2d(0, 0, Rotation2d.kZero), 3, 3) + }; + Translation2d translation = new Translation2d(1.5, 1.5); + assertTrue(GeometryUtil.contains(rectangles, translation)); + } + + @Test + @Order(18) + void testRectanglePose2ds() { + Rectangle2d rectangle = new Rectangle2d(new Pose2d(0, 0, Rotation2d.kZero), 1, 1); + Pose2d[] poses = GeometryUtil.rectanglePose2ds(rectangle); + Pose2d[] correctPoses = { + new Pose2d(1.0, 1.0, Rotation2d.kZero), + new Pose2d(-1.0, 1.0, Rotation2d.kZero), + new Pose2d(-1.0, -1.0, Rotation2d.kZero), + new Pose2d(1.0, -1.0, Rotation2d.kZero), + new Pose2d(0, 0, Rotation2d.kZero), + }; + for (int i = 0; i < 5; i++) { + assertEquals(poses[i], correctPoses[i]); + } + } +} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/LimelightHelpersTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/LimelightHelpersTest.java new file mode 100644 index 00000000..69ec7b9b --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/LimelightHelpersTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.core.utility; + +public class LimelightHelpersTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/SetpointTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/SetpointTest.java new file mode 100644 index 00000000..a16ce3ce --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/SetpointTest.java @@ -0,0 +1,207 @@ +package edu.wpi.team190.gompeilib.core.utility; + +import static edu.wpi.first.units.Units.*; +import static org.junit.jupiter.api.Assertions.*; + +import edu.wpi.first.units.DistanceUnit; +import edu.wpi.first.units.VoltageUnit; +import edu.wpi.first.units.measure.Distance; +import edu.wpi.first.units.measure.Voltage; +import org.junit.jupiter.api.*; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class SetpointTest { + + @Test + @Order(1) + public void testApplyZero() { + Distance d = Meters.of(1); + Distance s = Meters.of(0.1); + + Setpoint setpoint = new Setpoint<>(d, s); + assertEquals(d, setpoint.getNewSetpoint()); + } + + @Test + @Order(2) + public void testApplyIncrement() { + Distance d = Meters.of(1); + Distance s = Meters.of(0.1); + + Setpoint setpoint = new Setpoint<>(d, s); + setpoint.increment(); + + assertEquals(d.plus(s), setpoint.getNewSetpoint()); + } + + @Test + @Order(3) + public void testApplyDecrement() { + Distance d = Meters.of(1); + Distance s = Meters.of(0.1); + + Setpoint setpoint = new Setpoint<>(d, s); + setpoint.decrement(); + + assertEquals(d.minus(s), setpoint.getNewSetpoint()); + } + + @Test + @Order(4) + public void testApplyOffsetRepeatedly() { + Distance d = Meters.of(1); + Distance s = Meters.of(0.1); + + Setpoint setpoint = new Setpoint<>(d, s); + + setpoint.decrement(); + setpoint.decrement(); + setpoint.decrement(); + setpoint.decrement(); + setpoint.increment(); + setpoint.decrement(); + setpoint.increment(); + + assertEquals(d.minus(s).minus(s).minus(s), setpoint.getNewSetpoint()); + } + + @Test + @Order(5) + public void testResetOffset() { + Distance d = Meters.of(1); + Distance s = Meters.of(0.1); + + Setpoint setpoint = new Setpoint<>(d, s); + setpoint.decrement(); + setpoint.reset(); + + assertEquals(d, setpoint.getNewSetpoint()); + } + + @Test + @Order(6) + public void testSpecificIncrement() { + Distance d = Meters.of(1); + Distance s = Meters.of(0.1); + Distance a = Inches.of(4); + + Setpoint setpoint = new Setpoint<>(d, s); + setpoint.increment(a); + + assertEquals(d.plus(a), setpoint.getNewSetpoint()); + } + + @Test + @Order(7) + public void testSpecificDecrement() { + Distance d = Meters.of(1); + Distance s = Meters.of(0.1); + Distance a = Inches.of(4); + + Setpoint setpoint = new Setpoint<>(d, s); + setpoint.decrement(a); + + assertEquals(d.minus(a), setpoint.getNewSetpoint()); + } + + @Test + @Order(8) + public void testApplyGhostOffsetMaximum() { + Distance d = Meters.of(1); + Distance s = Meters.of(1); + Distance min = Meters.of(0); + Distance max = Meters.of(2); + + Setpoint setpoint = new Setpoint<>(d, s, min, max); + + setpoint.increment(); + setpoint.increment(); + setpoint.increment(); + + assertEquals(max, setpoint.getNewSetpoint()); + } + + @Test + @Order(9) + public void testApplyGhostOffsetMinimum() { + Distance d = Meters.of(1); + Distance s = Meters.of(1); + Distance min = Meters.of(0); + Distance max = Meters.of(2); + + Setpoint setpoint = new Setpoint<>(d, s, min, max); + + setpoint.decrement(); + setpoint.decrement(); + setpoint.decrement(); + + assertEquals(min, setpoint.getNewSetpoint()); + } + + @Test + @Order(10) + public void testApplyGhostOffsetMaximumOverride() { + Distance d = Meters.of(1); + Distance s = Meters.of(1); + Distance min = Meters.of(0); + Distance max = Meters.of(2); + + Setpoint setpoint = new Setpoint<>(d, s, min, max); + + setpoint.increment(s); + setpoint.increment(s); + setpoint.increment(s); + + assertEquals(max, setpoint.getNewSetpoint()); + } + + @Test + @Order(11) + public void testApplyGhostOffsetMinimumOverride() { + Distance d = Meters.of(1); + Distance s = Meters.of(1); + Distance min = Meters.of(0); + Distance max = Meters.of(2); + + Setpoint setpoint = new Setpoint<>(d, s, min, max); + + setpoint.decrement(s); + setpoint.decrement(s); + setpoint.decrement(s); + + assertEquals(min, setpoint.getNewSetpoint()); + } + + @Test + @Order(12) + public void testOffsetGetter() { + Distance d = Meters.of(1); + Distance s = Meters.of(1); + Distance min = Meters.of(0); + Distance max = Meters.of(2); + + Setpoint setpoint = new Setpoint<>(d, s, min, max); + setpoint.increment(s); + + assertEquals(Meters.of(1.0), setpoint.getOffset()); + } + + @Test + @Order(13) + public void testGhostOffsetMax() { + Voltage v = Volts.of(11); + Voltage s = Volts.of(1); + Voltage min = Volts.of(-12); + Voltage max = Volts.of(12); + + Setpoint setpoint = new Setpoint<>(v, s, min, max); + + setpoint.increment(); + assertEquals(Volts.of(1.0), setpoint.getOffset()); + + setpoint.increment(); + + Voltage a = (Voltage) setpoint.getNewSetpoint(); + assertEquals(Volts.of(12.0), a); + } +} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/VirtualSubsystemTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/VirtualSubsystemTest.java new file mode 100644 index 00000000..37bf97fa --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/VirtualSubsystemTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.core.utility; + +public class VirtualSubsystemTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/control/CurrentLimitsTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/control/CurrentLimitsTest.java new file mode 100644 index 00000000..4568e2e0 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/control/CurrentLimitsTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.core.utility.control; + +public class CurrentLimitsTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/control/GainsTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/control/GainsTest.java new file mode 100644 index 00000000..68e1be81 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/control/GainsTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.core.utility.control; + +public class GainsTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/control/LinearProfileTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/control/LinearProfileTest.java new file mode 100644 index 00000000..85cef528 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/control/LinearProfileTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.core.utility.control; + +public class LinearProfileTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/control/constraints/AngularPositionConstraintsTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/control/constraints/AngularPositionConstraintsTest.java new file mode 100644 index 00000000..48cfc70f --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/control/constraints/AngularPositionConstraintsTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.core.utility.control.constraints; + +public class AngularPositionConstraintsTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/control/constraints/AngularVelocityConstraintsTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/control/constraints/AngularVelocityConstraintsTest.java new file mode 100644 index 00000000..13c191ae --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/control/constraints/AngularVelocityConstraintsTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.core.utility.control.constraints; + +public class AngularVelocityConstraintsTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/control/constraints/LinearConstraintsTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/control/constraints/LinearConstraintsTest.java new file mode 100644 index 00000000..e21ccbb6 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/control/constraints/LinearConstraintsTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.core.utility.control.constraints; + +public class LinearConstraintsTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/phoenix/GainSlotTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/phoenix/GainSlotTest.java new file mode 100644 index 00000000..d5c8a9d5 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/phoenix/GainSlotTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.core.utility.phoenix; + +public class GainSlotTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/phoenix/PhoenixOdometryThreadTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/phoenix/PhoenixOdometryThreadTest.java new file mode 100644 index 00000000..1bbf0c3b --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/phoenix/PhoenixOdometryThreadTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.core.utility.phoenix; + +public class PhoenixOdometryThreadTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/phoenix/PhoenixUtilTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/phoenix/PhoenixUtilTest.java new file mode 100644 index 00000000..ebd15541 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/phoenix/PhoenixUtilTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.core.utility.phoenix; + +public class PhoenixUtilTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/sysid/CustomSysidRoutineTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/sysid/CustomSysidRoutineTest.java new file mode 100644 index 00000000..09c0cfa7 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/sysid/CustomSysidRoutineTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.core.utility.sysid; + +public class CustomSysidRoutineTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/sysid/CustomUnitsTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/sysid/CustomUnitsTest.java new file mode 100644 index 00000000..17911362 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/sysid/CustomUnitsTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.core.utility.sysid; + +public class CustomUnitsTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/tunable/LoggedTunableMeasureTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/tunable/LoggedTunableMeasureTest.java new file mode 100644 index 00000000..42ec300d --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/tunable/LoggedTunableMeasureTest.java @@ -0,0 +1,304 @@ +package edu.wpi.team190.gompeilib.core.utility.tunable; + +import static edu.wpi.first.units.Units.Meters; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.atLeastOnce; + +import edu.wpi.first.units.DistanceUnit; +import edu.wpi.team190.gompeilib.core.GompeiLib; +import java.util.Arrays; +import org.junit.jupiter.api.*; +import org.littletonrobotics.junction.networktables.LoggedNetworkNumber; +import org.mockito.MockedConstruction; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class LoggedTunableMeasureTest { + @BeforeEach + void setUp() { + GompeiLib.deinit(); + } + + @Test + @Order(1) + public void defaultValueTuningMode() { + GompeiLib.init(null, true, 0.02); + LoggedTunableMeasure number = + new LoggedTunableMeasure<>("number", Meters.of(190.0)); + assertEquals(190.0, number.get(Meters)); + } + + @Test + @Order(2) + public void defaultValueNoTuningMode() { + GompeiLib.init(null, false, 0.02); + LoggedTunableMeasure number = + new LoggedTunableMeasure<>("number", Meters.of(190.0)); + assertEquals(190.0, number.get(Meters)); + } + + @Test + @Order(3) + public void testGetTuningDisabled() { + // 1. Initialize GompeiLib with tuning = false + GompeiLib.init(null, false, 0.02); + + double expectedDefault = 12.34; + LoggedTunableMeasure tunable = + new LoggedTunableMeasure<>("TestKey", Meters.of(expectedDefault)); + + assertEquals( + expectedDefault, + tunable.get(Meters), + "Should return the default value when tuning is disabled"); + } + + @Test + @Order(4) + public void testGetTuningEnabled() { + // 1. Initialize GompeiLib with tuning = true + GompeiLib.init(null, true, 0.02); + + double defaultValue = 1.0; + double dashboardValue = 99.9; + + // 2. Mock the construction of LoggedNetworkNumber + try (MockedConstruction mocked = + mockConstruction( + LoggedNetworkNumber.class, + (mock, context) -> { + // When dashboardNumber.get() is called, return our fake dashboard value + when(mock.get()).thenReturn(dashboardValue); + })) { + + // 3. Initialize the tunable (this triggers 'new LoggedNetworkNumber') + LoggedTunableMeasure tunable = + new LoggedTunableMeasure<>("TestKey", Meters.of(defaultValue)); + + // 4. Verify that it returns the dashboard value, NOT the default + assertEquals( + dashboardValue, + tunable.get(Meters), + "Should return the dashboard value when tuning is enabled"); + + // 5. Verify the constructor was actually called with the right params + LoggedNetworkNumber mockInternal = mocked.constructed().get(0); + verify(mockInternal, atLeastOnce()).get(); + } + } + + @Test + @Order(5) + public void testHasChangedFirstCall() { + GompeiLib.init(null, false, 0.02); + LoggedTunableMeasure tunable = new LoggedTunableMeasure<>("Test", Meters.of(5.0)); + + // lastValue is null in the map for ID 1 + assertTrue(tunable.hasChanged(1), "First call for a new ID should always return true"); + } + + @Test + @Order(6) + public void testHasChangedNoUpdate() { + GompeiLib.init(null, false, 0.02); + LoggedTunableMeasure tunable = new LoggedTunableMeasure<>("Test", Meters.of(5.0)); + + tunable.hasChanged(1); // First call sets the map entry for ID 1 to 5.0 + + // Second call: currentValue (5.0) == lastValue (5.0) + assertFalse( + tunable.hasChanged(1), "Should return false if the value hasn't changed since last check"); + } + + @Test + @Order(7) + public void testHasChangedWithNewValue() { + GompeiLib.init(null, true, 0.02); + + // We use a Mock to control what get() returns manually + try (MockedConstruction mocked = + mockConstruction(LoggedNetworkNumber.class)) { + LoggedTunableMeasure tunable = + new LoggedTunableMeasure<>("Test", Meters.of(1.0)); + LoggedNetworkNumber mockNT = mocked.constructed().get(0); + + // Setup initial state + when(mockNT.get()).thenReturn(1.0); + tunable.hasChanged(1); + + // Change the value + when(mockNT.get()).thenReturn(2.0); + + assertTrue(tunable.hasChanged(1), "Should return true when the dashboard value changes"); + assertFalse(tunable.hasChanged(1), "Should return false again if called immediately after"); + } + } + + @Test + @Order(8) + public void testHasChangedIdIsolation() { + GompeiLib.init(null, false, 0.02); + LoggedTunableMeasure tunable = + new LoggedTunableMeasure<>("Test", Meters.of(10.0)); + + // ID 1 checks the value + assertTrue(tunable.hasChanged(1)); + assertFalse(tunable.hasChanged(1)); // ID 1 is now "up to date" + + // ID 2 checks for the first time + assertTrue(tunable.hasChanged(2), "ID 2 should return true even if ID 1 already checked"); + } + + @Test + @Order(9) + public void testIfChangedConsumerTriggers() { + GompeiLib.init(null, false, 0.02); + LoggedTunableMeasure num1 = new LoggedTunableMeasure<>("n1", Meters.of(1.0)); + LoggedTunableMeasure num2 = new LoggedTunableMeasure<>("n2", Meters.of(2.0)); + int id = 777; + + // We use an array to "capture" the results from inside the lambda + final double[][] capturedValues = new double[1][]; + + // First call: Should trigger because they are new to this ID + LoggedTunableMeasure.ifChanged( + id, + (values) -> + capturedValues[0] = + Arrays.stream(values).mapToDouble((value) -> value.in(Meters)).toArray(), + num1, + num2); + + assertNotNull(capturedValues[0], "Action should have been called"); + assertEquals(1.0, capturedValues[0][0]); + assertEquals(2.0, capturedValues[0][1]); + } + + @Test + @Order(10) + void testIfChangedRunnableDoesNotTrigger() { + GompeiLib.init(null, false, 0.02); + LoggedTunableMeasure num1 = new LoggedTunableMeasure<>("n1", Meters.of(1.0)); + int id = 888; + + // Manually consume the first "true" + num1.hasChanged(id); + + // Track if the runnable runs + final boolean[] ran = {false}; + + // Call ifChanged: Since value hasn't changed since the line above, it shouldn't run + LoggedTunableMeasure.ifChanged(id, () -> ran[0] = true, num1); + + assertFalse(ran[0], "Action should NOT run if nothing changed"); + } + + @Test + @Order(11) + void testIfChangedTriggersIfOnlyOneChanges() { + GompeiLib.init(null, true, 0.02); + int id = 999; + + try (MockedConstruction mocked = + mockConstruction(LoggedNetworkNumber.class)) { + LoggedTunableMeasure n1 = new LoggedTunableMeasure<>("n1", Meters.of(1.0)); + LoggedTunableMeasure n2 = new LoggedTunableMeasure<>("n2", Meters.of(2.0)); + LoggedNetworkNumber mockNT1 = mocked.constructed().get(0); + LoggedNetworkNumber mockNT2 = mocked.constructed().get(1); + + // Set initial state for both + when(mockNT1.get()).thenReturn(1.0); + when(mockNT2.get()).thenReturn(2.0); + n1.hasChanged(id); + n2.hasChanged(id); + + // Change ONLY n1 + when(mockNT1.get()).thenReturn(5.0); + + final boolean[] ran = {false}; + LoggedTunableMeasure.ifChanged( + id, + (values) -> { + ran[0] = true; + assertEquals(5.0, values[0].in(Meters)); + assertEquals(2.0, values[1].in(Meters)); + }, + n1, + n2); + + assertTrue(ran[0], "Action should trigger if even one value changes"); + } + } + + @Test + @Order(12) + void testIfChangedRunnableOverloadCoverage() { + // 1. Setup in non-tuning mode for simplicity + GompeiLib.init(null, false, 0.02); + LoggedTunableMeasure num = new LoggedTunableMeasure<>("Coverage", Meters.of(1.0)); + int id = 10101; + + // We need a counter to verify the Runnable actually ran + final int[] callCount = {0}; + Runnable mockAction = () -> callCount[0]++; + + // 2. First call: Should trigger (initial change) + // This executes the line: ifChanged(id, values -> action.run(), tunableNumbers); + LoggedTunableMeasure.ifChanged(id, mockAction, num); + + // 3. Assertions + assertEquals(1, callCount[0], "The Runnable action should have executed once"); + + // 4. Second call: Should NOT trigger (no change) + LoggedTunableMeasure.ifChanged(id, mockAction, num); + assertEquals(1, callCount[0], "The Runnable action should NOT have executed a second time"); + } + + @Test + @Order(13) + void testIfChangedSecondElementTriggers() { + GompeiLib.init(null, true, 0.02); + int id = 12345; + + try (MockedConstruction mocked = + mockConstruction(LoggedNetworkNumber.class)) { + + LoggedTunableMeasure n1 = new LoggedTunableMeasure<>("n1", Meters.of(1.0)); + LoggedTunableMeasure n2 = new LoggedTunableMeasure<>("n2", Meters.of(2.0)); + + LoggedNetworkNumber mockNT1 = mocked.constructed().get(0); + LoggedNetworkNumber mockNT2 = mocked.constructed().get(1); + + // Initial values + when(mockNT1.get()).thenReturn(1.0); + when(mockNT2.get()).thenReturn(2.0); + n1.hasChanged(id); + n2.hasChanged(id); + + // Only SECOND value changes + when(mockNT2.get()).thenReturn(5.0); + + final boolean[] ran = {false}; + + LoggedTunableMeasure.ifChanged(id, values -> ran[0] = true, n1, n2); + + assertTrue(ran[0], "Action should trigger when the second element changes"); + } + } + + @Test + @Order(14) + void testIfChangedWithNoTunables() { + GompeiLib.init(null, false, 0.02); + int id = 4242; + + final boolean[] ran = {false}; + + LoggedTunableMeasure.ifChanged( + id, (values) -> ran[0] = true + // no tunable numbers + ); + + assertFalse(ran[0], "Action should not run when no tunables are provided"); + } +} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/tunable/LoggedTunableNumberTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/tunable/LoggedTunableNumberTest.java new file mode 100644 index 00000000..95aafaf2 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/tunable/LoggedTunableNumberTest.java @@ -0,0 +1,267 @@ +package edu.wpi.team190.gompeilib.core.utility.tunable; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import edu.wpi.team190.gompeilib.core.GompeiLib; +import org.junit.jupiter.api.*; +import org.littletonrobotics.junction.networktables.LoggedNetworkNumber; +import org.mockito.MockedConstruction; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class LoggedTunableNumberTest { + @BeforeEach + void setUp() { + GompeiLib.deinit(); + } + + @Test + @Order(1) + public void noDefaultValueTuningMode() { + GompeiLib.init(null, true, 0.02); + LoggedTunableNumber number = new LoggedTunableNumber("number"); + number.initDefault(42.0); + assertEquals(42.0, number.getAsDouble()); + } + + @Test + @Order(2) + public void noDefaultValueNoTuningMode() { + GompeiLib.init(null, false, 0.02); + LoggedTunableNumber number = new LoggedTunableNumber("number"); + number.initDefault(42.0); + assertEquals(42.0, number.getAsDouble()); + } + + @Test + @Order(3) + public void defaultValueTuningMode() { + GompeiLib.init(null, true, 0.02); + LoggedTunableNumber number = new LoggedTunableNumber("number", 190.0); + number.initDefault(42.0); + assertEquals(190.0, number.getAsDouble()); + } + + @Test + @Order(4) + public void defaultValueNoTuningMode() { + GompeiLib.init(null, false, 0.02); + LoggedTunableNumber number = new LoggedTunableNumber("number", 190.0); + number.initDefault(42.0); + assertEquals(190.0, number.getAsDouble()); + } + + @Test + @Order(5) + public void testGetNoDefault() { + // We do NOT call initDefault or use the 2-arg constructor + LoggedTunableNumber tunable = new LoggedTunableNumber("TestKey"); + + assertEquals(0.0, tunable.get(), "Should return 0.0 when no default is set"); + } + + @Test + @Order(6) + public void testGetTuningDisabled() { + // 1. Initialize GompeiLib with tuning = false + GompeiLib.init(null, false, 0.02); + + double expectedDefault = 12.34; + LoggedTunableNumber tunable = new LoggedTunableNumber("TestKey", expectedDefault); + + assertEquals( + expectedDefault, tunable.get(), "Should return the default value when tuning is disabled"); + } + + @Test + @Order(7) + public void testGetTuningEnabled() { + // 1. Initialize GompeiLib with tuning = true + GompeiLib.init(null, true, 0.02); + + double defaultValue = 1.0; + double dashboardValue = 99.9; + + // 2. Mock the construction of LoggedNetworkNumber + try (MockedConstruction mocked = + mockConstruction( + LoggedNetworkNumber.class, + (mock, context) -> { + // When dashboardNumber.get() is called, return our fake dashboard value + when(mock.get()).thenReturn(dashboardValue); + })) { + + // 3. Initialize the tunable (this triggers 'new LoggedNetworkNumber') + LoggedTunableNumber tunable = new LoggedTunableNumber("TestKey", defaultValue); + + // 4. Verify that it returns the dashboard value, NOT the default + assertEquals( + dashboardValue, + tunable.get(), + "Should return the dashboard value when tuning is enabled"); + + // 5. Verify the constructor was actually called with the right params + LoggedNetworkNumber mockInternal = mocked.constructed().get(0); + verify(mockInternal, atLeastOnce()).get(); + } + } + + @Test + @Order(8) + public void testHasChangedFirstCall() { + GompeiLib.init(null, false, 0.02); + LoggedTunableNumber tunable = new LoggedTunableNumber("Test", 5.0); + + // lastValue is null in the map for ID 1 + assertTrue(tunable.hasChanged(1), "First call for a new ID should always return true"); + } + + @Test + @Order(9) + public void testHasChangedNoUpdate() { + GompeiLib.init(null, false, 0.02); + LoggedTunableNumber tunable = new LoggedTunableNumber("Test", 5.0); + + tunable.hasChanged(1); // First call sets the map entry for ID 1 to 5.0 + + // Second call: currentValue (5.0) == lastValue (5.0) + assertFalse( + tunable.hasChanged(1), "Should return false if the value hasn't changed since last check"); + } + + @Test + @Order(10) + public void testHasChangedWithNewValue() { + GompeiLib.init(null, true, 0.02); + + // We use a Mock to control what get() returns manually + try (MockedConstruction mocked = + mockConstruction(LoggedNetworkNumber.class)) { + LoggedTunableNumber tunable = new LoggedTunableNumber("Test", 1.0); + LoggedNetworkNumber mockNT = mocked.constructed().get(0); + + // Setup initial state + when(mockNT.get()).thenReturn(1.0); + tunable.hasChanged(1); + + // Change the value + when(mockNT.get()).thenReturn(2.0); + + assertTrue(tunable.hasChanged(1), "Should return true when the dashboard value changes"); + assertFalse(tunable.hasChanged(1), "Should return false again if called immediately after"); + } + } + + @Test + @Order(11) + public void testHasChangedIdIsolation() { + GompeiLib.init(null, false, 0.02); + LoggedTunableNumber tunable = new LoggedTunableNumber("Test", 10.0); + + // ID 1 checks the value + assertTrue(tunable.hasChanged(1)); + assertFalse(tunable.hasChanged(1)); // ID 1 is now "up to date" + + // ID 2 checks for the first time + assertTrue(tunable.hasChanged(2), "ID 2 should return true even if ID 1 already checked"); + } + + @Test + @Order(12) + public void testIfChangedConsumerTriggers() { + GompeiLib.init(null, false, 0.02); + LoggedTunableNumber num1 = new LoggedTunableNumber("n1", 1.0); + LoggedTunableNumber num2 = new LoggedTunableNumber("n2", 2.0); + int id = 777; + + // We use an array to "capture" the results from inside the lambda + final double[][] capturedValues = new double[1][]; + + // First call: Should trigger because they are new to this ID + LoggedTunableNumber.ifChanged(id, (values) -> capturedValues[0] = values, num1, num2); + + assertNotNull(capturedValues[0], "Action should have been called"); + assertEquals(1.0, capturedValues[0][0]); + assertEquals(2.0, capturedValues[0][1]); + } + + @Test + @Order(13) + void testIfChangedRunnableDoesNotTrigger() { + GompeiLib.init(null, false, 0.02); + LoggedTunableNumber num1 = new LoggedTunableNumber("n1", 1.0); + int id = 888; + + // Manually consume the first "true" + num1.hasChanged(id); + + // Track if the runnable runs + final boolean[] ran = {false}; + + // Call ifChanged: Since value hasn't changed since the line above, it shouldn't run + LoggedTunableNumber.ifChanged(id, () -> ran[0] = true, num1); + + assertFalse(ran[0], "Action should NOT run if nothing changed"); + } + + @Test + @Order(14) + void testIfChangedTriggersIfOnlyOneChanges() { + GompeiLib.init(null, true, 0.02); + int id = 999; + + try (MockedConstruction mocked = + mockConstruction(LoggedNetworkNumber.class)) { + LoggedTunableNumber n1 = new LoggedTunableNumber("n1", 1.0); + LoggedTunableNumber n2 = new LoggedTunableNumber("n2", 2.0); + LoggedNetworkNumber mockNT1 = mocked.constructed().get(0); + LoggedNetworkNumber mockNT2 = mocked.constructed().get(1); + + // Set initial state for both + when(mockNT1.get()).thenReturn(1.0); + when(mockNT2.get()).thenReturn(2.0); + n1.hasChanged(id); + n2.hasChanged(id); + + // Change ONLY n1 + when(mockNT1.get()).thenReturn(5.0); + + final boolean[] ran = {false}; + LoggedTunableNumber.ifChanged( + id, + (values) -> { + ran[0] = true; + assertEquals(5.0, values[0]); + assertEquals(2.0, values[1]); + }, + n1, + n2); + + assertTrue(ran[0], "Action should trigger if even one value changes"); + } + } + + @Test + @Order(15) + void testIfChangedRunnableOverloadCoverage() { + // 1. Setup in non-tuning mode for simplicity + GompeiLib.init(null, false, 0.02); + LoggedTunableNumber num = new LoggedTunableNumber("Coverage", 1.0); + int id = 10101; + + // We need a counter to verify the Runnable actually ran + final int[] callCount = {0}; + Runnable mockAction = () -> callCount[0]++; + + // 2. First call: Should trigger (initial change) + // This executes the line: ifChanged(id, values -> action.run(), tunableNumbers); + LoggedTunableNumber.ifChanged(id, mockAction, num); + + // 3. Assertions + assertEquals(1, callCount[0], "The Runnable action should have executed once"); + + // 4. Second call: Should NOT trigger (no change) + LoggedTunableNumber.ifChanged(id, mockAction, num); + assertEquals(1, callCount[0], "The Runnable action should NOT have executed a second time"); + } +} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/tunable/TunableUpdaterRegistryTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/tunable/TunableUpdaterRegistryTest.java new file mode 100644 index 00000000..b1a4b525 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/core/utility/tunable/TunableUpdaterRegistryTest.java @@ -0,0 +1,185 @@ +package edu.wpi.team190.gompeilib.core.utility.tunable; + +import static edu.wpi.first.units.Units.Radian; +import static org.junit.jupiter.api.Assertions.*; + +import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.units.Units; +import edu.wpi.team190.gompeilib.core.GompeiLib; +import edu.wpi.team190.gompeilib.core.utility.control.Gains; +import edu.wpi.team190.gompeilib.core.utility.control.constraints.LinearConstraints; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.*; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TunableUpdaterRegistryTest { + + @BeforeEach + void setUp() { + GompeiLib.deinit(); + GompeiLib.init(null, true, 0.02); + } + + @Test + @Order(2) + void periodicWithNoRegistrationsDoesNothing() { + assertDoesNotThrow(TunableUpdaterRegistry::periodic); + } + + @Test + @Order(3) + void registerGainsInvokesConsumer() { + Gains gains = + Gains.fromDoubles() + .withPrefix("test/gains") + .withKP(1) + .withKI(2) + .withKD(3) + .withKS(4) + .withKV(5) + .withKA(6) + .withKG(7) + .build(); + + AtomicInteger calls = new AtomicInteger(); + + TunableUpdaterRegistry.registerGains(gains, g -> calls.incrementAndGet()); + + TunableUpdaterRegistry.periodic(); + + assertEquals(1, calls.get()); + } + + @Test + @Order(4) + void registerGainsDuplicateIgnored() { + Gains gains = + Gains.fromDoubles() + .withPrefix("test/gains/dup") + .withKP(1) + .withKI(2) + .withKD(3) + .withKS(4) + .withKV(5) + .withKA(6) + .withKG(7) + .build(); + + AtomicInteger first = new AtomicInteger(); + AtomicInteger second = new AtomicInteger(); + + TunableUpdaterRegistry.registerGains(gains, g -> first.incrementAndGet()); + TunableUpdaterRegistry.registerGains(gains, g -> second.incrementAndGet()); + + TunableUpdaterRegistry.periodic(); + + assertEquals(1, first.get()); + assertEquals(0, second.get()); + } + + @Test + @Order(5) + void registerConstraintsInvokesConsumer() { + LinearConstraints constraints = + LinearConstraints.fromMeasures() + .withPrefix("test/constraints") + .withGoalTolerance(Units.Meters.of(0.1)) + .withMaxVelocity(Units.MetersPerSecond.of(2)) + .withMaxAcceleration(Units.MetersPerSecondPerSecond.of(3)) + .build(); + + AtomicInteger calls = new AtomicInteger(); + + TunableUpdaterRegistry.registerConstraints(constraints, c -> calls.incrementAndGet()); + + TunableUpdaterRegistry.periodic(); + + assertEquals(1, calls.get()); + } + + @Test + @Order(6) + void registerConstraintsDuplicateIgnored() { + LinearConstraints constraints = + LinearConstraints.fromMeasures() + .withPrefix("test/constraints/dup") + .withGoalTolerance(Units.Meters.of(0.2)) + .withMaxVelocity(Units.MetersPerSecond.of(3)) + .withMaxAcceleration(Units.MetersPerSecondPerSecond.of(4)) + .build(); + + AtomicInteger first = new AtomicInteger(); + AtomicInteger second = new AtomicInteger(); + + TunableUpdaterRegistry.registerConstraints(constraints, c -> first.incrementAndGet()); + TunableUpdaterRegistry.registerConstraints(constraints, c -> second.incrementAndGet()); + + TunableUpdaterRegistry.periodic(); + + assertEquals(1, first.get()); + assertEquals(0, second.get()); + } + + @Test + @Order(7) + void registerNumberAcceptsArray() { + LoggedTunableNumber[] nums = new LoggedTunableNumber[0]; + + assertDoesNotThrow(() -> TunableUpdaterRegistry.registerNumber(nums, v -> {})); + } + + @Test + @Order(8) + void registerNumberDuplicateIgnored() { + LoggedTunableNumber[] nums = new LoggedTunableNumber[1]; + nums[0] = new LoggedTunableNumber("test/testnumber1"); + + AtomicInteger first = new AtomicInteger(); + AtomicInteger second = new AtomicInteger(); + + TunableUpdaterRegistry.registerNumber(nums, v -> first.incrementAndGet()); + TunableUpdaterRegistry.registerNumber(nums, v -> second.incrementAndGet()); + + NetworkTableInstance.getDefault() + .getDoubleTopic("TunableNumbers/test/testnumber1") + .publish() + .set(100); + + TunableUpdaterRegistry.periodic(); + + assertEquals(1, first.get()); + assertEquals(0, second.get()); + } + + @Test + @Order(9) + void registerMeasureAcceptsArray() { + LoggedTunableMeasure[] measures = new LoggedTunableMeasure[0]; + + assertDoesNotThrow(() -> TunableUpdaterRegistry.registerMeasure(measures, () -> {})); + } + + @Test + @Order(10) + void registerMeasureDuplicateIgnored() { + LoggedTunableMeasure[] measures = new LoggedTunableMeasure[1]; + + measures[0] = new LoggedTunableMeasure<>("test/testmeasure1", Radian.zero()); + + AtomicInteger first = new AtomicInteger(); + AtomicInteger second = new AtomicInteger(); + + TunableUpdaterRegistry.registerMeasure(measures, first::incrementAndGet); + TunableUpdaterRegistry.registerMeasure(measures, second::incrementAndGet); + + NetworkTableInstance.getDefault() + .getDoubleTopic("TunableNumbers/test/testmeasure1 (Radian)") + .publish() + .set(100); + + TunableUpdaterRegistry.periodic(); + + assertEquals(1, first.get()); + assertEquals(0, second.get()); + } +} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmConstantsTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmConstantsTest.java new file mode 100644 index 00000000..ab358fcf --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmConstantsTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.arm; + +public class ArmConstantsTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIOSimTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIOSimTest.java new file mode 100644 index 00000000..2daa59e3 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIOSimTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.arm; + +public class ArmIOSimTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIOTalonFXSimTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIOTalonFXSimTest.java new file mode 100644 index 00000000..fcb0ff6e --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIOTalonFXSimTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.arm; + +public class ArmIOTalonFXSimTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIOTalonFXTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIOTalonFXTest.java new file mode 100644 index 00000000..816e62e4 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIOTalonFXTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.arm; + +public class ArmIOTalonFXTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIOTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIOTest.java new file mode 100644 index 00000000..a05136e7 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmIOTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.arm; + +public class ArmIOTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmStateTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmStateTest.java new file mode 100644 index 00000000..68c199cd --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmStateTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.arm; + +public class ArmStateTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmTest.java new file mode 100644 index 00000000..7e793d59 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/arm/ArmTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.arm; + +public class ArmTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveDriveConstantsTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveDriveConstantsTest.java new file mode 100644 index 00000000..8a948448 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveDriveConstantsTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive; + +public class SwerveDriveConstantsTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveDriveTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveDriveTest.java new file mode 100644 index 00000000..9812395c --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveDriveTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive; + +public class SwerveDriveTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIOSimTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIOSimTest.java new file mode 100644 index 00000000..a5b1c9df --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIOSimTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive; + +public class SwerveModuleIOSimTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIOTalonFXSimTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIOTalonFXSimTest.java new file mode 100644 index 00000000..e6f7cfd7 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIOTalonFXSimTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive; + +public class SwerveModuleIOTalonFXSimTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIOTalonFXTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIOTalonFXTest.java new file mode 100644 index 00000000..80f527ab --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIOTalonFXTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive; + +public class SwerveModuleIOTalonFXTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIOTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIOTest.java new file mode 100644 index 00000000..7d468aac --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleIOTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive; + +public class SwerveModuleIOTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleTest.java new file mode 100644 index 00000000..f5ac89ac --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/drivebases/swervedrive/SwerveModuleTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.drivebases.swervedrive; + +public class SwerveModuleTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorConstantsTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorConstantsTest.java new file mode 100644 index 00000000..131afabf --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorConstantsTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.elevator; + +public class ElevatorConstantsTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIOSimTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIOSimTest.java new file mode 100644 index 00000000..3c9f1513 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIOSimTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.elevator; + +public class ElevatorIOSimTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIOTalonFXSimTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIOTalonFXSimTest.java new file mode 100644 index 00000000..b6c4108e --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIOTalonFXSimTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.elevator; + +public class ElevatorIOTalonFXSimTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIOTalonFXTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIOTalonFXTest.java new file mode 100644 index 00000000..cf3096e8 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIOTalonFXTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.elevator; + +public class ElevatorIOTalonFXTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIOTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIOTest.java new file mode 100644 index 00000000..319c3d3a --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorIOTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.elevator; + +public class ElevatorIOTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorStateTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorStateTest.java new file mode 100644 index 00000000..a3329729 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorStateTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.elevator; + +public class ElevatorStateTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorTest.java new file mode 100644 index 00000000..a55b2c41 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/elevator/ElevatorTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.elevator; + +public class ElevatorTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelConstantsTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelConstantsTest.java new file mode 100644 index 00000000..d672da58 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelConstantsTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.flywheel; + +public class GenericFlywheelConstantsTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIOSimTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIOSimTest.java new file mode 100644 index 00000000..ca020cd1 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIOSimTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.flywheel; + +public class GenericFlywheelIOSimTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIOTalonFXSimTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIOTalonFXSimTest.java new file mode 100644 index 00000000..2366d269 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIOTalonFXSimTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.flywheel; + +public class GenericFlywheelIOTalonFXSimTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIOTalonFXTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIOTalonFXTest.java new file mode 100644 index 00000000..8e8d4d23 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIOTalonFXTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.flywheel; + +public class GenericFlywheelIOTalonFXTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIOTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIOTest.java new file mode 100644 index 00000000..bb69d902 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelIOTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.flywheel; + +public class GenericFlywheelIOTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelStateTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelStateTest.java new file mode 100644 index 00000000..64453809 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelStateTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.flywheel; + +public class GenericFlywheelStateTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelTest.java new file mode 100644 index 00000000..976333da --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/flywheel/GenericFlywheelTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.flywheel; + +public class GenericFlywheelTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerConstantsTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerConstantsTest.java new file mode 100644 index 00000000..96876082 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerConstantsTest.java @@ -0,0 +1,5 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.roller; + +public class GenericRollerConstantsTest { + // add comment +} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIOSimTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIOSimTest.java new file mode 100644 index 00000000..1c64083d --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIOSimTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.roller; + +public class GenericRollerIOSimTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIOTalonFXSimTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIOTalonFXSimTest.java new file mode 100644 index 00000000..1bffb8ad --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIOTalonFXSimTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.roller; + +public class GenericRollerIOTalonFXSimTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIOTalonFXTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIOTalonFXTest.java new file mode 100644 index 00000000..8c5fe072 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIOTalonFXTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.roller; + +public class GenericRollerIOTalonFXTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIOTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIOTest.java new file mode 100644 index 00000000..d49b5d27 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerIOTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.roller; + +public class GenericRollerIOTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerTest.java new file mode 100644 index 00000000..504253f2 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/generic/roller/GenericRollerTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.generic.roller; + +public class GenericRollerTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/VisionConstantsTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/VisionConstantsTest.java new file mode 100644 index 00000000..384682e9 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/VisionConstantsTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.vision; + +public class VisionConstantsTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/VisionTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/VisionTest.java new file mode 100644 index 00000000..e4a70fbd --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/VisionTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.vision; + +public class VisionTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraGompeiVisionTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraGompeiVisionTest.java new file mode 100644 index 00000000..47ef3add --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraGompeiVisionTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.vision.camera; + +public class CameraGompeiVisionTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraLimelightTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraLimelightTest.java new file mode 100644 index 00000000..dce0ea6b --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraLimelightTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.vision.camera; + +public class CameraLimelightTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraTest.java new file mode 100644 index 00000000..e522ffba --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.vision.camera; + +public class CameraTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraTypeTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraTypeTest.java new file mode 100644 index 00000000..09c3ccb5 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/camera/CameraTypeTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.vision.camera; + +public class CameraTypeTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/data/VisionMultiTxTyObservationTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/data/VisionMultiTxTyObservationTest.java new file mode 100644 index 00000000..2c598e54 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/data/VisionMultiTxTyObservationTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.vision.data; + +public class VisionMultiTxTyObservationTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/data/VisionPoseObservationTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/data/VisionPoseObservationTest.java new file mode 100644 index 00000000..2e4c118d --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/data/VisionPoseObservationTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.vision.data; + +public class VisionPoseObservationTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/data/VisionSingleTxTyObservationTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/data/VisionSingleTxTyObservationTest.java new file mode 100644 index 00000000..65b5121d --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/data/VisionSingleTxTyObservationTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.vision.data; + +public class VisionSingleTxTyObservationTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/io/CameraIOGompeiVisionTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/io/CameraIOGompeiVisionTest.java new file mode 100644 index 00000000..bef91369 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/io/CameraIOGompeiVisionTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.vision.io; + +public class CameraIOGompeiVisionTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/io/CameraIOLimelightTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/io/CameraIOLimelightTest.java new file mode 100644 index 00000000..0c81ebaa --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/io/CameraIOLimelightTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.vision.io; + +public class CameraIOLimelightTest {} diff --git a/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/io/CameraIOTest.java b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/io/CameraIOTest.java new file mode 100644 index 00000000..0d559f08 --- /dev/null +++ b/lib/src/test/java/edu/wpi/team190/gompeilib/subsystems/vision/io/CameraIOTest.java @@ -0,0 +1,3 @@ +package edu.wpi.team190.gompeilib.subsystems.vision.io; + +public class CameraIOTest {} diff --git a/settings.gradle b/settings.gradle index 8ea4b5d1..ab3a5474 100644 --- a/settings.gradle +++ b/settings.gradle @@ -28,3 +28,16 @@ pluginManagement { Properties props = System.getProperties() props.setProperty("org.gradle.internal.native.headers.unresolved.dependencies.ignore", "true") + +rootProject.name = '2k26-Robot-Code' + +include(":gompeilib") +project(":gompeilib").projectDir = file("lib") + +dependencyResolutionManagement { + versionCatalogs { + libs { + from(files("lib/gradle/libs.versions.toml")) + } + } +} diff --git a/src/main/java/frc/robot/subsystems/shared/intake/IntakeConstants.java b/src/main/java/frc/robot/subsystems/shared/intake/IntakeConstants.java index 076b61f3..173f739b 100644 --- a/src/main/java/frc/robot/subsystems/shared/intake/IntakeConstants.java +++ b/src/main/java/frc/robot/subsystems/shared/intake/IntakeConstants.java @@ -376,11 +376,11 @@ public class IntakeConstants { MOMENT_OF_INERTIA = 0.004; MOTOR_CONFIG = DCMotor.getKrakenX60Foc(1); - INTAKE_ANGLE_OFFSET = Rotation2d.fromDegrees(-30.9603232217); + INTAKE_ANGLE_OFFSET = Rotation2d.fromDegrees(-30.838927); ZERO_OFFSET = Rotation2d.kPi; MIN_ANGLE = Rotation2d.fromDegrees(30.838927); - MAX_ANGLE = Rotation2d.fromDegrees(236.74); + MAX_ANGLE = Rotation2d.fromDegrees(168.912511); // points A and D on the intake. PIN_LENGTH = Units.Inches.of(6.125).in(Units.Meters); @@ -510,7 +510,7 @@ public class IntakeConstants { MOMENT_OF_INERTIA = 0.004; MOTOR_CONFIG = DCMotor.getKrakenX60Foc(1); - INTAKE_ANGLE_OFFSET = Rotation2d.fromDegrees(-30.9603232217); + INTAKE_ANGLE_OFFSET = Rotation2d.fromDegrees(-30.838927); ZERO_OFFSET = Rotation2d.kPi; MIN_ANGLE = Rotation2d.fromDegrees(30.838927); @@ -590,19 +590,19 @@ public class IntakeConstants { Map.of( IntakeState.STOW, new Setpoint<>( - Rotation2d.fromDegrees(9.0).getMeasure(), + MIN_ANGLE.getMeasure(), LINKAGE_ANGLE_INCREMENT.getMeasure(), MIN_ANGLE.getMeasure(), MAX_ANGLE.getMeasure()), IntakeState.INTAKE, new Setpoint<>( - Rotation2d.fromDegrees(168.134766 + 8.0).getMeasure(), + MAX_ANGLE.getMeasure(), LINKAGE_ANGLE_INCREMENT.getMeasure(), MIN_ANGLE.getMeasure(), MAX_ANGLE.getMeasure()), IntakeState.AGITATE, new Setpoint<>( - Rotation2d.fromDegrees(168.134766 + 8.0).getMeasure(), + MAX_ANGLE.getMeasure(), LINKAGE_ANGLE_INCREMENT.getMeasure(), MIN_ANGLE.getMeasure(), MAX_ANGLE.getMeasure())); diff --git a/src/main/java/frc/robot/subsystems/v2_Turnover/V2_TurnoverMechanism3d.java b/src/main/java/frc/robot/subsystems/v2_Turnover/V2_TurnoverMechanism3d.java index 6c10917d..2f21993a 100644 --- a/src/main/java/frc/robot/subsystems/v2_Turnover/V2_TurnoverMechanism3d.java +++ b/src/main/java/frc/robot/subsystems/v2_Turnover/V2_TurnoverMechanism3d.java @@ -3,35 +3,55 @@ import edu.wpi.first.math.geometry.Pose3d; import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Transform3d; import edu.wpi.first.math.geometry.Translation3d; import frc.robot.subsystems.shared.fourbarlinkage.FourBarLinkageConstants.LinkageState; import frc.robot.subsystems.shared.intake.Intake; +import frc.robot.subsystems.v2_Turnover.shooter.V2_TurnoverShooter; import java.util.List; public class V2_TurnoverMechanism3d { - private static final Translation3d spindexerTranslation = - new Translation3d(-0.009525, 0, 0.067589); + // Shooter + private static final Translation3d baseTurretTranslation = // robot centric + new Translation3d(-0.017463, -0.163513, 0.371475); + private static final Translation3d baseHoodTranslation = + new Translation3d(0.043168, -0.1635125, 0.474975); // robot centric + private static final Transform3d turretToHoodTransform = + new Transform3d(baseHoodTranslation.minus(baseTurretTranslation), Rotation3d.kZero); + + // Climber private static final Translation3d climberTranslation = - new Translation3d(-0.202788, 0.090048, 0.477077); - private static final Translation3d staticIntakeTranslation = new Translation3d(0.0, 0.0, 0.0); + new Translation3d(-0.317482, 0.090043, 0.477114); + + // Intake private static final Translation3d intakeCrankTranslation = - new Translation3d(0.142476, 0, 0.278075); + new Translation3d(0.139700, 0, 0.254000); private static final Translation3d intakeFollowerTranslation = - new Translation3d(0.278238, 0, 0.196629); + new Translation3d(0.292100, 0, 0.171450); - private static final Rotation2d crankOffset = Rotation2d.fromDegrees(-171); - private static final Rotation2d couplerOffset = Rotation2d.fromDegrees(-18.88); - private static final Rotation2d followerOffset = Rotation2d.fromDegrees(-60.909742); + private static final Rotation2d crankOffset = Rotation2d.fromDegrees(-180 + 30.838927); + private static final Rotation2d couplerOffset = Rotation2d.fromDegrees(0); + private static final Rotation2d followerOffset = Rotation2d.fromDegrees(-70.753060); public static Pose3d[] getPoses( - Rotation2d spindexerPosition, Rotation2d climberPosition, Intake intake) { - - List linkageStates = intake.getLinkage().getLinkagePoses(); + Rotation2d climberPosition, Intake intake, V2_TurnoverShooter shooter) { + // Shooter + Pose3d turretPose = + new Pose3d( + baseTurretTranslation, + new Rotation3d(Rotation2d.k180deg.plus(shooter.getTurretRotation()))); + Pose3d hoodPose = + turretPose.transformBy( + new Transform3d( + turretToHoodTransform.getTranslation(), + new Rotation3d(0.0, shooter.getHoodAngle().getRadians(), 0.0))); - Pose3d spindexerPose = new Pose3d(spindexerTranslation, new Rotation3d(spindexerPosition)); + // Climber Pose3d climberPose = new Pose3d(climberTranslation, new Rotation3d(-climberPosition.getRadians(), 0.0, 0.0)); - Pose3d staticIntakePose = new Pose3d(staticIntakeTranslation, new Rotation3d()); + + // Intake + List linkageStates = intake.getLinkage().getLinkagePoses(); Pose3d crankPose = new Pose3d( intakeCrankTranslation, @@ -55,7 +75,7 @@ public static Pose3d[] getPoses( 0.0)); return new Pose3d[] { - spindexerPose, climberPose, staticIntakePose, crankPose, couplerPose, followerPose, + turretPose, hoodPose, climberPose, Pose3d.kZero, crankPose, couplerPose, followerPose }; } } diff --git a/src/main/java/frc/robot/subsystems/v2_Turnover/V2_TurnoverRobotContainer.java b/src/main/java/frc/robot/subsystems/v2_Turnover/V2_TurnoverRobotContainer.java index 36b951c6..4ae48da0 100644 --- a/src/main/java/frc/robot/subsystems/v2_Turnover/V2_TurnoverRobotContainer.java +++ b/src/main/java/frc/robot/subsystems/v2_Turnover/V2_TurnoverRobotContainer.java @@ -83,6 +83,7 @@ import java.util.Map; import java.util.function.Supplier; import java.util.stream.Collectors; +import org.littletonrobotics.junction.Logger; import org.littletonrobotics.junction.networktables.LoggedDashboardChooser; import org.littletonrobotics.junction.networktables.LoggedNetworkBoolean; @@ -917,6 +918,8 @@ public void robotPeriodic() { intake.isIntakeAtStow(), driver.rightBumper().getAsBoolean()); fuelSimulator.updateSim(); + Logger.recordOutput( + "Mechanism 3d", V2_TurnoverMechanism3d.getPoses(Rotation2d.kZero, intake, shooter)); } @Override