diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7889ba0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[{*.kt,*.kts}] +indent_size=2 +max_line_length=200 +ij_kotlin_packages_to_use_import_on_demand = unset \ No newline at end of file diff --git a/.github/workflows/gen-docs.yml b/.github/workflows/gen-docs.yml new file mode 100644 index 0000000..8cdfb9b --- /dev/null +++ b/.github/workflows/gen-docs.yml @@ -0,0 +1,61 @@ +name: Generate docs + +on: + push: + branches: + - main + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Pages + uses: actions/configure-pages@v2 + + - uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 17 + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + with: + gradle-version: current + + - name: Make gradlew executable + run: chmod +x ./gradlew + + - name: Generate HTML with Dokka + run: | + ./gradlew dokkaHtml + + - name: Upload artifact + uses: actions/upload-pages-artifact@v1 + with: + # Upload build HTML + path: './build/dokka/html' + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v1 + diff --git a/.github/workflows/ktlint.yml b/.github/workflows/ktlint.yml new file mode 100644 index 0000000..178607d --- /dev/null +++ b/.github/workflows/ktlint.yml @@ -0,0 +1,25 @@ +name: Check formatting with ktlint + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + - name: Give gradlew permission + run: chmod +x ./gradlew + - name: Check formatting using ktlint + run: ./gradlew ktlintCheck diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000..bfde876 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,19 @@ +name: Build and run tests + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 17 + uses: actions/setup-java@v1 + with: + java-version: 17 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Run tests + run: ./gradlew test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..74b9648 --- /dev/null +++ b/.gitignore @@ -0,0 +1,174 @@ +# This gitignore has been specially created by the WPILib team. +# If you remove items from this file, intellisense might break. + +### C++ ### +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### Gradle ### +.gradle +/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +# # VS Code Specific Java Settings +# DO NOT REMOVE .classpath and .project +.classpath +.project +.settings/ +bin/ + +# IntelliJ +*.iml +*.ipr +*.iws +.idea/ +out/ + +# Fleet +.fleet + +# Simulation GUI and other tools window save file +*-window.json + +*.wpilog \ No newline at end of file diff --git a/.run/Build & Deploy Robot for Debugging.run.xml b/.run/Build & Deploy Robot for Debugging.run.xml new file mode 100644 index 0000000..72b01bf --- /dev/null +++ b/.run/Build & Deploy Robot for Debugging.run.xml @@ -0,0 +1,68 @@ + + + + + + + true + true + false + false + + + + + + + + true + true + false + false + + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.run/Build & Deploy Robot.run.xml b/.run/Build & Deploy Robot.run.xml new file mode 100644 index 0000000..ede63c1 --- /dev/null +++ b/.run/Build & Deploy Robot.run.xml @@ -0,0 +1,68 @@ + + + + + + + true + true + false + false + + + + + + + + true + true + false + false + + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.run/Build Robot.run.xml b/.run/Build Robot.run.xml new file mode 100644 index 0000000..d78b6e7 --- /dev/null +++ b/.run/Build Robot.run.xml @@ -0,0 +1,68 @@ + + + + + + + true + true + false + false + + + + + + + + true + true + false + false + + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.run/Clean Build & Deploy Robot for Debugging.run.xml b/.run/Clean Build & Deploy Robot for Debugging.run.xml new file mode 100644 index 0000000..13d0e1a --- /dev/null +++ b/.run/Clean Build & Deploy Robot for Debugging.run.xml @@ -0,0 +1,71 @@ + + + + + + + true + true + false + false + + + + + + + + true + true + false + false + + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.run/Clean Build & Deploy Robot.run.xml b/.run/Clean Build & Deploy Robot.run.xml new file mode 100644 index 0000000..2091360 --- /dev/null +++ b/.run/Clean Build & Deploy Robot.run.xml @@ -0,0 +1,71 @@ + + + + + + + true + true + false + false + + + + + + + + true + true + false + false + + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.run/Clean Build Robot.run.xml b/.run/Clean Build Robot.run.xml new file mode 100644 index 0000000..65e1aab --- /dev/null +++ b/.run/Clean Build Robot.run.xml @@ -0,0 +1,163 @@ + + + + + + + true + true + false + false + + + + + + + + true + true + false + false + + + + + + + + true + true + false + false + + + + + + + + true + true + false + false + + + + + + + + true + true + false + false + + + + + + + + true + true + false + false + + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.run/Clean.run.xml b/.run/Clean.run.xml new file mode 100644 index 0000000..7ff02fc --- /dev/null +++ b/.run/Clean.run.xml @@ -0,0 +1,68 @@ + + + + + + + true + true + false + false + + + + + + + + true + true + false + false + + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.run/Debug Robot via IP.run.xml b/.run/Debug Robot via IP.run.xml new file mode 100644 index 0000000..e7385cf --- /dev/null +++ b/.run/Debug Robot via IP.run.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/.run/Debug Robot via USB.run.xml b/.run/Debug Robot via USB.run.xml new file mode 100644 index 0000000..90691d3 --- /dev/null +++ b/.run/Debug Robot via USB.run.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/.wpilib/wpilib_preferences.json b/.wpilib/wpilib_preferences.json new file mode 100644 index 0000000..a6475f6 --- /dev/null +++ b/.wpilib/wpilib_preferences.json @@ -0,0 +1,6 @@ +{ + "enableCppIntellisense": false, + "currentLanguage": "java", + "projectYear": "2024beta", + "teamNumber": 449 +} \ No newline at end of file diff --git a/6328 License.md b/6328 License.md new file mode 100644 index 0000000..be97dda --- /dev/null +++ b/6328 License.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5103ed9 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Bunnybots 2023 +Our source code for our veteran-led Bunnybot for our 2023 Bunnybots Game: Hibernation Harvest! + +Credit to 6328 for characterization commands and analysis. + +----------------------------- + +## Workflows + +There are currently three workflows, `run-tests.yml` to run tests, `ktlint.yml` to check formatting, and `gen-docs.yml` to generate documentation. Here is how `gen-docs.yml` works: + +- It's triggered whenever you push to `main` +- It runs `./gradlew dokkaHtml` to generate HTML from all our doc comments +- The generated HTML is uploaded and deployed to GitHub Pages +- The documentation is then accessible at https://blair-robot-project.github.io/framework2023/. diff --git a/WPILib-License.md b/WPILib-License.md new file mode 100644 index 0000000..43b62ec --- /dev/null +++ b/WPILib-License.md @@ -0,0 +1,24 @@ +Copyright (c) 2009-2023 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/build.gradle b/build.gradle new file mode 100644 index 0000000..9d9d96f --- /dev/null +++ b/build.gradle @@ -0,0 +1,138 @@ +import edu.wpi.first.gradlerio.GradleRIOPlugin + +plugins { + id "java" + id "idea" + id "org.jetbrains.kotlin.jvm" version "1.8.20" + id "edu.wpi.first.GradleRIO" version "2024.1.1-beta-3" + id "org.jlleitschuh.gradle.ktlint" version "11.3.1" + id "org.jetbrains.dokka" version "1.8.10" +} +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +def ROBOT_MAIN_CLASS = "frc.team449.Main" + +// Define my targets (RoboRIO) and artifacts (deployable files) +// This is added by GradleRIO's backing project DeployUtils. +deploy { + targets { + roborio(getTargetTypeClass('RoboRIO')) { + // Team number is loaded either from the .wpilib/wpilib_preferences.json + // or from command line. If not found an exception will be thrown. + // You can use getTeamOrDefault(team) instead of getTeamNumber if you + // want to store a team number in this file. + team = project.frc.getTeamNumber() + 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')) { + } + + // Static files artifact + frcStaticFileDeploy(getArtifactTypeClass('FileTreeArtifact')) { + files = project.fileTree('src/main/deploy') + directory = '/home/lvuser/deploy' + } + } + } + } +} + +def deployArtifact = deploy.targets.roborio.artifacts.frcJava + +// Set to true to use debug for JNI. +wpi.java.debugJni = false + +// Set this to true to enable desktop support. +def includeDesktopSupport = false + +// Defining my dependencies. In this case, WPILib (+ friends), and vendor libraries. +// Also defines JUnit 5. +dependencies { + compileOnly 'org.jetbrains:annotations:24.0.1' + + implementation 'com.github.shueja:Monologue:v1.0.0-alpha2' + + implementation("io.javalin:javalin:5.6.2") + implementation("org.slf4j:slf4j-nop:2.0.6") + + implementation 'org.jetbrains.kotlin:kotlin-stdlib' + + implementation "gov.nist.math:jama:1.0.3" + + implementation wpi.java.deps.wpilib() + implementation wpi.java.vendor.java() + + roborioDebug wpi.java.deps.wpilibJniDebug(wpi.platforms.roborio) + roborioDebug wpi.java.vendor.jniDebug(wpi.platforms.roborio) + + 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-api:5.8.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' +} + +test { + useJUnitPlatform() + systemProperty 'junit.jupiter.extensions.autodetection.enabled', 'true' +} + +// Simulation configuration (e.g. environment variables). +wpi.sim.addGui().defaultEnabled = true +wpi.sim.addDriverstation() + +// Setting up my Jar File. In this case, adding all libraries into the main jar ('fat jar') +// in order to make them all available at runtime. Also adding the manifest so WPILib +// knows where to look for our Robot Class. +jar { + from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } + from sourceSets.main.allSource + manifest edu.wpi.first.gradlerio.GradleRIOPlugin.javaManifest(ROBOT_MAIN_CLASS) + duplicatesStrategy = DuplicatesStrategy.INCLUDE +} + +repositories { + mavenCentral() + maven { url 'https://jitpack.io' } +} + +sourceSets { + main.kotlin.srcDirs = ['src/main/kotlin'] + main.resources.srcDirs = ['src/main/resources'] +} + +wrapper { + gradleVersion = '7.6' +} + +// pre-commit hook to format +tasks.register('updateGitHooks', Copy) { + from './scripts/pre-commit' + into './.git/hooks' +} + +// 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) { + options.compilerArgs.add '-XDstringConcat=inline' +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7f93135 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..1058752 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=permwrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=permwrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..1aa94a4 --- /dev/null +++ b/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/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/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/networktables.json b/networktables.json new file mode 100644 index 0000000..41b42e6 --- /dev/null +++ b/networktables.json @@ -0,0 +1,3 @@ +[ + +] diff --git a/networktables.json.bck b/networktables.json.bck new file mode 100644 index 0000000..41b42e6 --- /dev/null +++ b/networktables.json.bck @@ -0,0 +1,3 @@ +[ + +] diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..e23ea1a --- /dev/null +++ b/settings.gradle @@ -0,0 +1,30 @@ +import org.gradle.internal.os.OperatingSystem + +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + String frcYear = '2024' + 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/simgui-ds.json b/simgui-ds.json new file mode 100644 index 0000000..69b1a3c --- /dev/null +++ b/simgui-ds.json @@ -0,0 +1,97 @@ +{ + "keyboardJoysticks": [ + { + "axisConfig": [ + { + "decKey": 65, + "incKey": 68 + }, + { + "decKey": 87, + "incKey": 83 + }, + { + "decKey": 69, + "decayRate": 0.0, + "incKey": 82, + "keyRate": 0.009999999776482582 + } + ], + "axisCount": 3, + "buttonCount": 4, + "buttonKeys": [ + 90, + 88, + 67, + 86 + ], + "povConfig": [ + { + "key0": 328, + "key135": 323, + "key180": 322, + "key225": 321, + "key270": 324, + "key315": 327, + "key45": 329, + "key90": 326 + } + ], + "povCount": 1 + }, + { + "axisConfig": [ + { + "decKey": 74, + "incKey": 76 + }, + { + "decKey": 73, + "incKey": 75 + } + ], + "axisCount": 2, + "buttonCount": 4, + "buttonKeys": [ + 77, + 44, + 46, + 47 + ], + "povCount": 0 + }, + { + "axisConfig": [ + { + "decKey": 263, + "incKey": 262 + }, + { + "decKey": 265, + "incKey": 264 + } + ], + "axisCount": 2, + "buttonCount": 6, + "buttonKeys": [ + 260, + 268, + 266, + 261, + 269, + 267 + ], + "povCount": 0 + }, + { + "axisCount": 0, + "buttonCount": 0, + "povCount": 0 + } + ], + "robotJoysticks": [ + { + "guid": "Keyboard0" + } + ] +} diff --git a/simgui.json b/simgui.json new file mode 100644 index 0000000..4739e7f --- /dev/null +++ b/simgui.json @@ -0,0 +1,349 @@ +{ + "HALProvider": { + "Addressable LEDs": { + "0": { + "columns": 2, + "order": 1 + }, + "window": { + "visible": true + } + } + }, + "NTProvider": { + "types": { + "/FMSInfo": "FMSInfo", + "/LiveWindow/Ungrouped/DigitalInput[5]": "Digital Input", + "/LiveWindow/Ungrouped/DigitalInput[6]": "Digital Input", + "/LiveWindow/Ungrouped/DigitalInput[7]": "Digital Input", + "/LiveWindow/Ungrouped/DigitalInput[8]": "Digital Input", + "/LiveWindow/Ungrouped/DigitalInput[9]": "Digital Input", + "/LiveWindow/Ungrouped/PIDController[10]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[11]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[12]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[13]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[14]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[15]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[16]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[17]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[18]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[19]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[1]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[20]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[21]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[22]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[23]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[24]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[25]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[26]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[27]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[28]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[29]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[2]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[30]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[31]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[32]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[33]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[34]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[35]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[36]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[3]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[4]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[5]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[6]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[7]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[8]": "PIDController", + "/LiveWindow/Ungrouped/PIDController[9]": "PIDController", + "/LiveWindow/Ungrouped/navX-Sensor[1]": "Gyro", + "/LiveWindow/Ungrouped/navX-Sensor[4]": "Gyro", + "/Monologuing/field": "Field2d", + "/Monologuing/robot/elevator/mech": "Mechanism2d", + "/Monologuing/routineChooser": "String Chooser", + "/Monologuing/scheduler": "Scheduler", + "/SmartDashboard/Command Scheduler": "Scheduler", + "/SmartDashboard/Elevator Visual": "Mechanism2d", + "/SmartDashboard/Field": "Field2d", + "/SmartDashboard/Routine Chooser": "String Chooser", + "/SmartDashboard/VisionSystemSim-main/Sim Field": "Field2d" + }, + "windows": { + "/Monologuing/field": { + "bumpers": { + "length": 0.9271000027656555, + "width": 0.8510000109672546 + }, + "height": 8.229599952697754, + "image": "C:\\Users\\jpoth\\Downloads\\field(1).png", + "odo": { + "color": [ + 0.7647056579589844, + 0.0, + 1.0, + 255.0 + ] + }, + "units": "meters", + "width": 16.459199905395508, + "window": { + "visible": true + } + }, + "/SmartDashboard/Command Scheduler": { + "window": { + "visible": true + } + }, + "/SmartDashboard/Field": { + "height": 8.013679504394531, + "width": 16.541748046875 + }, + "/SmartDashboard/Routine Chooser": { + "window": { + "visible": true + } + }, + "/SmartDashboard/VisionSystemSim-main/Sim Field": { + "EstimatedRobot": { + "color": [ + 0.7058820724487305, + 0.0, + 1.0, + 255.0 + ] + }, + "Robot": { + "color": [ + 0.0, + 1.0, + 0.08823513984680176, + 255.0 + ] + }, + "VisionEstimation": { + "color": [ + 0.17647027969360352, + 0.0, + 1.0, + 255.0 + ] + }, + "bottom": 544, + "builtin": "2023 Charged Up", + "cameras": { + "arrows": false, + "style": "Track" + }, + "height": 8.013679504394531, + "left": 46, + "right": 1088, + "top": 36, + "width": 16.541748046875, + "window": { + "visible": true + } + } + } + }, + "NetworkTables": { + "Retained Values": { + "open": false + }, + "transitory": { + "CameraPublisher": { + "arducam-processed": { + "open": true, + "string[]##v_/CameraPublisher/arducam-processed/streams": { + "open": true + } + }, + "open": true + }, + "Monologuing": { + "open": true, + "robot": { + "drive": { + "boolean[]##v_/Monologuing/robot/drive/2.1 Used Last Vision Estimate?": { + "open": true + }, + "double[]##v_/Monologuing/robot/drive/1.1 Estimated Pose": { + "open": true + }, + "double[]##v_/Monologuing/robot/drive/1.2 Vision Pose": { + "open": true + }, + "double[]##v_/Monologuing/robot/drive/1.3 Current Chassis Speeds": { + "open": true + }, + "double[]##v_/Monologuing/robot/drive/1.4 Desired Chassis Speeds": { + "open": true + }, + "double[]##v_/Monologuing/robot/drive/1.7 Vision Stats": { + "open": true + }, + "double[]##v_/Monologuing/robot/drive/2.1 Current Rotation": { + "open": true + }, + "double[]##v_/Monologuing/robot/drive/2.2 Desired Rotation": { + "open": true + }, + "double[]##v_/Monologuing/robot/drive/2.2 Number of Targets": { + "open": true + }, + "double[]##v_/Monologuing/robot/drive/2.3 Avg Tag Distance": { + "open": true + }, + "double[]##v_/Monologuing/robot/drive/2.4 Average Ambiguity": { + "open": true + }, + "double[]##v_/Monologuing/robot/drive/2.5 Cam Height Error": { + "open": true + }, + "double[]##v_/Monologuing/robot/drive/Current Chassis Speeds": { + "open": true + }, + "double[]##v_/Monologuing/robot/drive/Estimated Pose": { + "open": true + }, + "open": true + }, + "intake": { + "double[]##v_/Monologuing/robot/intake/3.1 Intake 3D Position": { + "open": true + } + }, + "open": true + } + }, + "Shuffleboard": { + "SwerveSim": { + "BLModule": { + "BLDrive": { + "open": true + }, + "BLTurn": { + "BLTurnEnc": { + "open": true + }, + "open": true + }, + "open": true + }, + "open": true + }, + "open": true + }, + "SmartDashboard": { + "VisionSystemSim-main": { + "open": true + }, + "open": true + }, + "photonvision": { + "arducam": { + "open": true + }, + "open": true + } + } + }, + "NetworkTables Info": { + "Connections": { + "open": true + }, + "Server": { + "open": true + } + }, + "Plot": { + "Plot <0>": { + "plots": [ + { + "backgroundColor": [ + 0.0, + 0.0, + 0.0, + 0.8500000238418579 + ], + "height": 852, + "series": [ + { + "color": [ + 0.3333333432674408, + 0.658823549747467, + 0.4078431725502014, + 1.0 + ], + "id": "NT:/Monologuing/robot/elevator/2.3 Desired Pos" + }, + { + "color": [ + 0.7686275243759155, + 0.30588236451148987, + 0.32156863808631897, + 1.0 + ], + "id": "NT:/Monologuing/robot/elevator/2.4 Desired Vel" + }, + { + "color": [ + 0.3333333432674408, + 0.658823549747467, + 0.4078431725502014, + 1.0 + ], + "id": "NT:/Monologuing/robot/elevator/2.1 Estimated Pos" + }, + { + "color": [ + 0.7686275243759155, + 0.30588236451148987, + 0.32156863808631897, + 1.0 + ], + "id": "NT:/Monologuing/robot/elevator/2.2 Estimated Vel" + } + ] + } + ], + "window": { + "visible": false + } + }, + "Plot <1>": { + "plots": [ + { + "backgroundColor": [ + 0.0, + 0.0, + 0.0, + 0.8500000238418579 + ], + "height": 859, + "series": [ + { + "color": [ + 0.2980392277240753, + 0.44705885648727417, + 0.6901960968971252, + 1.0 + ], + "id": "NT:/Monologuing/robot/elevator/1.1 Last voltage" + }, + { + "color": [ + 0.8666667342185974, + 0.5176470875740051, + 0.32156863808631897, + 1.0 + ], + "id": "NT:/Monologuing/robot/drive/4.1 Driving[0]" + } + ] + } + ], + "window": { + "visible": false + } + } + } +} diff --git a/src/main/deploy/choreo/BRP-449.chor b/src/main/deploy/choreo/BRP-449.chor new file mode 100644 index 0000000..b7f7f16 --- /dev/null +++ b/src/main/deploy/choreo/BRP-449.chor @@ -0,0 +1,3440 @@ +{ + "version": "v0.0.1", + "robotConfiguration": { + "mass": 60.0, + "rotationalInertia": 6, + "wheelMaxTorque": 1.675, + "wheelMaxVelocity": 88.06, + "wheelbase": 0.6223, + "trackWidth": 0.5461, + "bumperLength": 0.9271, + "bumperWidth": 0.851, + "wheelRadius": 0.0508 + }, + "paths": { + "4": { + "waypoints": [ + { + "x": 9.335601806640625, + "y": 0.9251009821891785, + "heading": 0, + "velocityMagnitude": 0, + "velocityAngle": 1.5638036450126473, + "translationConstrained": true, + "headingConstrained": true, + "velocityMagnitudeConstrained": true, + "velocityAngleConstrained": true, + "angularVelocity": 0, + "angularVelocityConstrained": true, + "controlIntervalCount": 0 + }, + { + "x": 9.422258377075195, + "y": 4.109755992889404, + "heading": 0, + "velocityMagnitude": 0, + "velocityAngle": 1.5605920080832691, + "translationConstrained": true, + "headingConstrained": true, + "velocityMagnitudeConstrained": false, + "velocityAngleConstrained": true, + "angularVelocity": 0, + "angularVelocityConstrained": false, + "controlIntervalCount": 0 + }, + { + "x": 9.433091163635254, + "y": 5.9728875160217285, + "heading": 0, + "velocityMagnitude": 0, + "velocityAngle": -2.57429121999024, + "translationConstrained": true, + "headingConstrained": true, + "velocityMagnitudeConstrained": false, + "velocityAngleConstrained": true, + "angularVelocity": 0, + "angularVelocityConstrained": false, + "controlIntervalCount": 0 + }, + { + "x": 6.638393402099609, + "y": 4.185581207275391, + "heading": 0, + "velocityMagnitude": 0, + "velocityAngle": 0.021273206755077097, + "translationConstrained": true, + "headingConstrained": true, + "velocityMagnitudeConstrained": false, + "velocityAngleConstrained": true, + "angularVelocity": 0, + "angularVelocityConstrained": false, + "controlIntervalCount": 0 + }, + { + "x": 10.98208999633789, + "y": 4.218077659606934, + "heading": 0, + "velocityMagnitude": 0, + "velocityAngle": 0, + "translationConstrained": true, + "headingConstrained": true, + "velocityMagnitudeConstrained": true, + "velocityAngleConstrained": false, + "angularVelocity": 0, + "angularVelocityConstrained": true, + "controlIntervalCount": 0 + } + ], + "trajectory": [ + { + "timestamp": 0, + "x": 9.335601806640623, + "y": 0.9251009821891784, + "heading": 7.433229425921144e-37, + "velocityX": 6.410368319804879e-34, + "velocityY": 1.619305695497464e-35, + "angularVelocity": 0 + }, + { + "timestamp": 0.038511682074740636, + "x": 9.335727411279946, + "y": 0.9291167961323064, + "heading": -1.0037001746092558e-19, + "velocityX": 0.0032614685351080117, + "velocityY": 0.10427521538359463, + "angularVelocity": -2.606222766890331e-18 + }, + { + "timestamp": 0.07702336414948127, + "x": 9.335978620458318, + "y": 0.9371484239902014, + "heading": -3.196922765189427e-19, + "velocityX": 0.006522934466587738, + "velocityY": 0.20855043003075938, + "angularVelocity": -5.694954314081186e-18 + }, + { + "timestamp": 0.11553504622422191, + "x": 9.336355434069395, + "y": 0.9491958657327835, + "heading": -5.546914236937845e-19, + "velocityX": 0.00978439763664558, + "velocityY": 0.31282564389686435, + "angularVelocity": -6.102022705973671e-18 + }, + { + "timestamp": 0.15404672829896254, + "x": 9.336857852000183, + "y": 0.9652591213280927, + "heading": -8.231194396614724e-19, + "velocityX": 0.013045857872694985, + "velocityY": 0.41710085693309584, + "angularVelocity": -6.970041653063732e-18 + }, + { + "timestamp": 0.1925584103737032, + "x": 9.337485874130397, + "y": 0.9853381907421074, + "heading": -1.1249774836078608e-18, + "velocityX": 0.01630731498544771, + "velocityY": 0.5213760690859163, + "angularVelocity": -7.838090699792929e-18 + }, + { + "timestamp": 0.23107009244844384, + "x": 9.338239500331737, + "y": 1.009433073938538, + "heading": -1.4602674866494168e-18, + "velocityX": 0.019568768766686964, + "velocityY": 0.625651280296435, + "angularVelocity": -8.706189890224704e-18 + }, + { + "timestamp": 0.2695817745231845, + "x": 9.33911873046705, + "y": 1.0375437708785924, + "heading": -1.828272374911586e-18, + "velocityX": 0.02283021898665661, + "velocityY": 0.7299264904996693, + "angularVelocity": -9.555669426461947e-18 + }, + { + "timestamp": 0.30809345659792514, + "x": 9.34012356438939, + "y": 1.0696702815207073, + "heading": -2.2506326768457678e-18, + "velocityX": 0.026091665390984165, + "velocityY": 0.8342016996236742, + "angularVelocity": -1.0967070115315713e-17 + }, + { + "timestamp": 0.3466051386726658, + "x": 9.341254001940927, + "y": 1.1058126058202418, + "heading": -2.7064302356007662e-18, + "velocityX": 0.029353107697033986, + "velocityY": 0.9384769075885119, + "angularVelocity": -1.1835306849731083e-17 + }, + { + "timestamp": 0.38511682074740644, + "x": 9.342510042951687, + "y": 1.1459707437291202, + "heading": -3.1731399809933165e-18, + "velocityX": 0.03261454558955918, + "velocityY": 1.0427521143050211, + "angularVelocity": -1.2118654298759269e-17 + }, + { + "timestamp": 0.4236285028221471, + "x": 9.343891687238102, + "y": 1.1901446951954218, + "heading": -3.649862808122623e-18, + "velocityX": 0.03587597871548388, + "velocityY": 1.1470273196733423, + "angularVelocity": -1.2378655457067792e-17 + }, + { + "timestamp": 0.46214018489688774, + "x": 9.345398934601302, + "y": 1.238334460162899, + "heading": -4.135624468405925e-18, + "velocityX": 0.03913740667759855, + "velocityY": 1.2513025235811337, + "angularVelocity": -1.2613359152524966e-17 + }, + { + "timestamp": 0.5006518669716283, + "x": 9.347031784825132, + "y": 1.2905400385704136, + "heading": -4.654835473451493e-18, + "velocityX": 0.04239882902688467, + "velocityY": 1.3555777259014021, + "angularVelocity": -1.3481909782101063e-17 + }, + { + "timestamp": 0.539163549046369, + "x": 9.348790237673777, + "y": 1.3467614303512696, + "heading": -5.207498428769433e-18, + "velocityX": 0.045660245253095165, + "velocityY": 1.4598529264898374, + "angularVelocity": -1.4350528088444295e-17 + }, + { + "timestamp": 0.5776752311211096, + "x": 9.350674292888971, + "y": 1.40699863543242, + "heading": -5.821509101817224e-18, + "velocityX": 0.04892165477309202, + "velocityY": 1.564128125181516, + "angularVelocity": -1.594349199585464e-17 + }, + { + "timestamp": 0.6161869131958503, + "x": 9.352683950186623, + "y": 1.4712516537335134, + "heading": -6.468978620099235e-18, + "velocityX": 0.05218305691626845, + "velocityY": 1.6684033217867782, + "angularVelocity": -1.681228931633707e-17 + }, + { + "timestamp": 0.6546985952705909, + "x": 9.354819209252733, + "y": 1.539520485165741, + "heading": -7.14991115378602e-18, + "velocityX": 0.055444450906134805, + "velocityY": 1.7726785160860195, + "angularVelocity": -1.76811949201849e-17 + }, + { + "timestamp": 0.6932102773453316, + "x": 9.357080069738416, + "y": 1.6118051296304272, + "heading": -6.612093128484066e-18, + "velocityX": 0.05870583583678967, + "velocityY": 1.8769537078230385, + "angularVelocity": 1.396506225469321e-17 + }, + { + "timestamp": 0.7317219594200722, + "x": 9.359466531253739, + "y": 1.688105587017289, + "heading": -6.110201743902472e-18, + "velocityX": 0.06196721064247079, + "velocityY": 1.9812288966964262, + "angularVelocity": 1.3032185803885427e-17 + }, + { + "timestamp": 0.7702336414948129, + "x": 9.361978593360037, + "y": 1.7684218572022588, + "heading": -5.660092663100996e-18, + "velocityX": 0.06522857405759094, + "velocityY": 2.085504082348265, + "angularVelocity": 1.1687598848392519e-17 + }, + { + "timestamp": 0.8087453235695535, + "x": 9.36461625556015, + "y": 1.852753940044729, + "heading": -5.2434652778834646e-18, + "velocityX": 0.06848992456345294, + "velocityY": 2.1897792643490614, + "angularVelocity": 1.0818208191711212e-17 + }, + { + "timestamp": 0.8472570056442942, + "x": 9.367379517285903, + "y": 1.9411018353840075, + "heading": -4.818491658640497e-18, + "velocityX": 0.07175126031593472, + "velocityY": 2.294054442177298, + "angularVelocity": 1.103492773996652e-17 + }, + { + "timestamp": 0.8857686877190348, + "x": 9.370268377881706, + "y": 2.0334655430346826, + "heading": -4.404491630170937e-18, + "velocityX": 0.07501257904536195, + "velocityY": 2.398329615191123, + "angularVelocity": 1.0749985878873683e-17 + }, + { + "timestamp": 0.9242803697937755, + "x": 9.373282836582714, + "y": 2.1298450627804466, + "heading": -4.0240108262658506e-18, + "velocityX": 0.07827387791466267, + "velocityY": 2.5026047825882425, + "angularVelocity": 9.879620738584363e-18 + }, + { + "timestamp": 0.9627920518685161, + "x": 9.37642289248502, + "y": 2.2302403943656666, + "heading": -3.6504323401485956e-18, + "velocityX": 0.08153515331304911, + "velocityY": 2.606879943347587, + "angularVelocity": 9.700394156725681e-18 + }, + { + "timestamp": 1.0013037339432567, + "x": 9.37968854450395, + "y": 2.3346515374835928, + "heading": -3.3103873083446697e-18, + "velocityX": 0.08479640054654522, + "velocityY": 2.7111550961418156, + "angularVelocity": 8.829659523974913e-18 + }, + { + "timestamp": 1.0398154160179973, + "x": 9.383079791313795, + "y": 2.443078491759321, + "heading": -3.0038939088056992e-18, + "velocityX": 0.08805761335659339, + "velocityY": 2.8154302392012207, + "angularVelocity": 7.958452932319959e-18 + }, + { + "timestamp": 1.078327098092738, + "x": 9.386596631257447, + "y": 2.55552125672424, + "heading": -2.712666864172415e-18, + "velocityX": 0.09131878313780757, + "velocityY": 2.9197053700925846, + "angularVelocity": 7.562044502762482e-18 + }, + { + "timestamp": 1.1168387801674786, + "x": 9.390239062204358, + "y": 2.6719798317758787, + "heading": -2.455043433781108e-18, + "velocityX": 0.09457989759701421, + "velocityY": 3.023980485340122, + "angularVelocity": 6.689488102333705e-18 + }, + { + "timestamp": 1.1553504622422193, + "x": 9.394007081313857, + "y": 2.792454216110998, + "heading": -2.2066601032107886e-18, + "velocityX": 0.09784093829491186, + "velocityY": 3.1282555797306153, + "angularVelocity": 6.44955830195378e-18 + }, + { + "timestamp": 1.19386214431696, + "x": 9.397900684609104, + "y": 2.9169444086051644, + "heading": -1.933386641992854e-18, + "velocityX": 0.10110187572961327, + "velocityY": 3.2325306449239046, + "angularVelocity": 7.095859159001267e-18 + }, + { + "timestamp": 1.2323738263917006, + "x": 9.401919866125063, + "y": 3.0454504075719497, + "heading": -1.7122081073894685e-18, + "velocityX": 0.10436265827531789, + "velocityY": 3.336805666327168, + "angularVelocity": 5.743154556387858e-18 + }, + { + "timestamp": 1.2708855084664412, + "x": 9.406064615920602, + "y": 3.1779722102012298, + "heading": -1.476155173514594e-18, + "velocityX": 0.10762318268827334, + "velocityY": 3.441080614762319, + "angularVelocity": 6.129385437640386e-18 + }, + { + "timestamp": 1.3093971905411819, + "x": 9.410334914115749, + "y": 3.3145098108749202, + "heading": -1.2376298640724743e-18, + "velocityX": 0.110883190894905, + "velocityY": 3.545355417317451, + "angularVelocity": 6.1935835205216205e-18 + }, + { + "timestamp": 1.3479088726159225, + "x": 9.414730701089338, + "y": 3.4550631927552793, + "heading": 2.8326472990512134e-19, + "velocityX": 0.11414165096994072, + "velocityY": 3.6496297826613113, + "angularVelocity": 3.9491773945227515e-17 + }, + { + "timestamp": 1.3864205546906632, + "x": 9.416487602702174, + "y": 3.594033329865462, + "heading": 2.0824247764857095e-19, + "velocityX": 0.0456199656347806, + "velocityY": 3.608519016138527, + "angularVelocity": -1.948038506847508e-18 + }, + { + "timestamp": 1.4249322367654038, + "x": 9.418118779161949, + "y": 3.72898769314333, + "heading": 1.2837991956347999e-19, + "velocityX": 0.04235536782269175, + "velocityY": 3.5042448422782067, + "angularVelocity": -2.073722649094852e-18 + }, + { + "timestamp": 1.4634439188401445, + "x": 9.419624288998323, + "y": 3.859926262067809, + "heading": 8.114065042866733e-20, + "velocityX": 0.039092289800198535, + "velocityY": 3.399970135564655, + "angularVelocity": -1.2266214983339523e-18 + }, + { + "timestamp": 1.5019556009148851, + "x": 9.421004151718188, + "y": 3.9868490297914985, + "heading": 4.235807718061273e-20, + "velocityX": 0.03582971829639282, + "velocityY": 3.2956952510504913, + "angularVelocity": -1.0070336999424748e-18 + }, + { + "timestamp": 1.5404672829896258, + "x": 9.422258377075195, + "y": 4.109755992889404, + "heading": 0, + "velocityX": 0.032567400057303936, + "velocityY": 3.19142027760242, + "angularVelocity": -1.099875807928052e-18 + }, + { + "timestamp": 1.5728708853392595, + "x": 9.424747642519675, + "y": 4.2107130428092905, + "heading": -1.4040882525966132e-20, + "velocityX": 0.07682063918758748, + "velocityY": 3.1156119258149833, + "angularVelocity": -4.3331207807652294e-19 + }, + { + "timestamp": 1.6052744876888931, + "x": 9.428378265685053, + "y": 4.30906475724215, + "heading": -1.1819913684396256e-20, + "velocityX": 0.11204381309845972, + "velocityY": 3.0352092761677727, + "angularVelocity": 6.854112197807745e-20 + }, + { + "timestamp": 1.6376780900385268, + "x": 9.432897595673024, + "y": 4.404714495874338, + "heading": 2.3349695483900227e-21, + "velocityX": 0.1394699866764316, + "velocityY": 2.9518242323841037, + "angularVelocity": 4.368308637354792e-19 + }, + { + "timestamp": 1.6700816923881605, + "x": 9.438091279218941, + "y": 4.497600953370514, + "heading": 2.4915089595540556e-20, + "velocityX": 0.1602810542444682, + "velocityY": 2.8665472589724654, + "angularVelocity": 6.968401212937426e-19 + }, + { + "timestamp": 1.7024852947377942, + "x": 9.443778467028542, + "y": 4.5876861734749, + "heading": 5.304419057485532e-20, + "velocityX": 0.1755109740033915, + "velocityY": 2.780098926420832, + "angularVelocity": 8.680859138130865e-19 + }, + { + "timestamp": 1.7348888970874279, + "x": 9.4498063984981, + "y": 4.674947500361938, + "heading": 8.434161630297136e-20, + "velocityX": 0.1860265844678467, + "velocityY": 2.6929514177310208, + "angularVelocity": 9.658628285962778e-19 + }, + { + "timestamp": 1.7672924994370616, + "x": 9.456045423647167, + "y": 4.759372294130309, + "heading": 1.1680908657410498e-19, + "velocityX": 0.19254109718263593, + "velocityY": 2.6054138320002553, + "angularVelocity": 1.0019712819026323e-18 + }, + { + "timestamp": 1.7996961017866953, + "x": 9.462384788164714, + "y": 4.840954480993348, + "heading": 1.261900827710795e-19, + "velocityX": 0.1956376469858513, + "velocityY": 2.5176888045585115, + "angularVelocity": 2.8950495381581065e-19 + }, + { + "timestamp": 1.832099704136329, + "x": 9.468729194239346, + "y": 4.919692294857876, + "heading": 1.3490263100448028e-19, + "velocityX": 0.1957932333009324, + "velocityY": 2.429909274127901, + "angularVelocity": 2.688761178225079e-19 + }, + { + "timestamp": 1.8645033064859626, + "x": 9.474996039144651, + "y": 4.9955867887508845, + "heading": 1.3028928235433524e-19, + "velocityX": 0.19339963617894118, + "velocityY": 2.3421622409172134, + "angularVelocity": -1.42371308032916e-19 + }, + { + "timestamp": 1.8969069088355963, + "x": 9.48111320970876, + "y": 5.068640845584036, + "heading": 1.2133559071887303e-19, + "velocityX": 0.188780571311318, + "velocityY": 2.2545041765696707, + "angularVelocity": -2.763176447663805e-19 + }, + { + "timestamp": 1.92931051118523, + "x": 9.487017319760973, + "y": 5.138858515433651, + "heading": 1.0707275159759722e-19, + "velocityX": 0.1822053606419894, + "velocityY": 2.166971100680984, + "angularVelocity": -4.401620322213563e-19 + }, + { + "timestamp": 1.9617141135348637, + "x": 9.49265229621183, + "y": 5.206244568479539, + "heading": 2.2241510805607453e-20, + "velocityX": 0.17389969146195045, + "velocityY": 2.079585236196706, + "angularVelocity": -2.6179570889795982e-18 + }, + { + "timestamp": 1.9941177158844974, + "x": 9.49796823847145, + "y": 5.270804191896735, + "heading": -6.952468961730891e-20, + "velocityX": 0.16405405183849694, + "velocityY": 1.992359451909064, + "angularVelocity": -2.831975287500493e-18 + }, + { + "timestamp": 2.0265213182341313, + "x": 9.50292049238692, + "y": 5.332542783830851, + "heading": -2.2622195221612505e-19, + "velocityX": 0.15283035083673954, + "velocityY": 1.9053002585317274, + "angularVelocity": -4.835797721349406e-18 + }, + { + "timestamp": 2.058924920583765, + "x": 9.507468893167124, + "y": 5.391465813474863, + "heading": -1.8882276010757942e-19, + "velocityX": 0.1403671336021788, + "velocityY": 1.8184098486407023, + "angularVelocity": 1.1541678128651725e-18 + }, + { + "timestamp": 2.0913285229333987, + "x": 9.51157714213314, + "y": 5.447578726533896, + "heading": -1.6023011535688134e-19, + "velocityX": 0.12678371131977367, + "velocityY": 1.731687497383078, + "angularVelocity": 8.823909099626407e-19 + }, + { + "timestamp": 2.1237321252830323, + "x": 9.515212290099717, + "y": 5.5008868820776256, + "heading": -1.4095626507013954e-19, + "velocityX": 0.11218345193084106, + "velocityY": 1.645130531122315, + "angularVelocity": 5.948057382360047e-19 + }, + { + "timestamp": 2.156135727632666, + "x": 9.518344306276155, + "y": 5.551395511219431, + "heading": -2.248024505790623e-19, + "velocityX": 0.09665641932782838, + "velocityY": 1.5587350010291767, + "angularVelocity": -2.5875577312700087e-18 + }, + { + "timestamp": 2.1885393299822997, + "x": 9.5209457162136, + "y": 5.599109691031203, + "heading": -3.188656274851271e-19, + "velocityX": 0.08028150417891033, + "velocityY": 1.4724961532652485, + "angularVelocity": -2.9028619499481455e-18 + }, + { + "timestamp": 2.2209429323319334, + "x": 9.522991295869932, + "y": 5.644034329110968, + "heading": -4.891966679502781e-19, + "velocityX": 0.0631281557606156, + "velocityY": 1.386408757737169, + "angularVelocity": -5.256546776096603e-18 + }, + { + "timestamp": 2.253346534681567, + "x": 9.524457811581927, + "y": 5.686174155592809, + "heading": -6.705056202157354e-19, + "velocityX": 0.045257798690727956, + "velocityY": 1.3004673377716809, + "angularVelocity": -5.595333582626272e-18 + }, + { + "timestamp": 2.285750137031201, + "x": 9.525323797829595, + "y": 5.725533720335378, + "heading": -6.435565832114582e-19, + "velocityX": 0.026724999224586724, + "velocityY": 1.214666329930906, + "angularVelocity": 8.316679571589788e-19 + }, + { + "timestamp": 2.3181537393808345, + "x": 9.525569366301788, + "y": 5.762117393684421, + "heading": -6.282377661829914e-19, + "velocityX": 0.007578431235604929, + "velocityY": 1.1290001943088244, + "angularVelocity": 4.727504195076832e-19 + }, + { + "timestamp": 2.350557341730468, + "x": 9.525176041038433, + "y": 5.795929369667312, + "heading": -6.248418889518024e-19, + "velocityX": -0.012138319039683754, + "velocityY": 1.043463489585527, + "angularVelocity": 1.0479932342819168e-19 + }, + { + "timestamp": 2.382960944080102, + "x": 9.524126615418227, + "y": 5.826973670804758, + "heading": -6.3364197761870355e-19, + "velocityX": -0.032386078834140526, + "velocityY": 0.9580509229337928, + "angularVelocity": -2.715775677311782e-19 + }, + { + "timestamp": 2.4153645464297355, + "x": 9.522405027544709, + "y": 5.855254153957586, + "heading": -6.548936006090998e-19, + "velocityX": -0.0531295210619789, + "velocityY": 0.8727573819627032, + "angularVelocity": -6.558414967128584e-19 + }, + { + "timestamp": 2.447768148779369, + "x": 9.519996251206273, + "y": 5.880774516793202, + "heading": -6.888362196158089e-19, + "velocityX": -0.07433668369481979, + "velocityY": 0.7875779538414581, + "angularVelocity": -1.047495397394818e-18 + }, + { + "timestamp": 2.480171751129003, + "x": 9.516886200083079, + "y": 5.9035383045762275, + "heading": -5.506007832682073e-19, + "velocityX": -0.09597856095246612, + "velocityY": 0.7025079353031433, + "angularVelocity": 4.26605150961703e-18 + }, + { + "timestamp": 2.5125753534786366, + "x": 9.513061643273414, + "y": 5.923548917074331, + "heading": -5.043259035657883e-19, + "velocityX": -0.11802875397613825, + "velocityY": 0.6175428362004514, + "angularVelocity": 1.4280781303800316e-18 + }, + { + "timestamp": 2.5449789558282703, + "x": 9.508510130534903, + "y": 5.940809615433076, + "heading": -4.7137835362947e-19, + "velocityX": -0.14046317102031355, + "velocityY": 0.5326783785488524, + "angularVelocity": 1.0167864816487845e-18 + }, + { + "timestamp": 2.577382558177904, + "x": 9.503219925898293, + "y": 5.955323528919038, + "heading": -4.519466372685079e-19, + "velocityX": -0.1632597690691616, + "velocityY": 0.4479104924618523, + "angularVelocity": 5.996774606395658e-19 + }, + { + "timestamp": 2.6097861605275376, + "x": 9.49717994852565, + "y": 5.967093661463571, + "heading": -4.462088381994008e-19, + "velocityX": -0.1863983302680957, + "velocityY": 0.36323530999837034, + "angularVelocity": 1.7707267952265156e-19 + }, + { + "timestamp": 2.6421897628771713, + "x": 9.490379719860622, + "y": 5.976122897963523, + "heading": -4.543338183787842e-19, + "velocityX": -0.20986026774601668, + "velocityY": 0.2786491576623603, + "angularVelocity": -2.507433513307693e-19 + }, + { + "timestamp": 2.674593365226805, + "x": 9.48280931626334, + "y": 5.982414010312601, + "heading": -4.764815671962873e-19, + "velocityX": -0.2336284563548335, + "velocityY": 0.1941485480903284, + "angularVelocity": -6.834966224725472e-19 + }, + { + "timestamp": 2.7069969675764387, + "x": 9.47445932644268, + "y": 5.985969663149571, + "heading": -5.128041449919727e-19, + "velocityX": -0.2576870846199469, + "velocityY": 0.10973017131256312, + "angularVelocity": -1.1209427475481004e-18 + }, + { + "timestamp": 2.7394005699260724, + "x": 9.465320813098467, + "y": 5.986792419318408, + "heading": -4.5098715503885855e-19, + "velocityX": -0.282021524817208, + "velocityY": 0.02539088586384956, + "angularVelocity": 1.9077196088656083e-18 + }, + { + "timestamp": 2.771804172275706, + "x": 9.455385278269853, + "y": 5.984884745041843, + "heading": -2.8602806089077776e-19, + "velocityX": -0.30661821859834004, + "velocityY": -0.058872290061494546, + "angularVelocity": 5.090764079296635e-18 + }, + { + "timestamp": 2.8042077746253398, + "x": 9.44464463195612, + "y": 5.980249014814116, + "heading": -1.3565733993611353e-19, + "velocityX": -0.3314645760012335, + "velocityY": -0.1430621872749743, + "angularVelocity": 4.640555662244342e-18 + }, + { + "timestamp": 2.8366113769749735, + "x": 9.433091163635254, + "y": 5.9728875160217285, + "heading": 0, + "velocityX": -0.35654888602214, + "velocityY": -0.22718149398816284, + "angularVelocity": 4.186489407727029e-18 + }, + { + "timestamp": 2.8898593064936966, + "x": 9.407507023330087, + "y": 5.956859606510648, + "heading": 1.6012272048785258e-19, + "velocityX": -0.48047202091060703, + "velocityY": -0.30100530961387933, + "angularVelocity": 3.0071165753743872e-18 + }, + { + "timestamp": 2.9431072360124197, + "x": 9.37532475203567, + "y": 5.936899860175698, + "heading": 2.1188176174280419e-19, + "velocityX": -0.6043854021234748, + "velocityY": -0.3748454919346753, + "angularVelocity": 9.720385830700054e-19 + }, + { + "timestamp": 2.996355165531143, + "x": 9.336544934923388, + "y": 5.913007295411148, + "heading": 1.969629201732152e-19, + "velocityX": -0.7282877937750748, + "velocityY": -0.4487041088827337, + "angularVelocity": -2.8017677957318397e-19 + }, + { + "timestamp": 3.049603095049866, + "x": 9.291168236326735, + "y": 5.885180798221746, + "heading": -5.69918739629597e-19, + "velocityX": -0.8521777091952178, + "velocityY": -0.522583646742897, + "angularVelocity": -1.4402092419953408e-17 + }, + { + "timestamp": 3.102851024568589, + "x": 9.23919541697011, + "y": 5.853419093502993, + "heading": -9.887646126362287e-19, + "velocityX": -0.9760533381556034, + "velocityY": -0.5964871311584838, + "angularVelocity": -7.865955543786426e-18 + }, + { + "timestamp": 3.1560989540873123, + "x": 9.180627356588191, + "y": 5.817720707366942, + "heading": -1.4823749754006185e-18, + "velocityX": -1.0999124456346128, + "velocityY": -0.6704182952972421, + "angularVelocity": -9.270037591446577e-18 + }, + { + "timestamp": 3.2093468836060355, + "x": 9.115465084222988, + "y": 5.778083916728607, + "heading": -2.054357388107318e-18, + "velocityX": -1.2237522276296537, + "velocityY": -0.7443818190977213, + "angularVelocity": -1.074186987525422e-17 + }, + { + "timestamp": 3.2625948131247586, + "x": 9.043709819758144, + "y": 5.734506680271107, + "heading": -2.7089851843439773e-18, + "velocityX": -1.34756910012082, + "velocityY": -0.8183836789781219, + "angularVelocity": -1.2293955666979117e-17 + }, + { + "timestamp": 3.3158427426434818, + "x": 8.96536303243276, + "y": 5.6869865413172445, + "heading": -3.031687342340883e-18, + "velocityX": -1.4713583801945858, + "velocityY": -0.8924316754354307, + "angularVelocity": -6.060368156692205e-18 + }, + { + "timestamp": 3.369090672162205, + "x": 8.880426525998846, + "y": 5.635520486698401, + "heading": -2.954956262751476e-18, + "velocityX": -1.5951137856739797, + "velocityY": -0.9665362594191681, + "angularVelocity": 1.4410169410596809e-18 + }, + { + "timestamp": 3.422338601680928, + "x": 8.788902567634885, + "y": 5.580104733519928, + "heading": -2.8096198996615185e-18, + "velocityX": -1.7188266133761843, + "velocityY": -1.0407118864403515, + "angularVelocity": 2.729429230220988e-18 + }, + { + "timestamp": 3.475586531199651, + "x": 8.690794092860108, + "y": 5.520734391034758, + "heading": -2.444203489232445e-18, + "velocityX": -1.8424843118131036, + "velocityY": -1.114979362048128, + "angularVelocity": 6.862548144238517e-18 + }, + { + "timestamp": 3.5288344607183744, + "x": 8.586105052135341, + "y": 5.457402890506253, + "heading": -1.78509297458333e-18, + "velocityX": -1.9660678203075501, + "velocityY": -1.1893701990090748, + "angularVelocity": 1.2378145100294227e-17 + }, + { + "timestamp": 3.5820823902370975, + "x": 8.474841047191472, + "y": 5.39010094286798, + "heading": -1.7926897958080077e-18, + "velocityX": -2.089546127887395, + "velocityY": -1.2639354853151212, + "angularVelocity": -1.4266697089259563e-19 + }, + { + "timestamp": 3.6353303197558207, + "x": 8.357010639749344, + "y": 5.318814407701335, + "heading": -1.5556843330116533e-18, + "velocityX": -2.2128636457253266, + "velocityY": -1.3387663297138852, + "angularVelocity": 4.450981660874935e-18 + }, + { + "timestamp": 3.688578249274544, + "x": 8.232628539321633, + "y": 5.243519150108811, + "heading": -7.118729576928097e-19, + "velocityX": -2.335904917842405, + "velocityY": -1.4140504292480984, + "angularVelocity": 1.5846840684725827e-17 + }, + { + "timestamp": 3.741826178793267, + "x": 8.1017259282265, + "y": 5.164164657780524, + "heading": 5.54294098791122e-19, + "velocityX": -2.4583605837500735, + "velocityY": -1.490283153646069, + "angularVelocity": 2.377871119104557e-17 + }, + { + "timestamp": 3.79507410831199, + "x": 7.964413173666446, + "y": 5.080578830515485, + "heading": 5.745487949551461e-19, + "velocityX": -2.578743545545228, + "velocityY": -1.5697479323707426, + "angularVelocity": 3.8037946752418803e-19 + }, + { + "timestamp": 3.8483220378307132, + "x": 7.834386570789792, + "y": 4.9994228629449955, + "heading": 6.345872549710333e-19, + "velocityX": -2.4419090855154173, + "velocityY": -1.5241149900852726, + "angularVelocity": 1.127523761469392e-18 + }, + { + "timestamp": 3.9015699673494364, + "x": 7.711094579965497, + "y": 4.921960039014121, + "heading": 3.1385402523746345e-19, + "velocityX": -2.3154325799830713, + "velocityY": -1.454757483173811, + "angularVelocity": -6.023395532446379e-18 + }, + { + "timestamp": 3.9548178968681595, + "x": 7.5944798531964945, + "y": 4.848293085659303, + "heading": -5.504264105297611e-19, + "velocityX": -2.190033074018357, + "velocityY": -1.3834707569036095, + "angularVelocity": -1.6231250991360332e-17 + }, + { + "timestamp": 4.008065826386883, + "x": 7.484520668230775, + "y": 4.778459970937141, + "heading": -9.581601178782412e-19, + "velocityX": -2.0650415135306908, + "velocityY": -1.3114709877612607, + "angularVelocity": -7.65727038238914e-18 + }, + { + "timestamp": 4.061313755905606, + "x": 7.381205602053184, + "y": 4.71248046175784, + "heading": -1.4648466714460738e-18, + "velocityX": -1.94026447810076, + "velocityY": -1.2390999946035701, + "angularVelocity": -9.515611861125773e-18 + }, + { + "timestamp": 4.114561685424329, + "x": 7.28452761021566, + "y": 4.650366680934777, + "heading": -1.6254449722260528e-18, + "velocityX": -1.8156197379191947, + "velocityY": -1.1665013341264656, + "angularVelocity": -3.016049458842738e-18 + }, + { + "timestamp": 4.167809614943052, + "x": 7.194481914315111, + "y": 4.592126822746646, + "heading": -1.4228313180181299e-18, + "velocityX": -1.6910647364962277, + "velocityY": -1.0937487844978486, + "angularVelocity": 3.805097285489222e-18 + }, + { + "timestamp": 4.221057544461775, + "x": 7.111065060119478, + "y": 4.537766796263652, + "heading": -1.2643706503290093e-18, + "velocityX": -1.5665746058032446, + "velocityY": -1.0208852620998259, + "angularVelocity": 2.975901875587679e-18 + }, + { + "timestamp": 4.274305473980498, + "x": 7.034274434143014, + "y": 4.487291064295607, + "heading": -7.210191645961071e-19, + "velocityX": -1.4421335565632167, + "velocityY": -0.9479379278080052, + "angularVelocity": 1.0204178839174677e-17 + }, + { + "timestamp": 4.327553403499222, + "x": 6.964107990003666, + "y": 4.440703116458126, + "heading": -9.013376647533111e-19, + "velocityX": -1.3177309385273974, + "velocityY": -0.8749250582804436, + "angularVelocity": -3.3863949737865667e-18 + }, + { + "timestamp": 4.380801333017945, + "x": 6.9005640819078415, + "y": 4.398005756206596, + "heading": -6.836090002944485e-19, + "velocityX": -1.193359228615281, + "velocityY": -0.801859539656196, + "angularVelocity": 4.088959478659132e-18 + }, + { + "timestamp": 4.434049262536668, + "x": 6.843641357513053, + "y": 4.359201285097782, + "heading": -4.827360221656651e-19, + "velocityX": -1.0690129157937158, + "velocityY": -0.7287507976280857, + "angularVelocity": 3.772408486667274e-18 + }, + { + "timestamp": 4.487297192055391, + "x": 6.793338685868074, + "y": 4.324291626496749, + "heading": 1.250178093016813e-19, + "velocityX": -0.9446878423186736, + "velocityY": -0.6556059346637725, + "angularVelocity": 1.1413660606865953e-17 + }, + { + "timestamp": 4.540545121574114, + "x": 6.749655107161122, + "y": 4.293278411715859, + "heading": 7.233486990776333e-19, + "velocityX": -0.8203807941788867, + "velocityY": -0.5824304355343005, + "angularVelocity": 1.1236697086483106e-17 + }, + { + "timestamp": 4.593793051092837, + "x": 6.7125897966065144, + "y": 4.266163041840482, + "heading": 1.3151864719949234e-18, + "velocityX": -0.6960892355743994, + "velocityY": -0.509228623919408, + "angularVelocity": 1.1114755884317254e-17 + }, + { + "timestamp": 4.6470409806115605, + "x": 6.682142037824221, + "y": 4.242946733254192, + "heading": 1.4833598477443198e-18, + "velocityX": -0.5718111306391168, + "velocityY": -0.43600396853225964, + "angularVelocity": 3.1583083402507556e-18 + }, + { + "timestamp": 4.700288910130284, + "x": 6.65831120278551, + "y": 4.223630551903339, + "heading": 1.64981294853882e-18, + "velocityX": -0.4475448201292212, + "velocityY": -0.36275929459493594, + "angularVelocity": 3.1260014476336314e-18 + }, + { + "timestamp": 4.753536839649007, + "x": 6.641096736417822, + "y": 4.208215439579853, + "heading": 1.3968003826691399e-18, + "velocityX": -0.3232889339224929, + "velocityY": -0.2894969337364861, + "angularVelocity": -4.7515940065872536e-18 + }, + { + "timestamp": 4.80678476916773, + "x": 6.630498144590057, + "y": 4.196702234418521, + "heading": 7.260749450146028e-19, + "velocityX": -0.19904232753385848, + "velocityY": -0.21621883264557085, + "angularVelocity": -1.2596271848649556e-17 + }, + { + "timestamp": 4.860032698686453, + "x": 6.626514984598739, + "y": 4.189091687117633, + "heading": 4.786365666958557e-19, + "velocityX": -0.07480403514876906, + "velocityY": -0.14292663338602665, + "angularVelocity": -4.6469100159348216e-18 + }, + { + "timestamp": 4.913280628205176, + "x": 6.629146857536548, + "y": 4.185384473943056, + "heading": 2.3618468980917354e-19, + "velocityX": 0.04942676572774048, + "velocityY": -0.06962173380419516, + "angularVelocity": -4.553263539134624e-18 + }, + { + "timestamp": 4.966528557723899, + "x": 6.638393402099609, + "y": 4.185581207275391, + "heading": 0, + "velocityX": 0.17365078129113545, + "velocityY": 0.0036946663299880058, + "angularVelocity": -4.435565056192453e-18 + }, + { + "timestamp": 5.028339052223556, + "x": 6.659476247359193, + "y": 4.185879969977683, + "heading": -2.458396276404072e-19, + "velocityX": 0.34108844186160825, + "velocityY": 0.00483352713338811, + "angularVelocity": -3.9773122639999994e-18 + }, + { + "timestamp": 5.090149546723213, + "x": 6.690908497115958, + "y": 4.186249126225705, + "heading": -4.794739448057151e-19, + "velocityX": 0.5085261008054116, + "velocityY": 0.005972387877019005, + "angularVelocity": -3.779848850849013e-18 + }, + { + "timestamp": 5.151960041222869, + "x": 6.73269015125753, + "y": 4.186688676015329, + "heading": -7.009030331817381e-19, + "velocityX": 0.6759637579311725, + "velocityY": 0.007111248553858993, + "angularVelocity": -3.582386770179082e-18 + }, + { + "timestamp": 5.213770535722526, + "x": 6.784821209657488, + "y": 4.187198619341911, + "heading": -9.10126991100271e-19, + "velocityX": 0.8434014130116355, + "velocityY": 0.008250109155574243, + "angularVelocity": -3.3849262788686062e-18 + }, + { + "timestamp": 5.275581030222183, + "x": 6.847301672172556, + "y": 4.18777895620019, + "heading": -1.1071459324547749e-18, + "velocityX": 1.010839065774094, + "velocityY": 0.00938896967216917, + "angularVelocity": -3.187467628455642e-18 + }, + { + "timestamp": 5.3373915247218395, + "x": 6.920131538638988, + "y": 4.188429686584155, + "heading": -1.3138363613465469e-18, + "velocityX": 1.1782767158874046, + "velocityY": 0.01052783009151207, + "angularVelocity": -3.343937620344753e-18 + }, + { + "timestamp": 5.399202019221496, + "x": 7.003310808867847, + "y": 4.189150810486871, + "heading": -1.5083220359631332e-18, + "velocityX": 1.3457143629440058, + "velocityY": 0.011666690398678441, + "angularVelocity": -3.14648320673789e-18 + }, + { + "timestamp": 5.461012513721153, + "x": 7.096839482638708, + "y": 4.189942327900254, + "heading": -1.6778418861102721e-18, + "velocityX": 1.5131520064344455, + "velocityY": 0.01280555057502086, + "angularVelocity": -2.742574040198153e-18 + }, + { + "timestamp": 5.52282300822081, + "x": 7.200717559691066, + "y": 4.19080423881475, + "heading": -1.8212468135453705e-18, + "velocityX": 1.6805896457103302, + "velocityY": 0.013944410596816349, + "angularVelocity": -2.3200742045641136e-18 + }, + { + "timestamp": 5.584633502720466, + "x": 7.314945039712318, + "y": 4.1917365432189, + "heading": -1.9371340770567474e-18, + "velocityX": 1.8480272799287492, + "velocityY": 0.015083270433237763, + "angularVelocity": -1.8748801503066307e-18 + }, + { + "timestamp": 5.646443997220123, + "x": 7.43952192232039, + "y": 4.1927392410987085, + "heading": -2.0408174233321747e-18, + "velocityX": 2.01546490796582, + "velocityY": 0.016222130043198665, + "angularVelocity": -1.6774392811054847e-18 + }, + { + "timestamp": 5.70825449171978, + "x": 7.574448207037693, + "y": 4.193812332436685, + "heading": -2.1131553651126336e-18, + "velocityX": 2.1829025282762045, + "velocityY": 0.017360989370227026, + "angularVelocity": -1.1703181963552742e-18 + }, + { + "timestamp": 5.770064986219436, + "x": 7.7197238932501815, + "y": 4.194955817210358, + "heading": -2.195166575952037e-18, + "velocityX": 2.350340138652278, + "velocityY": 0.018499848333678435, + "angularVelocity": -1.326817010497694e-18 + }, + { + "timestamp": 5.831875480719093, + "x": 7.8753489801391305, + "y": 4.196169695389781, + "heading": -2.264975230718563e-18, + "velocityX": 2.517777735782602, + "velocityY": 0.019638706812628887, + "angularVelocity": -1.129398162801884e-18 + }, + { + "timestamp": 5.89368597521875, + "x": 8.041323466558321, + "y": 4.1974539669330495, + "heading": -2.3072687042707436e-18, + "velocityX": 2.6852153143688753, + "velocityY": 0.020777564613663587, + "angularVelocity": -6.842442749279986e-19 + }, + { + "timestamp": 5.9554964697184065, + "x": 8.21764735078843, + "y": 4.198808631777348, + "heading": -2.337361894093645e-18, + "velocityX": 2.8526528651390715, + "velocityY": 0.02191642139840774, + "angularVelocity": -4.868621941767107e-19 + }, + { + "timestamp": 6.017306964218063, + "x": 8.404320629963914, + "y": 4.200233689818039, + "heading": -2.355180833690065e-18, + "velocityX": 3.0200903695491412, + "velocityY": 0.02305527649028737, + "angularVelocity": -2.8828343845164218e-19 + }, + { + "timestamp": 6.07911745871772, + "x": 8.601343298353688, + "y": 4.201729140845959, + "heading": -2.3608060407320693e-18, + "velocityX": 3.187527781238981, + "velocityY": 0.02419412819822115, + "angularVelocity": -9.100734554654671e-20 + }, + { + "timestamp": 6.140927953217377, + "x": 8.80871533876452, + "y": 4.203294984233899, + "heading": -2.3351084008043562e-18, + "velocityX": 3.3549649147683756, + "velocityY": 0.02533296975887398, + "angularVelocity": 4.157487894925156e-19 + }, + { + "timestamp": 6.202738447717033, + "x": 9.015703343151486, + "y": 4.204702859409942, + "heading": -2.1446820364753337e-18, + "velocityX": 3.3487517946991447, + "velocityY": 0.022777283816251635, + "angularVelocity": 3.0808101776722092e-18 + }, + { + "timestamp": 6.26454894221669, + "x": 9.212341975531281, + "y": 4.2060403401824225, + "heading": -2.0047712820216723e-18, + "velocityX": 3.1813146613943504, + "velocityY": 0.021638409194230757, + "angularVelocity": 2.263543788726926e-18 + }, + { + "timestamp": 6.326359436716347, + "x": 9.398631218703736, + "y": 4.207307426944435, + "heading": -1.8387963261016747e-18, + "velocityX": 3.0138772498170407, + "velocityY": 0.02049954093181682, + "angularVelocity": 2.685223022395206e-18 + }, + { + "timestamp": 6.388169931216003, + "x": 9.574571066935462, + "y": 4.208504119827007, + "heading": -1.672275264354956e-18, + "velocityX": 2.8464397454821033, + "velocityY": 0.019360674789277475, + "angularVelocity": 2.694058180497591e-18 + }, + { + "timestamp": 6.44998042571566, + "x": 9.740161517359757, + "y": 4.209630418895655, + "heading": -1.4988253948477009e-18, + "velocityX": 2.6790021947683296, + "velocityY": 0.01822180970667645, + "angularVelocity": 2.8061557828406487e-18 + }, + { + "timestamp": 6.511790920215317, + "x": 9.895402568256603, + "y": 4.210686324189689, + "heading": -1.3682141824071403e-18, + "velocityX": 2.511564616227247, + "velocityY": 0.01708294526003873, + "angularVelocity": 2.113091300836739e-18 + }, + { + "timestamp": 6.5736014147149735, + "x": 10.040294218479321, + "y": 4.211671835735315, + "heading": -1.2370525990388503e-18, + "velocityX": 2.344127019134622, + "velocityY": 0.015944081237376676, + "angularVelocity": 2.121995457706308e-18 + }, + { + "timestamp": 6.63541190921463, + "x": 10.17483646720885, + "y": 4.2125869535512495, + "heading": -1.1181012652869172e-18, + "velocityX": 2.176689408790894, + "velocityY": 0.01480521751755444, + "angularVelocity": 1.9244521449458323e-18 + }, + { + "timestamp": 6.697222403714287, + "x": 10.299029313830903, + "y": 4.213431677651533, + "heading": -9.730760684704942e-19, + "velocityX": 2.0092517885088386, + "velocityY": 0.0136663540248621, + "angularVelocity": 2.34628768825949e-18 + }, + { + "timestamp": 6.759032898213944, + "x": 10.41287275786769, + "y": 4.214206008047086, + "heading": -8.232453614546966e-19, + "velocityX": 1.841814160496972, + "velocityY": 0.012527490708826364, + "angularVelocity": 2.4240335458871123e-18 + }, + { + "timestamp": 6.8208433927136, + "x": 10.51636679893699, + "y": 4.214909944746641, + "heading": -7.315642270842732e-19, + "velocityX": 1.6743765263012564, + "velocityY": 0.01138862753411592, + "angularVelocity": 1.4832616817746723e-18 + }, + { + "timestamp": 6.882653887213257, + "x": 10.609511436726066, + "y": 4.2155434877573486, + "heading": -6.381706774047405e-19, + "velocityX": 1.5069388870460279, + "velocityY": 0.010249764475035264, + "angularVelocity": 1.5109659573711873e-18 + }, + { + "timestamp": 6.944464381712914, + "x": 10.692306670974313, + "y": 4.216106637085162, + "heading": -5.442244989502098e-19, + "velocityX": 1.3395012435745384, + "velocityY": 0.009110901512312773, + "angularVelocity": 1.5199066466431908e-18 + }, + { + "timestamp": 7.00627487621257, + "x": 10.764752501461214, + "y": 4.216599392735122, + "heading": -4.624869062138984e-19, + "velocityX": 1.1720635965354431, + "velocityY": 0.007972038631124114, + "angularVelocity": 1.3223902486576805e-18 + }, + { + "timestamp": 7.068085370712227, + "x": 10.826848927997757, + "y": 4.217021754711548, + "heading": -3.710813756426528e-19, + "velocityX": 1.0046259464384006, + "velocityY": 0.006833175819821598, + "angularVelocity": 1.4788028118852417e-18 + }, + { + "timestamp": 7.129895865211884, + "x": 10.878595950420129, + "y": 4.217373723018184, + "heading": -3.02154183325193e-19, + "velocityX": 0.8371882936911367, + "velocityY": 0.005694313069087075, + "angularVelocity": 1.11513740490256e-18 + }, + { + "timestamp": 7.1917063597115405, + "x": 10.919993568584998, + "y": 4.217655297658306, + "heading": -2.262930091186312e-19, + "velocityX": 0.6697506386249291, + "velocityY": 0.004555450371349547, + "angularVelocity": 1.227318712671864e-18 + }, + { + "timestamp": 7.253516854211197, + "x": 10.951041782365888, + "y": 4.217866478634804, + "heading": -1.4462413004463968e-19, + "velocityX": 0.5023129815125947, + "velocityY": 0.003416587720374073, + "angularVelocity": 1.3212785667213008e-18 + }, + { + "timestamp": 7.315327348710854, + "x": 10.97174059165038, + "y": 4.218007265950247, + "heading": -5.814834591741866e-20, + "velocityX": 0.33487532258148117, + "velocityY": 0.00227772511096487, + "angularVelocity": 1.3990469909142111e-18 + }, + { + "timestamp": 7.377137843210511, + "x": 10.98208999633789, + "y": 4.218077659606934, + "heading": 0, + "velocityX": 0.16743766202303859, + "velocityY": 0.0011388625387465416, + "angularVelocity": 9.407520175646076e-19 + }, + { + "timestamp": 7.438948337710167, + "x": 10.98208999633789, + "y": 4.218077659606934, + "heading": 0, + "velocityX": 6.325612915616031e-33, + "velocityY": -2.0300544405243873e-31, + "angularVelocity": 0 + } + ] + }, + "9": { + "waypoints": [ + { + "x": 9.487251281738281, + "y": 1.0334224700927734, + "heading": 0, + "velocityMagnitude": 0, + "velocityAngle": 1.5872783360152984, + "translationConstrained": true, + "headingConstrained": true, + "velocityMagnitudeConstrained": true, + "velocityAngleConstrained": true, + "angularVelocity": 0, + "angularVelocityConstrained": true, + "controlIntervalCount": 0 + }, + { + "x": 9.443922996520996, + "y": 4.337231159210205, + "heading": 0, + "velocityMagnitude": 0, + "velocityAngle": 1.5639473253351956, + "translationConstrained": true, + "headingConstrained": true, + "velocityMagnitudeConstrained": false, + "velocityAngleConstrained": true, + "angularVelocity": 0, + "angularVelocityConstrained": false, + "controlIntervalCount": 0 + }, + { + "x": 9.443922996520996, + "y": 5.182139873504639, + "heading": 0, + "velocityMagnitude": 0, + "velocityAngle": 2.3847582198426744, + "translationConstrained": true, + "headingConstrained": true, + "velocityMagnitudeConstrained": false, + "velocityAngleConstrained": true, + "angularVelocity": 0, + "angularVelocityConstrained": false, + "controlIntervalCount": 0 + }, + { + "x": 7.158337116241455, + "y": 5.182139873504639, + "heading": 0, + "velocityMagnitude": 0, + "velocityAngle": 0, + "translationConstrained": true, + "headingConstrained": true, + "velocityMagnitudeConstrained": false, + "velocityAngleConstrained": false, + "angularVelocity": 0, + "angularVelocityConstrained": false, + "controlIntervalCount": 0 + }, + { + "x": 7.1691694259643555, + "y": 3.600644588470459, + "heading": 0, + "velocityMagnitude": 0, + "velocityAngle": 0, + "translationConstrained": true, + "headingConstrained": true, + "velocityMagnitudeConstrained": false, + "velocityAngleConstrained": false, + "angularVelocity": 0, + "angularVelocityConstrained": false, + "controlIntervalCount": 0 + }, + { + "x": 9.443922996520996, + "y": 3.6439731121063232, + "heading": 0, + "velocityMagnitude": 0, + "velocityAngle": 0, + "translationConstrained": true, + "headingConstrained": true, + "velocityMagnitudeConstrained": true, + "velocityAngleConstrained": false, + "angularVelocity": 0, + "angularVelocityConstrained": true, + "controlIntervalCount": 0 + } + ], + "trajectory": [ + { + "timestamp": 0, + "x": 9.48725128173828, + "y": 1.0334224700927734, + "heading": 2.042026558603943e-38, + "velocityX": 2.0789894429506748e-33, + "velocityY": 2.830923369396266e-32, + "angularVelocity": 0 + }, + { + "timestamp": 0.04240564041799475, + "x": 9.4871448998338, + "y": 1.0382926456122414, + "heading": 9.943795864363938e-21, + "velocityX": -0.0025086734555163667, + "velocityY": 0.11484735217915842, + "angularVelocity": 2.3449239037314736e-19 + }, + { + "timestamp": 0.0848112808359895, + "x": 9.486932136114321, + "y": 1.0480329966093236, + "heading": 2.9830043131816505e-20, + "velocityX": -0.005017344800842892, + "velocityY": 0.22969470337132947, + "angularVelocity": 4.689530796217198e-19 + }, + { + "timestamp": 0.12721692125398426, + "x": 9.486612990676212, + "y": 1.062643523038951, + "heading": 5.965665587916088e-20, + "velocityX": -0.007526013873606992, + "velocityY": 0.3445420535006988, + "angularVelocity": 7.033644604732387e-19 + }, + { + "timestamp": 0.169622561671979, + "x": 9.486187463623557, + "y": 1.0821242248524547, + "heading": 9.942134329561084e-20, + "velocityX": -0.01003468049194515, + "velocityY": 0.4593894024823678, + "angularVelocity": 9.37721821241599e-19 + }, + { + "timestamp": 0.21202820208997375, + "x": 9.485655555069107, + "y": 1.1064751019971164, + "heading": 1.4912322566451605e-19, + "velocityX": -0.012543344451253364, + "velocityY": 0.5742367502208419, + "angularVelocity": 1.1720584352015508e-18 + }, + { + "timestamp": 0.2544338425079685, + "x": 9.48501726513543, + "y": 1.1356961544156405, + "heading": 2.0876008033417065e-19, + "velocityX": -0.015052005520230899, + "velocityY": 0.6890840966081908, + "angularVelocity": 1.4063426358296795e-18 + }, + { + "timestamp": 0.2968394829259633, + "x": 9.484272593956234, + "y": 1.1697873820455318, + "heading": 2.783254890317532e-19, + "velocityX": -0.017560663436024596, + "velocityY": 0.8039314415217913, + "angularVelocity": 1.6404754810223779e-18 + }, + { + "timestamp": 0.33924512334395807, + "x": 9.483421541677973, + "y": 1.2087487848183536, + "heading": 3.5782400363277806e-19, + "velocityX": -0.02006931789821662, + "velocityY": 0.9187787848215332, + "angularVelocity": 1.874715668970879e-18 + }, + { + "timestamp": 0.38165076376195284, + "x": 9.482464108461746, + "y": 1.2525803626588379, + "heading": 4.472535009967894e-19, + "velocityX": -0.022577968561308556, + "velocityY": 1.033626126346329, + "angularVelocity": 2.1089057880414308e-18 + }, + { + "timestamp": 0.4240564041799476, + "x": 9.48140029448562, + "y": 1.30128211548381, + "heading": 5.466053971653031e-19, + "velocityX": -0.025086615025226803, + "velocityY": 1.148473465909707, + "angularVelocity": 2.3428934825764293e-18 + }, + { + "timestamp": 0.4664620445979424, + "x": 9.480230099947454, + "y": 1.354854043200871, + "heading": 6.270895036316174e-19, + "velocityX": -0.027595256823189456, + "velocityY": 1.2633208032941787, + "angularVelocity": 1.8979577939439575e-18 + }, + { + "timestamp": 0.5088676850159372, + "x": 9.478953525068398, + "y": 1.4132961457067776, + "heading": 7.022555074979213e-19, + "velocityX": -0.03010389340600295, + "velocityY": 1.3781681382439617, + "angularVelocity": 1.772547607398775e-18 + }, + { + "timestamp": 0.5512733254339319, + "x": 9.477570570097257, + "y": 1.4766084228854086, + "heading": 7.873356000130682e-19, + "velocityX": -0.032612524121449, + "velocityY": 1.4930154704554248, + "angularVelocity": 2.006339334097056e-18 + }, + { + "timestamp": 0.5936789658519267, + "x": 9.47608123531603, + "y": 1.5447908746051997, + "heading": 8.823057726560091e-19, + "velocityX": -0.03512114818679701, + "velocityY": 1.6078627995643624, + "angularVelocity": 2.2395650064451655e-18 + }, + { + "timestamp": 0.6360846062699215, + "x": 9.474485521047026, + "y": 1.617843500715841, + "heading": 9.774794670564602e-19, + "velocityX": -0.03762976465149391, + "velocityY": 1.7227101251287322, + "angularVelocity": 2.244364461927908e-18 + }, + { + "timestamp": 0.6784902466879162, + "x": 9.472783427662161, + "y": 1.6957663010439643, + "heading": 1.0626249201373726e-18, + "velocityX": -0.04013837234549543, + "velocityY": 1.8375574466047766, + "angularVelocity": 2.0078809576964904e-18 + }, + { + "timestamp": 0.720895887105911, + "x": 9.47097495559539, + "y": 1.7785592753873969, + "heading": 1.136044401622763e-18, + "velocityX": -0.04264696980605482, + "velocityY": 1.9524047633132235, + "angularVelocity": 1.7313619299700687e-18 + }, + { + "timestamp": 0.7633015275239058, + "x": 9.469060105359615, + "y": 1.866222423507331, + "heading": 1.2193239881571744e-18, + "velocityX": -0.0451555551712103, + "velocityY": 2.0672520743901783, + "angularVelocity": 1.9638807726635784e-18 + }, + { + "timestamp": 0.8057071679419006, + "x": 9.467038877570412, + "y": 1.9587557451173774, + "heading": 1.286588431468213e-18, + "velocityX": -0.047664126019975314, + "velocityY": 2.1820993787135547, + "angularVelocity": 1.5862156816463694e-18 + }, + { + "timestamp": 0.8481128083598953, + "x": 9.464911272980254, + "y": 2.0561592398677684, + "heading": 1.3636923650301692e-18, + "velocityX": -0.05017267912366991, + "velocityY": 2.296946674788811, + "angularVelocity": 1.818248429873892e-18 + }, + { + "timestamp": 0.8905184487778901, + "x": 9.46267729252993, + "y": 2.1584329073217075, + "heading": 1.4505957234023422e-18, + "velocityX": -0.052681210041686424, + "velocityY": 2.4117939605636165, + "angularVelocity": 2.0493362812866533e-18 + }, + { + "timestamp": 0.9329240891958849, + "x": 9.460336937429323, + "y": 2.2655767469182706, + "heading": 1.5472803584359657e-18, + "velocityX": -0.05518971242820439, + "velocityY": 2.5266412331108703, + "angularVelocity": 2.2799961860839735e-18 + }, + { + "timestamp": 0.9753297296138796, + "x": 9.457890209292124, + "y": 2.377590757910745, + "heading": 1.6536466656743909e-18, + "velocityX": -0.05769817676044675, + "velocityY": 2.6414884880493084, + "angularVelocity": 2.5083071179121423e-18 + }, + { + "timestamp": 1.0177353700318743, + "x": 9.4553371103785, + "y": 2.494474939256018, + "heading": 1.743873672813524e-18, + "velocityX": -0.060206587793311304, + "velocityY": 2.7563357183893307, + "angularVelocity": 2.127714181383059e-18 + }, + { + "timestamp": 1.060141010449869, + "x": 9.452677644080838, + "y": 2.616229289394468, + "heading": 1.8274868542003677e-18, + "velocityX": -0.06271491882651421, + "velocityY": 2.8711829119507097, + "angularVelocity": 1.9717486677423863e-18 + }, + { + "timestamp": 1.1025466508678636, + "x": 9.449911816058748, + "y": 2.7428538057403618, + "heading": 1.8988600974648397e-18, + "velocityX": -0.06522311639447756, + "velocityY": 2.986030044535951, + "angularVelocity": 1.6831095364933795e-18 + }, + { + "timestamp": 1.1449522912858583, + "x": 9.447039637651764, + "y": 2.8743484831746504, + "heading": 1.9472251907105894e-18, + "velocityX": -0.0677310465524073, + "velocityY": 3.1008770564049897, + "angularVelocity": 1.1405370707587538e-18 + }, + { + "timestamp": 1.187357931703853, + "x": 9.444061143025511, + "y": 3.0107133067865663, + "heading": 1.9879182660274187e-18, + "velocityX": -0.0702381710133901, + "velocityY": 3.215723716651743, + "angularVelocity": 9.59617525698653e-19 + }, + { + "timestamp": 1.2297635721218476, + "x": 9.443464378511562, + "y": 3.148042361806912, + "heading": 1.8408102143003954e-18, + "velocityX": -0.014072762753181542, + "velocityY": 3.2384619986071734, + "angularVelocity": -3.4690697324267305e-18 + }, + { + "timestamp": 1.2721692125398423, + "x": 9.442974063870757, + "y": 3.2805012734237167, + "heading": 1.6823653317280151e-18, + "velocityX": -0.011562486454314621, + "velocityY": 3.123615403778499, + "angularVelocity": -3.736412209123864e-18 + }, + { + "timestamp": 1.314574852957837, + "x": 9.442590166523006, + "y": 3.4080900253308806, + "heading": 1.5132869376537693e-18, + "velocityX": -0.00905297846243119, + "velocityY": 3.0087684244231836, + "angularVelocity": -3.987169181689804e-18 + }, + { + "timestamp": 1.3569804933758316, + "x": 9.44231267552572, + "y": 3.5308086119494133, + "heading": 1.355336087070504e-18, + "velocityX": -0.006543728519569948, + "velocityY": 2.89392131350538, + "angularVelocity": -3.724762272283759e-18 + }, + { + "timestamp": 1.3993861337938263, + "x": 9.442141585393275, + "y": 3.648657030464007, + "heading": 1.2032167278614567e-18, + "velocityX": -0.004034607939290763, + "velocityY": 2.779074136197618, + "angularVelocity": -3.587245465070565e-18 + }, + { + "timestamp": 1.441791774211821, + "x": 9.442076892829686, + "y": 3.76163527917735, + "heading": 1.0408717529122375e-18, + "velocityX": -0.001525565085163529, + "velocityY": 2.6642269188642267, + "angularVelocity": -3.828383542319785e-18 + }, + { + "timestamp": 1.4841974146298156, + "x": 9.442118595635634, + "y": 3.8697433569545434, + "heading": 8.899938356909357e-19, + "velocityX": 0.000983425904485768, + "velocityY": 2.549379674767898, + "angularVelocity": -3.55797027813502e-18 + }, + { + "timestamp": 1.5266030550478102, + "x": 9.442266692239146, + "y": 3.9729812629833083, + "heading": 7.285255211391333e-19, + "velocityX": 0.0034923798236392647, + "velocityY": 2.434532411516591, + "angularVelocity": -3.8077105384507665e-18 + }, + { + "timestamp": 1.569008695465805, + "x": 9.442521181460629, + "y": 4.071348996653543, + "heading": 5.568860923837275e-19, + "velocityX": 0.006001305925577423, + "velocityY": 2.319685133878004, + "angularVelocity": -4.047563375041425e-18 + }, + { + "timestamp": 1.6114143358837996, + "x": 9.442882062382234, + "y": 4.164846557490197, + "heading": 3.89214419951117e-19, + "velocityX": 0.008510210382726976, + "velocityY": 2.204837845036861, + "angularVelocity": -3.95399672781954e-18 + }, + { + "timestamp": 1.6538199763017942, + "x": 9.443349334269415, + "y": 4.253473945112896, + "heading": 2.1145958274049998e-19, + "velocityX": 0.011019097517791274, + "velocityY": 2.0899905472258355, + "angularVelocity": -4.1917755606155634e-18 + }, + { + "timestamp": 1.6962256167197889, + "x": 9.443922996520996, + "y": 4.337231159210205, + "heading": 0, + "velocityX": 0.013527970475770293, + "velocityY": 1.9751432420706365, + "angularVelocity": -4.9865929033974614e-18 + }, + { + "timestamp": 1.7140312213529185, + "x": 9.444851439716329, + "y": 4.371885132633888, + "heading": -6.384086665630808e-20, + "velocityX": 0.05214331186518852, + "velocityY": 1.946239632840007, + "angularVelocity": -3.585437355003227e-18 + }, + { + "timestamp": 1.7318368259860482, + "x": 9.446439474657646, + "y": 4.405989059480052, + "heading": -1.2895806679414802e-19, + "velocityX": 0.08918736398104961, + "velocityY": 1.9153478665197825, + "angularVelocity": -3.657118908476392e-18 + }, + { + "timestamp": 1.7496424306191778, + "x": 9.448654037918123, + "y": 4.439505555725891, + "heading": -1.9535739456212597e-19, + "velocityX": 0.12437450488808559, + "velocityY": 1.8823565352828266, + "angularVelocity": -3.7291258719060736e-18 + }, + { + "timestamp": 1.7674480352523074, + "x": 9.451456170424079, + "y": 4.472395653726791, + "heading": -2.630514551197965e-19, + "velocityX": 0.1573736227268157, + "velocityY": 1.8471766996162327, + "angularVelocity": -3.8018407655954725e-18 + }, + { + "timestamp": 1.785253639885437, + "x": 9.454800219765596, + "y": 4.5046194646593865, + "heading": -3.3204054645526424e-19, + "velocityX": 0.18780880573410802, + "velocityY": 1.809756624194348, + "angularVelocity": -3.874572393877372e-18 + }, + { + "timestamp": 1.8030592445185667, + "x": 9.45863315790976, + "y": 4.5361371594617745, + "heading": -4.0232225320104645e-19, + "velocityX": 0.21526582349437903, + "velocityY": 1.770099665345931, + "angularVelocity": -3.947168368442765e-18 + }, + { + "timestamp": 1.8208648491516963, + "x": 9.462894162503119, + "y": 4.566910293113452, + "heading": -4.738817957811649e-19, + "velocityX": 0.2393069306635331, + "velocityY": 1.7282835537310302, + "angularVelocity": -4.018934288722903e-18 + }, + { + "timestamp": 1.838670453784826, + "x": 9.467514642286881, + "y": 4.596903433349225, + "heading": -5.466979372881272e-19, + "velocityX": 0.2594958092671896, + "velocityY": 1.6844774919894794, + "angularVelocity": -4.0895074983092414e-18 + }, + { + "timestamp": 1.8564760584179556, + "x": 9.472418880413178, + "y": 4.626085960665692, + "heading": -6.207409559673841e-19, + "velocityX": 0.2754322713184185, + "velocityY": 1.6389517748903728, + "angularVelocity": -4.1584114719162204e-18 + }, + { + "timestamp": 1.8742816630510852, + "x": 9.477525397270549, + "y": 4.654433805173152, + "heading": -6.959741396849906e-19, + "velocityX": 0.2867926679598693, + "velocityY": 1.5920742424390129, + "angularVelocity": -4.225253612980791e-18 + }, + { + "timestamp": 1.8920872676842149, + "x": 9.482748991935516, + "y": 4.681930823696735, + "heading": -7.723538759753341e-19, + "velocityX": 0.2933680025249804, + "velocityY": 1.5442900755202997, + "angularVelocity": -4.2896463843917775e-18 + }, + { + "timestamp": 1.9098928723173445, + "x": 9.488003245817486, + "y": 4.708569546182436, + "heading": -8.498326848116759e-19, + "velocityX": 0.2950898882814487, + "velocityY": 1.4960863747436854, + "angularVelocity": -4.351372577780205e-18 + }, + { + "timestamp": 1.9276984769504741, + "x": 9.493203137170577, + "y": 4.734351147430229, + "heading": -9.283590304652731e-19, + "velocityX": 0.2920367749499054, + "velocityY": 1.4479486531909405, + "angularVelocity": -4.410204415310111e-18 + }, + { + "timestamp": 1.9455040815836038, + "x": 9.498267388376567, + "y": 4.759284683886495, + "heading": -1.0078785533899687e-18, + "velocityX": 0.2844189405716801, + "velocityY": 1.4003195606102483, + "angularVelocity": -4.465981307522584e-18 + }, + { + "timestamp": 1.9633096862167334, + "x": 9.503120261158855, + "y": 4.783385795359616, + "heading": -1.0883359474256107e-18, + "velocityX": 0.27254748615844004, + "velocityY": 1.3535688323818684, + "angularVelocity": -4.518654087152181e-18 + }, + { + "timestamp": 1.981115290849863, + "x": 9.507692676393317, + "y": 4.806675145899408, + "heading": -1.1311853620839733e-18, + "velocityX": 0.25679640364232453, + "velocityY": 1.3079786404139804, + "angularVelocity": -2.406511942248596e-18 + }, + { + "timestamp": 1.9989208954829927, + "x": 9.511922690166164, + "y": 4.829176858482163, + "heading": -1.1748571361283305e-18, + "velocityX": 0.23756642136020728, + "velocityY": 1.263743245252432, + "angularVelocity": -2.4526977117660686e-18 + }, + { + "timestamp": 2.016726500116122, + "x": 9.515755456205326, + "y": 4.850917120727883, + "heading": -1.2662243537310274e-18, + "velocityX": 0.21525615771746257, + "velocityY": 1.2209786016065654, + "angularVelocity": -5.131373562747908e-18 + }, + { + "timestamp": 2.0345321047492515, + "x": 9.519142838088333, + "y": 4.871923049672868, + "heading": -1.3583021162082306e-18, + "velocityX": 0.1902424519021115, + "velocityY": 1.1797369074398463, + "angularVelocity": -5.171279224311694e-18 + }, + { + "timestamp": 2.052337709382381, + "x": 9.522042821424499, + "y": 4.89222183283118, + "heading": -1.4510355551800577e-18, + "velocityX": 0.16286913002499506, + "velocityY": 1.1400221209305184, + "angularVelocity": -5.208102989905589e-18 + }, + { + "timestamp": 2.0701433140155103, + "x": 9.524418841431352, + "y": 4.911840119848378, + "heading": -1.4974409061102444e-18, + "velocityX": 0.133442253481921, + "velocityY": 1.1018040342587163, + "angularVelocity": -2.6062213787679026e-18 + }, + { + "timestamp": 2.0879489186486397, + "x": 9.526239103410534, + "y": 4.930803620151, + "heading": -1.4974658953514371e-18, + "velocityX": 0.10222972017435777, + "velocityY": 1.0650298427573541, + "angularVelocity": -1.4032278309499374e-21 + }, + { + "timestamp": 2.105754523281769, + "x": 9.527475941909024, + "y": 4.949136858822179, + "heading": -1.4979953825818616e-18, + "velocityX": 0.06946343715785236, + "velocityY": 1.029633031223677, + "angularVelocity": -2.973686380366138e-20 + }, + { + "timestamp": 2.1235601279148986, + "x": 9.52810524148387, + "y": 4.966863047915789, + "heading": -1.4520529247658228e-18, + "velocityX": 0.03534278042273317, + "velocityY": 0.9955398571878791, + "angularVelocity": 2.5802244308587756e-18 + }, + { + "timestamp": 2.141365732548028, + "x": 9.528105927185328, + "y": 4.9840040384280595, + "heading": -1.4065278576583552e-18, + "velocityX": 0.00003851042804685489, + "velocityY": 0.9626738808059755, + "angularVelocity": 2.5567829210090895e-18 + }, + { + "timestamp": 2.1591713371811574, + "x": 9.527459524177115, + "y": 5.000580326339002, + "heading": -1.31444940198482e-18, + "velocityX": -0.036303345016051765, + "velocityY": 0.9309590015326615, + "angularVelocity": 5.1713182538767925e-18 + }, + { + "timestamp": 2.176976941814287, + "x": 9.526149781356017, + "y": 5.016611093250944, + "heading": -1.269642784580628e-18, + "velocityX": -0.07355789640877802, + "velocityY": 0.9003214011680397, + "angularVelocity": 2.516432963598342e-18 + }, + { + "timestamp": 2.194782546447416, + "x": 9.524162351863495, + "y": 5.032114267818632, + "heading": -1.2720728525708661e-18, + "velocityX": -0.111618197386393, + "velocityY": 0.8706907115551589, + "angularVelocity": -1.3647769751105282e-19 + }, + { + "timestamp": 2.2125881510805456, + "x": 9.521484522913049, + "y": 5.047106598446598, + "heading": -1.2278446861838926e-18, + "velocityX": -0.1503924750450378, + "velocityY": 0.8420006473732871, + "angularVelocity": 2.4839463646673614e-18 + }, + { + "timestamp": 2.230393755713675, + "x": 9.518104987688332, + "y": 5.061603730851322, + "heading": -1.183859233402495e-18, + "velocityX": -0.18980176715983477, + "velocityY": 0.8141892793548637, + "angularVelocity": 2.4703150854190343e-18 + }, + { + "timestamp": 2.2481993603468045, + "x": 9.514013652772206, + "y": 5.07562028630483, + "heading": -1.1400886719074168e-18, + "velocityX": -0.22977792669371902, + "velocityY": 0.7871990725566343, + "angularVelocity": 2.458246366413904e-18 + }, + { + "timestamp": 2.266004964979934, + "x": 9.50920147540166, + "y": 5.089169937922843, + "heading": -1.0848232169429015e-18, + "velocityX": -0.2702619467128905, + "velocityY": 0.760976776537162, + "angularVelocity": 3.1038235596053945e-18 + }, + { + "timestamp": 2.2838105696130633, + "x": 9.503660325676538, + "y": 5.102265483420015, + "heading": -1.0251358554564357e-18, + "velocityX": -0.3112025589299214, + "velocityY": 0.7354732269416935, + "angularVelocity": 3.352167585142378e-18 + }, + { + "timestamp": 2.3016161742461927, + "x": 9.497382869616938, + "y": 5.114918913468731, + "heading": -9.655885210540119e-19, + "velocityX": -0.35255506279877724, + "velocityY": 0.7106430985878675, + "angularVelocity": 3.3443033960048003e-18 + }, + { + "timestamp": 2.319421778879322, + "x": 9.490362469639482, + "y": 5.127141475270189, + "heading": -8.59228497341353e-19, + "velocityX": -0.3942803472338164, + "velocityY": 0.6864446365791875, + "angularVelocity": 5.9734019707136975e-18 + }, + { + "timestamp": 2.3372273835124515, + "x": 9.48259309960139, + "y": 5.138943731252082, + "heading": -7.529669309175775e-19, + "velocityX": -0.43634407245233126, + "velocityY": 0.6628393826029343, + "angularVelocity": 5.967872426907207e-18 + }, + { + "timestamp": 2.355032988145581, + "x": 9.47406927204869, + "y": 5.150335612998557, + "heading": -5.998533026801316e-19, + "velocityX": -0.47871598456387426, + "velocityY": 0.6397919071661478, + "angularVelocity": 8.599181871609305e-18 + }, + { + "timestamp": 2.3728385927787103, + "x": 9.464785975711115, + "y": 5.161326470632612, + "heading": -3.998698353554635e-19, + "velocityX": -0.5213693400956316, + "velocityY": 0.6172695541946409, + "angularVelocity": 1.1231490104233921e-17 + }, + { + "timestamp": 2.3906441974118398, + "x": 9.454738621622305, + "y": 5.171925117933987, + "heading": -1.9993072194663066e-19, + "velocityX": -0.5642804215768302, + "velocityY": 0.5952422015289273, + "angularVelocity": 1.1228999117764367e-17 + }, + { + "timestamp": 2.408449802044969, + "x": 9.443922996520996, + "y": 5.182139873504639, + "heading": 0, + "velocityX": -0.6074281286234412, + "velocityY": 0.5736820389490421, + "angularVelocity": 1.1228527488456345e-17 + }, + { + "timestamp": 2.441083473597997, + "x": 9.421235266185908, + "y": 5.201198581045461, + "heading": 3.6632091737902714e-19, + "velocityX": -0.695224571903227, + "velocityY": 0.584019714418288, + "angularVelocity": 1.1225243708183917e-17 + }, + { + "timestamp": 2.473717145151025, + "x": 9.395678476358919, + "y": 5.220559316480926, + "heading": 3.950269546669323e-19, + "velocityX": -0.7831417248119713, + "velocityY": 0.5932748144505112, + "angularVelocity": 8.796442845638519e-19 + }, + { + "timestamp": 2.506350816704053, + "x": 9.367248792322409, + "y": 5.2401831140518045, + "heading": 4.225934108245564e-19, + "velocityX": -0.8711763857251311, + "velocityY": 0.6013358790778222, + "angularVelocity": 8.447238066106848e-19 + }, + { + "timestamp": 2.538984488257081, + "x": 9.33594258785004, + "y": 5.260026803817625, + "heading": 3.959837725893085e-19, + "velocityX": -0.9593221658033272, + "velocityY": 0.6080740787493973, + "angularVelocity": -8.154050996492256e-19 + }, + { + "timestamp": 2.571618159810109, + "x": 9.301756592559922, + "y": 5.28004232712052, + "heading": 3.114803772539543e-19, + "velocityX": -1.0475681608354517, + "velocityY": 0.6133396075390749, + "angularVelocity": -2.5894552382449347e-18 + }, + { + "timestamp": 2.604251831363137, + "x": 9.264688101016896, + "y": 5.300175904395579, + "heading": 1.947640695222268e-19, + "velocityX": -1.135897059048004, + "velocityY": 0.6169571585698372, + "angularVelocity": -3.576562153024813e-18 + }, + { + "timestamp": 2.636885502916165, + "x": 9.224735270640254, + "y": 5.320367016783676, + "heading": 7.40663093278534e-20, + "velocityX": -1.2242824198226732, + "velocityY": 0.6187202183268213, + "angularVelocity": -3.6985649278098886e-18 + }, + { + "timestamp": 2.669519174469193, + "x": 9.181897548648003, + "y": 5.3405471520145635, + "heading": -4.738766542721829e-20, + "velocityX": -1.312684719604387, + "velocityY": 0.6183838431448163, + "angularVelocity": -3.721737827523668e-18 + }, + { + "timestamp": 2.702152846022221, + "x": 9.136176288552582, + "y": 5.360638251356262, + "heading": -1.3919438561122211e-19, + "velocityX": -1.4010455434388964, + "velocityY": 0.6156554989239036, + "angularVelocity": -2.8132510835047748e-18 + }, + { + "timestamp": 2.734786517575249, + "x": 9.087575648237515, + "y": 5.38055077835316, + "heading": -3.1451627198444227e-19, + "velocityX": -1.4892789564328437, + "velocityY": 0.6101834715269836, + "angularVelocity": -5.37242301252158e-18 + }, + { + "timestamp": 2.767420189128277, + "x": 9.036103910894113, + "y": 5.400181313477486, + "heading": -4.984458978394373e-19, + "velocityX": -1.577258545970237, + "velocityY": 0.6015423392500471, + "angularVelocity": -5.636191538862516e-18 + }, + { + "timestamp": 2.800053860681305, + "x": 8.981775447032602, + "y": 5.4194095675394145, + "heading": -6.925837136135975e-19, + "velocityX": -1.6647977771434062, + "velocityY": 0.5892151617290188, + "angularVelocity": -5.949003213880137e-18 + }, + { + "timestamp": 2.832687532234333, + "x": 8.924613654513877, + "y": 5.438094715812673, + "heading": -8.987704525662525e-19, + "velocityX": -1.7516200230745813, + "velocityY": 0.5725726644915266, + "angularVelocity": -6.318220846486254e-18 + }, + { + "timestamp": 2.865321203787361, + "x": 8.864655390388519, + "y": 5.456071019769574, + "heading": -1.0294610256530182e-18, + "velocityX": -1.8373128511735881, + "velocityY": 0.5508514090329182, + "angularVelocity": -4.004776778391296e-18 + }, + { + "timestamp": 2.897954875340389, + "x": 8.801957649624493, + "y": 5.4731429013815855, + "heading": -1.1767280866403955e-18, + "velocityX": -1.9212591712870637, + "velocityY": 0.5231370176742964, + "angularVelocity": -4.512733482684496e-18 + }, + { + "timestamp": 2.930588546893417, + "x": 8.736607504769767, + "y": 5.489080118872384, + "heading": -1.1856206374138729e-18, + "velocityX": -2.0025373102298354, + "velocityY": 0.48836728239122423, + "angularVelocity": -2.7249636003536904e-19 + }, + { + "timestamp": 2.963222218446445, + "x": 8.668736377664992, + "y": 5.503614711495437, + "heading": -1.2166901874810268e-18, + "velocityX": -2.079788263925095, + "velocityY": 0.445386373379314, + "angularVelocity": -9.520702152066405e-19 + }, + { + "timestamp": 2.995855889999473, + "x": 8.598538931192524, + "y": 5.51644315523542, + "heading": -1.272875294996435e-18, + "velocityX": -2.1510741247243037, + "velocityY": 0.3931045184156154, + "angularVelocity": -1.721691325591454e-18 + }, + { + "timestamp": 3.028489561552501, + "x": 8.526293982060697, + "y": 5.5272392514284645, + "heading": -1.3568263007892658e-18, + "velocityX": -2.2138161504271343, + "velocityY": 0.3308268937960027, + "angularVelocity": -2.5725269220569475e-18 + }, + { + "timestamp": 3.061123233105529, + "x": 8.452378864421089, + "y": 5.5356830638050605, + "heading": -1.1202008395033444e-18, + "velocityX": -2.264995451691677, + "velocityY": 0.2587453992995388, + "angularVelocity": 7.250961237685142e-18 + }, + { + "timestamp": 3.093756904658557, + "x": 8.377262861304947, + "y": 5.541503791129315, + "heading": -7.567982278279353e-19, + "velocityX": -2.3017944209582493, + "velocityY": 0.1783656894014662, + "angularVelocity": 1.1135818174546654e-17 + }, + { + "timestamp": 3.126390576211585, + "x": 8.301470281296071, + "y": 5.544520067523571, + "heading": -4.2451642109200585e-19, + "velocityX": -2.322526899424121, + "velocityY": 0.09242834933104346, + "angularVelocity": 1.0182177094305531e-17 + }, + { + "timestamp": 3.159024247764613, + "x": 8.225524229056585, + "y": 5.5446555195936496, + "heading": -6.419282428655621e-20, + "velocityX": -2.327229779097387, + "velocityY": 0.004150684358640343, + "angularVelocity": 1.104146740013862e-17 + }, + { + "timestamp": 3.191657919317641, + "x": 8.149897657636762, + "y": 5.541923806442718, + "heading": 2.6600608408756135e-19, + "velocityX": -2.3174398656595123, + "velocityY": -0.08370842203557924, + "angularVelocity": 1.0118350537396719e-17 + }, + { + "timestamp": 3.224291590870669, + "x": 8.074988972359481, + "y": 5.536397930435266, + "heading": 5.669464384757661e-19, + "velocityX": -2.295441539747066, + "velocityY": -0.16933050265189162, + "angularVelocity": 9.221774814473882e-18 + }, + { + "timestamp": 3.256925262423697, + "x": 8.001120065278322, + "y": 5.528181037877018, + "heading": 8.396159802906952e-19, + "velocityX": -2.263579412482889, + "velocityY": -0.251791850785044, + "angularVelocity": 8.355466655505314e-18 + }, + { + "timestamp": 3.289558933976725, + "x": 7.928546140625048, + "y": 5.517386213720495, + "heading": 1.0850578237345321e-18, + "velocityX": -2.2238970118746137, + "velocityY": -0.33078791453121625, + "angularVelocity": 7.521122855720812e-18 + }, + { + "timestamp": 3.322192605529753, + "x": 7.857468753455215, + "y": 5.504124997826886, + "heading": 1.2498038775969776e-18, + "velocityX": -2.1780383201546814, + "velocityY": -0.40636604042728, + "angularVelocity": 5.048345584833791e-18 + }, + { + "timestamp": 3.354826277082781, + "x": 7.788047870036826, + "y": 5.488501852729208, + "heading": 1.3893738984859056e-18, + "velocityX": -2.1272777506994864, + "velocityY": -0.47874310042900636, + "angularVelocity": 4.276871388382114e-18 + }, + { + "timestamp": 3.387459948635809, + "x": 7.720411653283996, + "y": 5.470612062415975, + "heading": 1.0686978502777718e-18, + "velocityX": -2.0725898599220134, + "velocityY": -0.5482003544763124, + "angularVelocity": -9.826540893112917e-18 + }, + { + "timestamp": 3.420093620188837, + "x": 7.654663966143918, + "y": 5.450541406963203, + "heading": 7.246751826948704e-19, + "velocityX": -2.014719276476262, + "velocityY": -0.6150290328245204, + "angularVelocity": -1.0541955993846661e-17 + }, + { + "timestamp": 3.4527272917418648, + "x": 7.590889993639064, + "y": 5.4283666677544415, + "heading": 6.49705937648609e-19, + "velocityX": -1.954238351673864, + "velocityY": -0.6795048841724649, + "angularVelocity": -2.2972967003537705e-18 + }, + { + "timestamp": 3.4853609632948928, + "x": 7.529160428726087, + "y": 5.40415646387206, + "heading": 5.529406335391227e-19, + "velocityX": -1.8915911687310976, + "velocityY": -0.7418780275165117, + "angularVelocity": -2.9651976681071948e-18 + }, + { + "timestamp": 3.5179946348479207, + "x": 7.4695345966300986, + "y": 5.377972170861107, + "heading": 4.350554097070228e-19, + "velocityX": -1.827126071275839, + "velocityY": -0.8023704279919941, + "angularVelocity": -3.612379562157046e-18 + }, + { + "timestamp": 3.5506283064009487, + "x": 7.412062803528599, + "y": 5.349868806100156, + "heading": 2.9666664077545544e-19, + "velocityX": -1.76111943175355, + "velocityY": -0.8611769201416861, + "angularVelocity": -4.24067376305906e-18 + }, + { + "timestamp": 3.5832619779539767, + "x": 7.3567881189273505, + "y": 5.31989583302525, + "heading": 1.3833796557360445e-19, + "velocityX": -1.6937930049161405, + "velocityY": -0.9184676945161352, + "angularVelocity": -4.851696264215519e-18 + }, + { + "timestamp": 3.6158956495070047, + "x": 7.303747742547076, + "y": 5.288097869990755, + "heading": -3.941532186257001e-20, + "velocityX": -1.6253266597381706, + "velocityY": -0.9743912199038477, + "angularVelocity": -5.446928506404398e-18 + }, + { + "timestamp": 3.6485293210600327, + "x": 7.252974063954799, + "y": 5.254515305256301, + "heading": -7.88746893068946e-21, + "velocityX": -1.5558677947031856, + "velocityY": -1.029077119927617, + "angularVelocity": 9.661145052202089e-19 + }, + { + "timestamp": 3.6811629926130607, + "x": 7.2044954928258065, + "y": 5.219184826255724, + "heading": 5.118481093167075e-21, + "velocityX": -1.4855383664145283, + "velocityY": -1.082638799718508, + "angularVelocity": 3.9854416876380984e-19 + }, + { + "timestamp": 3.7137966641660887, + "x": 7.158337116241455, + "y": 5.182139873504639, + "heading": 0, + "velocityX": -1.4144401897698629, + "velocityY": -1.1351757552284538, + "angularVelocity": -1.5684683960049997e-19 + }, + { + "timestamp": 3.7414907274933724, + "x": 7.120852532109941, + "y": 5.149489538414079, + "heading": 1.0745519784573005e-20, + "velocityX": -1.353524171896549, + "velocityY": -1.178965134321498, + "angularVelocity": 3.8800874694948773e-19 + }, + { + "timestamp": 3.769184790820656, + "x": 7.08508589038847, + "y": 5.115670726143776, + "heading": 9.195270075585857e-21, + "velocityX": -1.2914912954009872, + "velocityY": -1.221157468683352, + "angularVelocity": -5.597697879888322e-20 + }, + { + "timestamp": 3.79687885414794, + "x": 7.0510680175463785, + "y": 5.080730097325854, + "heading": -4.120447004048063e-21, + "velocityX": -1.228345311414701, + "velocityY": -1.2616649425907351, + "angularVelocity": -4.808141205686055e-19 + }, + { + "timestamp": 3.8245729174752237, + "x": 7.018829493833099, + "y": 5.044716816861446, + "heading": -2.8635234930444935e-20, + "velocityX": -1.1640951106484203, + "velocityY": -1.3003971298399937, + "angularVelocity": -8.851987690468531e-19 + }, + { + "timestamp": 3.8522669808025074, + "x": 6.988400495078303, + "y": 5.007682613177794, + "heading": -6.374294844247492e-20, + "velocityX": -1.098755296223239, + "velocityY": -1.3372614645235503, + "angularVelocity": -1.2676968124680518e-18 + }, + { + "timestamp": 3.879961044129791, + "x": 6.959810618911943, + "y": 4.969681821257425, + "heading": -1.0372073464335989e-19, + "velocityX": -1.0323467462498963, + "velocityY": -1.3721638270007799, + "angularVelocity": -1.4435495083754531e-18 + }, + { + "timestamp": 3.907655107457075, + "x": 6.933088696208582, + "y": 4.930771406065292, + "heading": -1.5295745974019348e-19, + "velocityX": -0.9648971473620426, + "velocityY": -1.4050092517047952, + "angularVelocity": -1.777878869589048e-18 + }, + { + "timestamp": 3.9353491707843586, + "x": 6.908262589169743, + "y": 4.891010962916479, + "heading": -2.1072283099568796e-19, + "velocityX": -0.8964414772020872, + "velocityY": -1.4357027597911687, + "angularVelocity": -2.0858393494447477e-18 + }, + { + "timestamp": 3.9630432341116424, + "x": 6.885358978142829, + "y": 4.850462691377148, + "heading": -2.7624488082850763e-19, + "velocityX": -0.827022411129839, + "velocityY": -1.4641503148215067, + "angularVelocity": -2.3659238652212594e-18 + }, + { + "timestamp": 3.990737297438926, + "x": 6.864403140012061, + "y": 4.809191339506532, + "heading": -2.4182243014725925e-19, + "velocityX": -0.7566906265474782, + "velocityY": -1.4902598937135898, + "angularVelocity": 1.2429539162791189e-18 + }, + { + "timestamp": 4.01843136076621, + "x": 6.845418721750748, + "y": 4.767264115655309, + "heading": -2.13489838207875e-19, + "velocityX": -0.685504977617706, + "velocityY": -1.5139426582417082, + "angularVelocity": 1.0230562964475242e-18 + }, + { + "timestamp": 4.0461254240934945, + "x": 6.828427513449241, + "y": 4.7247505656541895, + "heading": -1.9035484353323274e-19, + "velocityX": -0.6135325141964989, + "velocityY": -1.5351142047557378, + "angularVelocity": 8.353771837793482e-19 + }, + { + "timestamp": 4.073819487420779, + "x": 6.813449225773627, + "y": 4.681722414060745, + "heading": -1.7149141991215955e-19, + "velocityX": -0.5408483218444247, + "velocityY": -1.5536958619955332, + "angularVelocity": 6.811357830936083e-19 + }, + { + "timestamp": 4.101513550748063, + "x": 6.8005012773057025, + "y": 4.638253369169532, + "heading": -1.5593935716162944e-19, + "velocityX": -0.4675351650246373, + "velocityY": -1.569615999555684, + "angularVelocity": 5.615664370799311e-19 + }, + { + "timestamp": 4.129207614075347, + "x": 6.789598597504067, + "y": 4.594418892696229, + "heading": -1.427091986664009e-19, + "velocityX": -0.3936829230434275, + "velocityY": -1.5828113034650333, + "angularVelocity": 4.777252028217134e-19 + }, + { + "timestamp": 4.156901677402631, + "x": 6.7807534510551255, + "y": 4.550295936364767, + "heading": -1.3078707268298111e-19, + "velocityX": -0.3193878176853724, + "velocityY": -1.5932279712811777, + "angularVelocity": 4.304936779834702e-19 + }, + { + "timestamp": 4.1845957407299155, + "x": 6.773975289112579, + "y": 4.50596264898097, + "heading": -1.1914143965999851e-19, + "velocityX": -0.2447514423016853, + "velocityY": -1.6008227777871797, + "angularVelocity": 4.205098273160101e-19 + }, + { + "timestamp": 4.2122898040572, + "x": 6.769270632337623, + "y": 4.461498058876275, + "heading": -8.246208709796346e-20, + "velocityX": -0.16987961352428851, + "velocityY": -1.6055639643493063, + "angularVelocity": 1.3244478396697712e-18 + }, + { + "timestamp": 4.239983867384484, + "x": 6.766642989760458, + "y": 4.416981737753159, + "heading": -6.835343094343368e-20, + "velocityX": -0.09488107780040866, + "velocityY": -1.6074319104794648, + "angularVelocity": 5.094467867757219e-19 + }, + { + "timestamp": 4.267677930711768, + "x": 6.766092816327569, + "y": 4.372493452867139, + "heading": -4.5283135279092575e-20, + "velocityX": -0.019866114494882854, + "velocityY": -1.6064195549877718, + "angularVelocity": 8.330410070957423e-19 + }, + { + "timestamp": 4.295371994039052, + "x": 6.767617510647199, + "y": 4.328112815061299, + "heading": -1.83116755901566e-20, + "velocityX": 0.05505491561896332, + "velocityY": -1.6025325457429587, + "angularVelocity": 9.739074640031068e-19 + }, + { + "timestamp": 4.323066057366336, + "x": 6.771211452987862, + "y": 4.28391893037677, + "heading": -3.461364002947858e-20, + "velocityX": 0.1297730238496046, + "velocityY": -1.5957891105488256, + "angularVelocity": -5.886446259595525e-19 + }, + { + "timestamp": 4.350760120693621, + "x": 6.776866082124187, + "y": 4.239990062777867, + "heading": -4.498980952085583e-20, + "velocityX": 0.20418199631809572, + "velocityY": -1.586219655807003, + "angularVelocity": -3.74671184772026e-19 + }, + { + "timestamp": 4.378454184020905, + "x": 6.784570008263401, + "y": 4.196403314970527, + "heading": -8.839742873815279e-20, + "velocityX": 0.2781796967880941, + "velocityY": -1.573866113189625, + "angularVelocity": -1.567397663024212e-18 + }, + { + "timestamp": 4.406148247348189, + "x": 6.794309158115849, + "y": 4.153234333407163, + "heading": -1.8339666089580011e-19, + "velocityX": 0.35166922734851225, + "velocityY": -1.558781066295692, + "angularVelocity": -3.4303098461228627e-18 + }, + { + "timestamp": 4.433842310675473, + "x": 6.806066947263768, + "y": 4.110557042436363, + "heading": -2.696427100318041e-19, + "velocityX": 0.42455991412196786, + "velocityY": -1.5410266982654612, + "angularVelocity": -3.114242761138099e-18 + }, + { + "timestamp": 4.461536374002757, + "x": 6.819824474376613, + "y": 4.068443411264147, + "heading": -3.4626467085586136e-19, + "velocityX": 0.49676809611723804, + "velocityY": -1.520673606993746, + "angularVelocity": -2.7667283142663646e-18 + }, + { + "timestamp": 4.489230437330042, + "x": 6.83556073153069, + "y": 4.026963256042092, + "heading": -4.1243476277543403e-19, + "velocityX": 0.5682177067376644, + "velocityY": -1.4977995367400567, + "angularVelocity": -2.3893236872795488e-18 + }, + { + "timestamp": 4.516924500657326, + "x": 6.8532528249006415, + "y": 3.986184078077806, + "heading": -4.673685141873994e-19, + "velocityX": 0.6388406482959381, + "velocityY": -1.4724880737926662, + "angularVelocity": -1.9835931108962957e-18 + }, + { + "timestamp": 4.54461856398461, + "x": 6.8728762003622395, + "y": 3.9461709379519925, + "heading": -5.103268481000934e-19, + "velocityX": 0.7085769693559092, + "velocityY": -1.4448273499249418, + "angularVelocity": -1.5511753625583402e-18 + }, + { + "timestamp": 4.572312627311894, + "x": 6.8944048690270705, + "y": 3.9069863642805704, + "heading": -5.406144797086824e-19, + "velocityX": 0.7773748622731119, + "velocityY": -1.4149087914021297, + "angularVelocity": -1.0936517020884735e-18 + }, + { + "timestamp": 4.600006690639178, + "x": 6.917811628359295, + "y": 3.8686902950155, + "heading": -4.440474823091974e-19, + "velocityX": 0.8451905036688525, + "velocityY": -1.3828259440477353, + "angularVelocity": 3.486919029620622e-18 + }, + { + "timestamp": 4.6277007539664625, + "x": 6.943068275241948, + "y": 3.831340048547767, + "heading": -3.335518357804389e-19, + "velocityX": 0.911987763737461, + "velocityY": -1.3486733971224423, + "angularVelocity": 3.9898665024985945e-18 + }, + { + "timestamp": 4.655394817293747, + "x": 6.970145808109622, + "y": 3.7949903214566754, + "heading": -2.0856300853582498e-19, + "velocityX": 0.9777378114463198, + "velocityY": -1.3125458211572592, + "angularVelocity": 4.513198126042523e-18 + }, + { + "timestamp": 4.683088880621031, + "x": 6.9990146159995055, + "y": 3.7596932095232787, + "heading": -1.8209221598354913e-19, + "velocityX": 1.0424186421730879, + "velocityY": -1.2745371279129505, + "angularVelocity": 9.558283193675835e-19 + }, + { + "timestamp": 4.710782943948315, + "x": 7.029644653058208, + "y": 3.725498248565643, + "heading": -2.1000804685949047e-19, + "velocityX": 1.1060145525313745, + "velocityY": -1.2347397546370906, + "angularVelocity": -1.0080081201848759e-18 + }, + { + "timestamp": 4.738477007275599, + "x": 7.062005597653486, + "y": 3.692452471728192, + "heading": -1.084446977273025e-19, + "velocityX": 1.168515584471714, + "velocityY": -1.193244069926507, + "angularVelocity": 3.667332467594088e-18 + }, + { + "timestamp": 4.7661710706028835, + "x": 7.096066995764078, + "y": 3.6606004800341547, + "heading": -1.3461920373400012e-20, + "velocityX": 1.2299169575825302, + "velocityY": -1.1501378948121377, + "angularVelocity": 3.429715902110701e-18 + }, + { + "timestamp": 4.793865133930168, + "x": 7.131798388752283, + "y": 3.629984523258401, + "heading": -1.5274380466475932e-20, + "velocityX": 1.2902185051697632, + "velocityY": -1.105506130102267, + "angularVelocity": -6.54457296822698e-20 + }, + { + "timestamp": 4.821559197257452, + "x": 7.1691694259643555, + "y": 3.600644588470459, + "heading": 0, + "velocityX": 1.3494241264067142, + "velocityY": -1.0594304794210403, + "angularVelocity": 5.515398649571509e-19 + }, + { + "timestamp": 4.860229979851676, + "x": 7.224490933942949, + "y": 3.562237323488572, + "heading": -1.4273741900724991e-19, + "velocityX": 1.4305763749103853, + "velocityY": -0.9931856147028838, + "angularVelocity": -3.691092303190871e-18 + }, + { + "timestamp": 4.8989007624459, + "x": 7.2829218829627, + "y": 3.526426656699073, + "heading": -2.826990069245146e-20, + "velocityX": 1.5109843944166248, + "velocityY": -0.92603936065278, + "angularVelocity": 2.9600508098266124e-18 + }, + { + "timestamp": 4.937571545040123, + "x": 7.344429149979499, + "y": 3.493251746439967, + "heading": 1.2416945578820799e-19, + "velocityX": 1.5905358746472524, + "velocityY": -0.8578804987530882, + "angularVelocity": 3.941977094311565e-18 + }, + { + "timestamp": 4.976242327634347, + "x": 7.408974239733027, + "y": 3.4627568691940223, + "heading": 5.383607057807366e-19, + "velocityX": 1.6690918937639638, + "velocityY": -0.7885766772793149, + "angularVelocity": 1.0710701988609312e-17 + }, + { + "timestamp": 5.014913110228571, + "x": 7.476511914328953, + "y": 3.434992445897612, + "heading": 7.743218382860568e-19, + "velocityX": 1.7464780918608307, + "velocityY": -0.7179690048619871, + "angularVelocity": 6.101793487036617e-18 + }, + { + "timestamp": 5.053583892822795, + "x": 7.5469883327111305, + "y": 3.4100163422501466, + "heading": 1.0565848104622019e-18, + "velocityX": 1.8224719970550483, + "velocityY": -0.6458649650186884, + "angularVelocity": 7.29912787877777e-18 + }, + { + "timestamp": 5.092254675417019, + "x": 7.620338469503448, + "y": 3.3878955288635875, + "heading": 1.7969259306275936e-18, + "velocityX": 1.896784390478618, + "velocityY": -0.57202911093561, + "angularVelocity": 1.9144713618424857e-17 + }, + { + "timestamp": 5.130925458011243, + "x": 7.696482446244184, + "y": 3.368708211087513, + "heading": 2.1772966886455785e-18, + "velocityX": 1.9690311814922792, + "velocityY": -0.49617091997871005, + "angularVelocity": 9.836126791797473e-18 + }, + { + "timestamp": 5.1695962406054665, + "x": 7.7753201738126005, + "y": 3.3525465572778215, + "heading": 2.2655219858710494e-18, + "velocityX": 2.038689736271096, + "velocityY": -0.4179293183506455, + "angularVelocity": 2.2814472891884663e-18 + }, + { + "timestamp": 5.20826702319969, + "x": 7.856723292228703, + "y": 3.3395201391961975, + "heading": 2.1978350166492497e-18, + "velocityX": 2.1050289897225842, + "velocityY": -0.33685426587587375, + "angularVelocity": -1.7503375651963432e-18 + }, + { + "timestamp": 5.246937805793914, + "x": 7.940522653406374, + "y": 3.3297600570672055, + "heading": 2.424348273216306e-18, + "velocityX": 2.166994189307819, + "velocityY": -0.2523890512225693, + "angularVelocity": 5.857478253606304e-18 + }, + { + "timestamp": 5.285608588388138, + "x": 8.026488266632105, + "y": 3.3234231719471747, + "heading": 2.134590026356317e-18, + "velocityX": 2.223011986278472, + "velocityY": -0.16386751689312626, + "angularVelocity": -7.492948496000614e-18 + }, + { + "timestamp": 5.324279370982362, + "x": 8.1142964045733, + "y": 3.320694059402225, + "heading": 1.9377574367066e-18, + "velocityX": 2.270658415749503, + "velocityY": -0.07057298461180006, + "angularVelocity": -5.089954885991328e-18 + }, + { + "timestamp": 5.362950153576586, + "x": 8.203475863616505, + "y": 3.3217768219584887, + "heading": 1.8474257066253063e-18, + "velocityX": 2.306119841922259, + "velocityY": 0.027999499457421157, + "angularVelocity": -2.3359164889328848e-18 + }, + { + "timestamp": 5.40162093617081, + "x": 8.293327802057727, + "y": 3.326854415713941, + "heading": 1.879986343890872e-18, + "velocityX": 2.3235096994038806, + "velocityY": 0.1313031031394509, + "angularVelocity": 8.419990651878979e-19 + }, + { + "timestamp": 5.4402917187650335, + "x": 8.382847452378353, + "y": 3.3359693948238105, + "heading": 1.943872151906946e-18, + "velocityX": 2.314916955779332, + "velocityY": 0.23570712818288259, + "angularVelocity": 1.6520466355085231e-18 + }, + { + "timestamp": 5.478962501359257, + "x": 8.470783550505773, + "y": 3.3488130804859626, + "heading": 2.1687712359482573e-18, + "velocityX": 2.2739673786832317, + "velocityY": 0.3321289304362025, + "angularVelocity": 5.8157392041052145e-18 + }, + { + "timestamp": 5.517633283953481, + "x": 8.555976068741822, + "y": 3.364637313238136, + "heading": 2.2495148531033526e-18, + "velocityX": 2.203020278383855, + "velocityY": 0.4092038404864077, + "angularVelocity": 2.087977525164704e-18 + }, + { + "timestamp": 5.556304066547705, + "x": 8.637675493242655, + "y": 3.382513178337331, + "heading": 2.3065202944378103e-18, + "velocityX": 2.1126912624994216, + "velocityY": 0.46225765034978306, + "angularVelocity": 1.4741219888713533e-18 + }, + { + "timestamp": 5.594974849141929, + "x": 8.715517230799358, + "y": 3.4016256353771546, + "heading": 2.150607115589708e-18, + "velocityX": 2.0129341154924116, + "velocityY": 0.49423507251897597, + "angularVelocity": -4.0318086405284845e-18 + }, + { + "timestamp": 5.633645631736153, + "x": 8.789355423341263, + "y": 3.42135659338803, + "heading": 1.7517953363711107e-18, + "velocityX": 1.909405178496008, + "velocityY": 0.5102290847825096, + "angularVelocity": -1.0312999914821358e-17 + }, + { + "timestamp": 5.672316414330377, + "x": 8.859146004052764, + "y": 3.441254167538288, + "heading": 1.416161918834889e-18, + "velocityX": 1.8047367037750677, + "velocityY": 0.5145376642371936, + "angularVelocity": -8.679250929848945e-18 + }, + { + "timestamp": 5.7109871969246, + "x": 8.924888961953693, + "y": 3.460985377675674, + "heading": 1.2028399924524531e-18, + "velocityX": 1.7000679451144207, + "velocityY": 0.5102356046017003, + "angularVelocity": -5.516359997505784e-18 + }, + { + "timestamp": 5.749657979518824, + "x": 8.986602419973897, + "y": 3.4802993804547686, + "heading": 8.295806023476167e-19, + "velocityX": 1.595867833029694, + "velocityY": 0.4994469075466367, + "angularVelocity": -9.652232660551833e-18 + }, + { + "timestamp": 5.788328762113048, + "x": 9.044311163070581, + "y": 3.499002488214024, + "heading": 3.165440414148323e-19, + "velocityX": 1.4923086429935726, + "velocityY": 0.4836495799815523, + "angularVelocity": -1.3266774338561772e-17 + }, + { + "timestamp": 5.826999544707272, + "x": 9.098041553812648, + "y": 3.516941626488888, + "heading": -1.3634511417910052e-19, + "velocityX": 1.3894311709660454, + "velocityY": 0.46389385141491557, + "angularVelocity": -1.1711403346191615e-17 + }, + { + "timestamp": 5.865670327301496, + "x": 9.147819310254551, + "y": 3.5339932846439632, + "heading": -5.351657410219041e-19, + "velocityX": 1.287218750244256, + "velocityY": 0.44094422225686936, + "angularVelocity": -1.0313228436099753e-17 + }, + { + "timestamp": 5.90434110989572, + "x": 9.193668593077833, + "y": 3.5500559859721394, + "heading": -8.103923290509881e-19, + "velocityX": 1.1856311082292463, + "velocityY": 0.4153704748290859, + "angularVelocity": -7.1171713469746e-18 + }, + { + "timestamp": 5.9430118924899435, + "x": 9.235611699378987, + "y": 3.565045036055544, + "heading": -1.0408712708752226e-18, + "velocityX": 1.0846200538858524, + "velocityY": 0.3876065876578114, + "angularVelocity": -5.960028046884079e-18 + }, + { + "timestamp": 5.981682675084167, + "x": 9.2736690360789, + "y": 3.578888774891126, + "heading": -1.2302348448445404e-18, + "velocityX": 0.9841367085649779, + "velocityY": 0.3579896217991075, + "angularVelocity": -4.896813153460037e-18 + }, + { + "timestamp": 6.020353457678391, + "x": 9.307859217238196, + "y": 3.59152584378987, + "heading": -1.3138022795035935e-18, + "velocityX": 0.8841347101262037, + "velocityY": 0.3267859621912809, + "angularVelocity": -2.160997315257982e-18 + }, + { + "timestamp": 6.059024240272615, + "x": 9.338199210185026, + "y": 3.6029031524068102, + "heading": -1.362101109862026e-18, + "velocityX": 0.7845714751933003, + "velocityY": 0.29420942255871, + "angularVelocity": -1.2489755201874302e-18 + }, + { + "timestamp": 6.097695022866839, + "x": 9.364704494330118, + "y": 3.612974339107483, + "heading": -1.3775148409871813e-18, + "velocityX": 0.6854085272391679, + "velocityY": 0.2604340027547543, + "angularVelocity": -3.9859007505180033e-19 + }, + { + "timestamp": 6.136365805461063, + "x": 9.387389215756244, + "y": 3.621698585836685, + "heading": -9.194127131953696e-19, + "velocityX": 0.5866113873142459, + "velocityY": 0.22560305594913474, + "angularVelocity": 1.184620555949573e-17 + }, + { + "timestamp": 6.175036588055287, + "x": 9.406266330244824, + "y": 3.6290396923599735, + "heading": -4.324037121145138e-19, + "velocityX": 0.48814927503944505, + "velocityY": 0.1898359958297115, + "angularVelocity": 1.2593717368297416e-17 + }, + { + "timestamp": 6.2137073706495105, + "x": 9.421347732169448, + "y": 3.634965343433838, + "heading": -6.350098887170209e-19, + "velocityX": 0.38999474313399257, + "velocityY": 0.1532332856058955, + "angularVelocity": -5.239258436014818e-18 + }, + { + "timestamp": 6.252378153243734, + "x": 9.432644369013142, + "y": 3.639446521662576, + "heading": -4.293741073807372e-19, + "velocityX": 0.2921233056550846, + "velocityY": 0.1158802053674366, + "angularVelocity": 5.317599602073295e-18 + }, + { + "timestamp": 6.291048935837958, + "x": 9.440166342343868, + "y": 3.642457031895519, + "heading": 2.1988295300212938e-20, + "velocityX": 0.19451308781759213, + "velocityY": 0.07784973644138414, + "angularVelocity": 1.1671920640227671e-17 + }, + { + "timestamp": 6.329719718432182, + "x": 9.443922996520996, + "y": 3.6439731121063232, + "heading": 0, + "velocityX": 0.09714450872500752, + "velocityY": 0.03920479775939959, + "angularVelocity": -5.686029032587966e-19 + }, + { + "timestamp": 6.368390501026406, + "x": 9.443922996520996, + "y": 3.6439731121063232, + "heading": 0, + "velocityX": 1.8284117622554373e-32, + "velocityY": 7.799691816180278e-34, + "angularVelocity": 0 + } + ] + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/frc/team449/Main.kt b/src/main/kotlin/frc/team449/Main.kt new file mode 100644 index 0000000..18b6c47 --- /dev/null +++ b/src/main/kotlin/frc/team449/Main.kt @@ -0,0 +1,9 @@ +@file:JvmName("Main") + +package frc.team449 + +import edu.wpi.first.wpilibj.RobotBase + +fun main() { + RobotBase.startRobot { RobotLoop() } +} diff --git a/src/main/kotlin/frc/team449/RobotBase.kt b/src/main/kotlin/frc/team449/RobotBase.kt new file mode 100644 index 0000000..03c5981 --- /dev/null +++ b/src/main/kotlin/frc/team449/RobotBase.kt @@ -0,0 +1,17 @@ +package frc.team449 + +import edu.wpi.first.wpilibj.PowerDistribution +import edu.wpi.first.wpilibj.smartdashboard.Field2d +import edu.wpi.first.wpilibj2.command.Command +import frc.team449.control.holonomic.HolonomicDrive + +abstract class RobotBase { + + val field = Field2d() + + abstract val powerDistribution: PowerDistribution + + abstract val drive: HolonomicDrive? + + abstract val driveCommand: Command +} diff --git a/src/main/kotlin/frc/team449/RobotLoop.kt b/src/main/kotlin/frc/team449/RobotLoop.kt new file mode 100644 index 0000000..d3acdf1 --- /dev/null +++ b/src/main/kotlin/frc/team449/RobotLoop.kt @@ -0,0 +1,139 @@ +package frc.team449 + +import edu.wpi.first.wpilibj.DriverStation +import edu.wpi.first.wpilibj.RobotBase +import edu.wpi.first.wpilibj.TimedRobot +import edu.wpi.first.wpilibj.Timer +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard +import edu.wpi.first.wpilibj2.command.Command +import edu.wpi.first.wpilibj2.command.CommandScheduler +import edu.wpi.first.wpilibj2.command.InstantCommand +import frc.team449.control.holonomic.SwerveSim +import frc.team449.robot2023.Robot +import frc.team449.robot2023.auto.routines.RoutineChooser +import frc.team449.robot2023.commands.light.BlairChasing +import frc.team449.robot2023.commands.light.BreatheHue +import frc.team449.robot2023.commands.light.Rainbow +import frc.team449.robot2023.constants.vision.VisionConstants +import frc.team449.robot2023.subsystems.ControllerBindings +import monologue.Logged +import monologue.Monologue +import monologue.Monologue.LogBoth +import kotlin.jvm.optionals.getOrNull + +/** The main class of the robot, constructs all the subsystems and initializes default commands. */ +class RobotLoop : TimedRobot(), Logged { + + @LogBoth + private val robot = Robot() + + private val routineChooser: RoutineChooser = RoutineChooser(robot) + + @LogBoth + private val field = robot.field + + private var autoCommand: Command? = null + private var routineMap = hashMapOf() + private val controllerBinder = ControllerBindings(robot.driveController, robot.mechController, robot) + + override fun robotInit() { + // Yes this should be a print statement, it's useful to know that robotInit started. + println("Started robotInit.") + + if (RobotBase.isSimulation()) { + // Don't complain about joysticks if there aren't going to be any + DriverStation.silenceJoystickConnectionWarning(true) +// val instance = NetworkTableInstance.getDefault() +// instance.stopServer() +// instance.startClient4("localhost") + } + + println("Generating Auto Routines : ${Timer.getFPGATimestamp()}") + routineMap = routineChooser.routineMap() + println("DONE Generating Auto Routines : ${Timer.getFPGATimestamp()}") + + SmartDashboard.putData("Routine Chooser", routineChooser) + SmartDashboard.putData("Command Scheduler", CommandScheduler.getInstance()) + SmartDashboard.putBoolean("Enable Logging?", false) + + robot.light.defaultCommand = BlairChasing(robot.light) + + controllerBinder.bindButtons() + + Monologue.setupLogging(this, "/Monologuing") + } + + override fun robotPeriodic() { + CommandScheduler.getInstance().run() + + robot.field.robotPose = robot.drive.pose + + robot.field.getObject("bumpers").pose = robot.drive.pose + + if (SmartDashboard.getBoolean("Enable Logging?", false)) { + Monologue.update() + } else if (RobotBase.isSimulation()) { + Monologue.updateNT() + } else { + Monologue.updateFileLog() + } + } + + override fun autonomousInit() { + /** Every time auto starts, we update the chosen auto command */ + this.autoCommand = routineMap[routineChooser.selected] + CommandScheduler.getInstance().schedule(this.autoCommand) + + if (DriverStation.getAlliance().getOrNull() == DriverStation.Alliance.Red) { + BreatheHue(robot.light, 0).schedule() + } else { + BreatheHue(robot.light, 95).schedule() + } + } + + override fun autonomousPeriodic() {} + + override fun teleopInit() { + if (autoCommand != null) { + CommandScheduler.getInstance().cancel(autoCommand) + } + + (robot.light.currentCommand ?: InstantCommand()).cancel() + + robot.drive.defaultCommand = robot.driveCommand + } + + override fun teleopPeriodic() { + } + + override fun disabledInit() { + robot.drive.stop() + + (robot.light.currentCommand ?: InstantCommand()).cancel() + Rainbow(robot.light).schedule() + } + + override fun disabledPeriodic() { + routineChooser.updateOptions(DriverStation.getAlliance().getOrNull() == DriverStation.Alliance.Red) + } + + override fun testInit() { + if (autoCommand != null) { + CommandScheduler.getInstance().cancel(autoCommand) + } + } + + override fun testPeriodic() {} + + override fun simulationInit() {} + + override fun simulationPeriodic() { + robot.drive as SwerveSim + + VisionConstants.ESTIMATORS.forEach { + it.simulationPeriodic(robot.drive.odoPose) + } + + VisionConstants.VISION_SIM.debugField.getObject("EstimatedRobot").pose = robot.drive.pose + } +} diff --git a/src/main/kotlin/frc/team449/control/DriveCommand.kt b/src/main/kotlin/frc/team449/control/DriveCommand.kt new file mode 100644 index 0000000..0047ecd --- /dev/null +++ b/src/main/kotlin/frc/team449/control/DriveCommand.kt @@ -0,0 +1,24 @@ +package frc.team449.control + +import edu.wpi.first.math.kinematics.ChassisSpeeds +import edu.wpi.first.wpilibj2.command.Command + +/** + * Generic driving command that applies the OI output to the drivetrain. + * @param drive The drivetrain to be controlled. + * @param oi The OI that feeds the inputted [ChassisSpeeds] to the [drive]. + */ +class DriveCommand( + private val drive: DriveSubsystem, + private val oi: OI +) : Command() { + + init { + addRequirements(drive) + } + + /** Take returned [ChassisSpeeds] from a joystick/[OI] and feed it to a [DriveSubsystem]. */ + override fun execute() { + drive.set(oi.get()) + } +} diff --git a/src/main/kotlin/frc/team449/control/DriveSubsystem.kt b/src/main/kotlin/frc/team449/control/DriveSubsystem.kt new file mode 100644 index 0000000..1c18479 --- /dev/null +++ b/src/main/kotlin/frc/team449/control/DriveSubsystem.kt @@ -0,0 +1,37 @@ +package frc.team449.control + +import edu.wpi.first.math.MathUtil +import edu.wpi.first.math.geometry.Pose2d +import edu.wpi.first.math.geometry.Rotation2d +import edu.wpi.first.math.geometry.Translation2d +import edu.wpi.first.math.kinematics.ChassisSpeeds +import edu.wpi.first.wpilibj2.command.Subsystem + +/** A drivetrain that uses closed-loop velocity control. */ +interface DriveSubsystem : Subsystem { + var heading: Rotation2d + get() = Rotation2d(MathUtil.angleModulus(this.pose.rotation.radians)) + set(value) { + this.pose = Pose2d(Translation2d(this.pose.x, this.pose.y), value) + } + + var pose: Pose2d + + /** Sets the drivetrain's desired speeds. */ + fun set(desiredSpeeds: ChassisSpeeds) + + /** Sets all the robot's drive motors to 0. */ + fun stop() + + /** + * Used to simulate a drivetrain. Only one instance of this class should be made per drivetrain. + */ + interface SimController { + fun update() + + /** + * Simulate the current drawn by the drivetrain + */ + fun getCurrentDraw(): Double + } +} diff --git a/src/main/kotlin/frc/team449/control/OI.kt b/src/main/kotlin/frc/team449/control/OI.kt new file mode 100644 index 0000000..2806da6 --- /dev/null +++ b/src/main/kotlin/frc/team449/control/OI.kt @@ -0,0 +1,11 @@ +package frc.team449.control + +import edu.wpi.first.math.kinematics.ChassisSpeeds + +/** + * An Operator Input (OI) that gets the desired [ChassisSpeeds] to give a [DriveSubsystem]. + */ + +fun interface OI { + fun get(): ChassisSpeeds +} diff --git a/src/main/kotlin/frc/team449/control/TrapezoidalExponentialProfile.kt b/src/main/kotlin/frc/team449/control/TrapezoidalExponentialProfile.kt new file mode 100644 index 0000000..7f1bba5 --- /dev/null +++ b/src/main/kotlin/frc/team449/control/TrapezoidalExponentialProfile.kt @@ -0,0 +1,300 @@ +package frc.team449.control + +import edu.wpi.first.math.util.Units +import frc.team449.robot2023.constants.MotorConstants +import kotlin.math.* + +// TODO: Copy parameter descriptions from Rafi's spreadsheet +// and figure out to do for current vel > 0 situations (ideas: don't do anything, or use a trap profile in that case idk) + +/** @param effectiveGearing is motor rotations : output rotations (should be greater than 1 if a reduction + * @param pulleyRadius in meters + * @param systemMass in kilograms + * @param angle in degrees + * @param tolerance in meters + * @param vMax user specified max speed in m/s + * @param startingDistance the start distance of the profile + * @param finalDistance the goal distance of the profile + * @param aStop the max negative acceleration of the profile + * @param efficiency percent efficiency of the system + */ +class TrapezoidalExponentialProfile( + pulleyRadius: Double, + currentLimit: Int, + numMotors: Int, + effectiveGearing: Double, + systemMass: Double, + angle: Double, + private val tolerance: Double = 0.05, + private val vMax: Double = 5.0, + startingDistance: Double = 0.0, + private var finalDistance: Double, + private var aStop: Double = 9.81, + efficiency: Double +) { + private val trueStartingDistance: Double = startingDistance + private val trueFinalDistance: Double = finalDistance + private var switchedStartingAndFinal: Boolean = false + + // NEO Motor Constants + val freeSpeed = MotorConstants.FREE_SPEED + val freeCurrent = MotorConstants.FREE_CURRENT + val stallCurrent = MotorConstants.STALL_CURRENT + val stallTorque = MotorConstants.STALL_TORQUE * efficiency + + private fun expDecelIntercept( + vFree: Double, + vLim: Double, + aLim: Double, + aStop: Double, + xF: Double, + x20: Double + ): Double { + val A = vFree + val B = -(vFree - vLim) + val C = -aLim / (vFree - vLim) + val D = 2 * aStop + val deltaX = xF - x20 + + var lastSeed = -5.0 + var seed = 0.0 + var seedVal = evaluateExp(A, B, C, D, deltaX, seed) + var seedDer = evaluateExpDerivative(A, B, C, D, seed) + + while (abs(seed - lastSeed) > 1e-4) { + lastSeed = seed + seed -= seedVal / seedDer + seedVal = evaluateExp(A, B, C, D, deltaX, seed) + seedDer = evaluateExpDerivative(A, B, C, D, seed) + + if (seedVal == 0.0) { + return 0.0 + } + + if (seedDer == 0.0) { + error("Newton method derivative is 0") + } + } + + return seed + } + + private fun evaluateExp( + A: Double, + B: Double, + C: Double, + D: Double, + deltaX: Double, + t: Double + ): Double { + return B.pow(2) * exp(2 * C * t) + (D * B / C + 2 * A * B) * exp(C * t) + A * D * t - D * B / C - deltaX * D + A.pow( + 2 + ) + } + + private fun evaluateExpDerivative( + A: Double, + B: Double, + C: Double, + D: Double, + t: Double + ): Double { + return 2 * B.pow(2) * C * exp(2 * C * t) + (B * D / C + 2 * A * B) * C * exp(C * t) + A * D + } + + private val vLim = + pulleyRadius * freeSpeed / effectiveGearing * (1 - (currentLimit - freeCurrent) / (stallCurrent - freeCurrent)) + private val effectiveStallTorque = stallTorque * numMotors + private val effectiveGravity = 9.81 * sin(Units.degreesToRadians(angle)) + private var aLim = + ((currentLimit - freeCurrent) / (stallCurrent - freeCurrent) * effectiveStallTorque * effectiveGearing) / (systemMass * pulleyRadius) - effectiveGravity + private val vFree = + pulleyRadius * freeSpeed / effectiveGearing * (1 - systemMass * effectiveGravity * pulleyRadius / effectiveGearing / effectiveStallTorque) + private val vMax2 = min(vMax, vFree) + + init { + if (startingDistance > finalDistance) { + finalDistance = trueStartingDistance - trueFinalDistance + val buffer = aStop + aStop = aLim + aLim = buffer + switchedStartingAndFinal = true + } else if (startingDistance > 0) { + finalDistance = trueFinalDistance - trueStartingDistance + } + } + + private val t12 = vLim / aLim + private val t13 = vMax2 / aLim + private val t14 = sqrt(2 * finalDistance / (aLim + aLim.pow(2) / aStop)) + private val t1f = min(t14, min(t12, t13)) + private val x1f = 0.5 * aLim * t1f.pow(2) + private val v1f = aLim * t1f + + private val ENTER_EXP = t12 < t13 && t12 < t14 + private val t20 = if (ENTER_EXP) t12 else Double.NaN + private val x20 = if (ENTER_EXP) x1f else Double.NaN + private val v20 = if (ENTER_EXP) v1f else Double.NaN + private val dt23 = + if ((vFree - vMax2) / (vFree - vLim) > 0) -(vFree - vLim) / aLim * ln((vFree - vMax2) / (vFree - vLim)) else Double.NaN + private val dt24 = if (ENTER_EXP) expDecelIntercept(vFree, vLim, aLim, aStop, finalDistance, x20) else Double.NaN + private val t2f = t20 + if (!dt23.isNaN() && !dt24.isNaN()) min(dt23, dt24) else if (!dt24.isNaN()) dt24 else 0.0 + private val x2f = + x20 + vFree * (t2f - t20) + (vFree - vLim).pow(2) / aLim * (exp(-aLim / (vFree - vLim) * (t2f - t20)) - 1) + private val v2f = vFree - (vFree - vLim) * exp(-aLim / (vFree - vLim) * (t2f - t20)) + private val ENTER_COAST = + if (ENTER_EXP) { + if (!dt23.isNaN() && !dt24.isNaN()) { + dt24 > dt23 + } else { + false + } + } else { + t14 > t13 + } + + private val t30 = + if (ENTER_COAST) { + if (ENTER_EXP) { + t2f + } else { + t13 + } + } else { + Double.NaN + } + + private val x30 = + if (ENTER_COAST) { + if (ENTER_EXP) { + x2f + } else { + x1f + } + } else { + Double.NaN + } + + private val v30 = + if (ENTER_COAST) { + if (ENTER_EXP) { + v2f + } else { + v1f + } + } else { + Double.NaN + } + + private val dt34 = if (!x30.isNaN()) (finalDistance - x30) - vMax2 - vMax2 / 2 / aStop else Double.NaN + private val t3f = if (!dt34.isNaN() && !t30.isNaN()) dt34 + t30 else Double.NaN + private val x3f = if (!x30.isNaN() && !dt34.isNaN()) x30 + vMax2 * dt34 else Double.NaN + private val v3f = vMax2 + + private val t40 = if (ENTER_COAST) t3f else if (ENTER_EXP) t2f else t14 + private val x40 = if (ENTER_COAST) x3f else if (ENTER_EXP) x2f else x1f + private val v40 = if (ENTER_COAST) v3f else if (ENTER_EXP) v2f else v1f + private val t4f = t40 + v40 / aStop + private val x4f = x40 + v40 * (t4f - t40) - 0.5 * aStop * (t4f - t40).pow(2) + private val v4f = v40 - aStop * (t4f - t40) + + val finalTime = t4f + + private fun sample1(t: Double): Pair { + return if (switchedStartingAndFinal) { + Pair( + trueStartingDistance - (0.5 * aLim * t.pow(2)), + -(aLim * t) + ) + } else { + Pair( + 0.5 * aLim * t.pow(2) + trueStartingDistance, + aLim * t + ) + } + } + + private fun sample2(t: Double): Pair { + return if (switchedStartingAndFinal) { + Pair( + trueStartingDistance - (x20 + vFree * (t - t20) + (vFree - vLim).pow(2) / aLim * (exp(-aLim / (vFree - vLim) * (t - t20)) - 1)), + -(vFree - (vFree - vLim) * exp(-aLim / (vFree - vLim) * (t - t20))) + ) + } else { + Pair( + x20 + vFree * (t - t20) + (vFree - vLim).pow(2) / aLim * (exp(-aLim / (vFree - vLim) * (t - t20)) - 1) + trueStartingDistance, + vFree - (vFree - vLim) * exp(-aLim / (vFree - vLim) * (t - t20)) + ) + } + } + + private fun sample3(t: Double): Pair { + return if (switchedStartingAndFinal) { + Pair( + // hey, this was vMax in this script. Shouldn't this be vMax2 for the actual top speed? + trueStartingDistance - (x30 + vMax2 * (t - t30)), + -vMax + ) + } else { + Pair( + // hey, this was vMax in this script. Shouldn't this be vMax2 for the actual top speed? + x30 + vMax2 * (t - t30) + trueStartingDistance, + vMax + ) + } + } + + private fun sample4(t: Double): Pair { + return if (switchedStartingAndFinal) { + Pair( + trueStartingDistance - (x40 + v40 * (t - t40) - 0.5 * aStop * (t - t40).pow(2)), + -(v40 - aStop * (t - t40)) + ) + } else { + Pair( + x40 + v40 * (t - t40) - 0.5 * aStop * (t - t40).pow(2) + trueStartingDistance, + v40 - aStop * (t - t40) + ) + } + } + + fun calculate(t: Double): Pair { + if (abs(trueFinalDistance - trueStartingDistance) < tolerance) return Pair(trueFinalDistance, 0.0) + + if (t <= 0.0) return Pair(trueStartingDistance, 0.0) + + if (t >= t4f) return Pair(trueFinalDistance, 0.0) + + if (t <= t1f) { + return sample1(t) + } + + if (ENTER_EXP) { + return if (ENTER_COAST) { + if (t <= t2f) { + sample2(t) + } else if (t <= t3f) { + sample3(t) + } else { + sample4(t) + } + } else { + if (t <= t2f) { + sample2(t) + } else { + sample4(t) + } + } + } else { + return if (ENTER_COAST) { + if (t <= t3f) { + sample3(t) + } else { + sample4(t) + } + } else { + sample4(t) + } + } + } +} diff --git a/src/main/kotlin/frc/team449/control/auto/ChoreoFollower.kt b/src/main/kotlin/frc/team449/control/auto/ChoreoFollower.kt new file mode 100644 index 0000000..c615ee9 --- /dev/null +++ b/src/main/kotlin/frc/team449/control/auto/ChoreoFollower.kt @@ -0,0 +1,91 @@ +package frc.team449.control.auto + +import edu.wpi.first.math.controller.PIDController +import edu.wpi.first.math.geometry.Pose2d +import edu.wpi.first.math.geometry.Rotation2d +import edu.wpi.first.math.kinematics.ChassisSpeeds +import edu.wpi.first.wpilibj.Timer +import edu.wpi.first.wpilibj2.command.Command +import frc.team449.control.holonomic.HolonomicDrive +import frc.team449.robot2023.constants.auto.AutoConstants +import kotlin.math.PI + +class ChoreoFollower( + private val drivetrain: HolonomicDrive, + private val trajectory: ChoreoTrajectory, + private val xController: PIDController = PIDController(AutoConstants.DEFAULT_X_KP, 0.0, 0.0), + private val yController: PIDController = PIDController(AutoConstants.DEFAULT_Y_KP, 0.0, 0.0), + private val thetaController: PIDController = PIDController(AutoConstants.DEFAULT_ROTATION_KP, 0.0, 0.0), + poseTol: Pose2d = Pose2d(0.035, 0.035, Rotation2d(0.035)), + private val timeout: Double = 0.65, + private val resetPose: Boolean = false, + private val debug: Boolean = false +) : Command() { + + private val timer = Timer() + + init { + addRequirements(drivetrain) + + xController.reset() + xController.setTolerance(poseTol.x) + + yController.reset() + yController.setTolerance(poseTol.y) + + thetaController.reset() + thetaController.enableContinuousInput(-PI, PI) + thetaController.setTolerance(poseTol.rotation.radians) + } + + private fun calculate(currPose: Pose2d, desState: ChoreoTrajectory.ChoreoState): ChassisSpeeds { + val xFF = desState.xVel + val yFF = desState.yVel + val angFF = desState.thetaVel + + val xPID = xController.calculate(currPose.x, desState.xPos) + val yPID = yController.calculate(currPose.y, desState.yPos) + val angPID = thetaController.calculate(currPose.rotation.radians, desState.theta) + + return if (debug) { + ChassisSpeeds.fromFieldRelativeSpeeds(xFF, yFF, angFF, currPose.rotation) + } else { + ChassisSpeeds.fromFieldRelativeSpeeds(xFF + xPID, yFF + yPID, angFF + angPID, currPose.rotation) + } + } + + private fun allControllersAtReference(): Boolean { + return xController.atSetpoint() && yController.atSetpoint() && thetaController.atSetpoint() + } + + override fun initialize() { + xController.reset() + yController.reset() + thetaController.reset() + + if (resetPose) { + drivetrain.pose = trajectory.initialPose() + } + + timer.restart() + } + + override fun execute() { + val currTime = timer.get() + + val desiredMatrix = trajectory.sample(currTime) + + drivetrain.set(calculate(drivetrain.pose, desiredMatrix)) + } + + override fun isFinished(): Boolean { + return (timer.hasElapsed(trajectory.totalTime) && allControllersAtReference()) || + timer.hasElapsed(trajectory.totalTime + timeout) + } + + override fun end(interrupted: Boolean) { + timer.stop() + timer.reset() + drivetrain.stop() + } +} diff --git a/src/main/kotlin/frc/team449/control/auto/ChoreoRoutine.kt b/src/main/kotlin/frc/team449/control/auto/ChoreoRoutine.kt new file mode 100644 index 0000000..c1e159e --- /dev/null +++ b/src/main/kotlin/frc/team449/control/auto/ChoreoRoutine.kt @@ -0,0 +1,66 @@ +package frc.team449.control.auto + +import edu.wpi.first.math.controller.PIDController +import edu.wpi.first.math.geometry.Pose2d +import edu.wpi.first.math.geometry.Rotation2d +import edu.wpi.first.wpilibj2.command.* +import frc.team449.control.holonomic.HolonomicDrive +import frc.team449.robot2023.constants.auto.AutoConstants +import kotlin.math.abs + +class ChoreoRoutine( + private val xController: PIDController = PIDController(AutoConstants.DEFAULT_X_KP, 0.0, 0.0), + private val yController: PIDController = PIDController(AutoConstants.DEFAULT_Y_KP, 0.0, 0.0), + private val thetaController: PIDController = PIDController(AutoConstants.DEFAULT_ROTATION_KP, 0.0, 0.0), + private val drive: HolonomicDrive, + private val stopEventMap: HashMap = HashMap(), + private val parallelEventMap: HashMap = HashMap(), + private val poseTol: Pose2d = Pose2d(0.05, 0.05, Rotation2d.fromDegrees(1.5)), + private val resetPosition: Boolean = false, + private val resetPositionTolerance: Pose2d = Pose2d(0.0, 0.0, Rotation2d.fromDegrees(0.0)), + private val timeout: Double = 1.5, + private val debug: Boolean = false +) { + + private fun resetPose(trajectory: ChoreoTrajectory): Command { + val poseError = drive.pose.relativeTo(trajectory.initialPose()) + + if (abs(poseError.x) < resetPositionTolerance.x && + abs(poseError.y) < resetPositionTolerance.y && + abs(poseError.rotation.radians) < resetPositionTolerance.rotation.radians + ) { + return PrintCommand("Pose not reset.") + } + + return InstantCommand({ drive.pose = trajectory.initialPose() }) + } + + fun createRoutine(trajectories: MutableList): Command { + val ezraGallun = SequentialCommandGroup( + resetPose(trajectories[0]), + stopEventMap.getOrDefault(0, InstantCommand()) + ) + + for (i in 0 until trajectories.size) { + ezraGallun.addCommands( + ParallelDeadlineGroup( + ChoreoFollower( + drive, + trajectories[i], + xController, + yController, + thetaController, + poseTol, + timeout, + resetPosition, + debug + ), + parallelEventMap.getOrDefault(i, InstantCommand()) + ) + ) + ezraGallun.addCommands(stopEventMap.getOrDefault(i + 1, InstantCommand())) + } + + return ezraGallun + } +} diff --git a/src/main/kotlin/frc/team449/control/auto/ChoreoRoutineStructure.kt b/src/main/kotlin/frc/team449/control/auto/ChoreoRoutineStructure.kt new file mode 100644 index 0000000..6ddd877 --- /dev/null +++ b/src/main/kotlin/frc/team449/control/auto/ChoreoRoutineStructure.kt @@ -0,0 +1,14 @@ +package frc.team449.control.auto + +import edu.wpi.first.wpilibj2.command.Command + +interface ChoreoRoutineStructure { + + val routine: ChoreoRoutine + + val trajectory: MutableList + + fun createCommand(): Command { + return routine.createRoutine(trajectory) + } +} diff --git a/src/main/kotlin/frc/team449/control/auto/ChoreoTrajectory.kt b/src/main/kotlin/frc/team449/control/auto/ChoreoTrajectory.kt new file mode 100644 index 0000000..df8c669 --- /dev/null +++ b/src/main/kotlin/frc/team449/control/auto/ChoreoTrajectory.kt @@ -0,0 +1,116 @@ +package frc.team449.control.auto + +import edu.wpi.first.math.InterpolatingMatrixTreeMap +import edu.wpi.first.math.MatBuilder +import edu.wpi.first.math.MathUtil +import edu.wpi.first.math.geometry.Pose2d +import edu.wpi.first.math.geometry.Rotation2d +import edu.wpi.first.math.numbers.N2 +import edu.wpi.first.math.numbers.N3 +import edu.wpi.first.wpilibj.Filesystem +import org.json.simple.JSONArray +import org.json.simple.JSONObject +import org.json.simple.parser.JSONParser +import java.io.File +import java.io.FileReader + +class ChoreoTrajectory( + val name: String, + val stateMap: InterpolatingMatrixTreeMap, + val totalTime: Double, + val objectiveTimestamps: ArrayList +) { + + fun initialPose(): Pose2d { + val initialState = stateMap.get(0.0) + + return Pose2d( + initialState[0, 0], + initialState[0, 1], + Rotation2d(initialState[0, 2]) + ) + } + + fun sample(t: Double): ChoreoState { + val timeSeconds = MathUtil.clamp(t, 0.0, totalTime) + val interpolatedMat = stateMap.get(timeSeconds) + + return ChoreoState( + interpolatedMat[0, 0], + interpolatedMat[0, 1], + interpolatedMat[0, 2], + interpolatedMat[1, 0], + interpolatedMat[1, 1], + interpolatedMat[1, 2] + ) + } + + class ChoreoState( + val xPos: Double, + val yPos: Double, + val theta: Double, + val xVel: Double, + val yVel: Double, + val thetaVel: Double + ) + + companion object { + fun createTrajectory( + filename: String + ): MutableList { + val path = Filesystem.getDeployDirectory().absolutePath.plus("/choreo/$filename.chor") + val document = (JSONParser().parse(FileReader(File(path).absolutePath)) as JSONObject)["paths"] as HashMap<*, *> + + val trajList = mutableListOf() + + document.forEach { (name, pathData) -> + name as String + pathData as JSONObject + val trajectory = pathData["trajectory"] as JSONArray + + val info = parse(trajectory) + + val last = trajectory.last() as JSONObject + val totalTime = last["timestamp"] as Double + + trajList.add( + ChoreoTrajectory( + filename + name, + info.first, + totalTime, + info.second + ) + ) + } + + return trajList + } + + private fun parse(trajectory: JSONArray): Pair, ArrayList> { + val stateMap = InterpolatingMatrixTreeMap() + + val timestamps = arrayListOf() + + trajectory.forEach { state -> + state as JSONObject + val stateTime = state["timestamp"].toString().toDouble() + + timestamps.add(stateTime) + + val builder = MatBuilder(N2.instance, N3.instance) + val matrix = builder.fill( + state["x"].toString().toDouble(), + state["y"].toString().toDouble(), + state["heading"].toString().toDouble(), + state["velocityX"].toString().toDouble(), + state["velocityY"].toString().toDouble(), + state["angularVelocity"].toString().toDouble() + ) + + stateMap.put(stateTime, matrix) + } + + return stateMap to timestamps + } + } +} diff --git a/src/main/kotlin/frc/team449/control/differential/DifferentialDrive.kt b/src/main/kotlin/frc/team449/control/differential/DifferentialDrive.kt new file mode 100644 index 0000000..3cf0e19 --- /dev/null +++ b/src/main/kotlin/frc/team449/control/differential/DifferentialDrive.kt @@ -0,0 +1,212 @@ +package frc.team449.control.differential + +import edu.wpi.first.math.controller.DifferentialDriveFeedforward +import edu.wpi.first.math.controller.PIDController +import edu.wpi.first.math.estimator.DifferentialDrivePoseEstimator +import edu.wpi.first.math.geometry.Pose2d +import edu.wpi.first.math.kinematics.ChassisSpeeds +import edu.wpi.first.math.kinematics.DifferentialDriveKinematics +import edu.wpi.first.math.kinematics.DifferentialDriveWheelSpeeds +import edu.wpi.first.wpilibj.Timer +import edu.wpi.first.wpilibj2.command.SubsystemBase +import frc.team449.control.DriveSubsystem +import frc.team449.robot2023.constants.RobotConstants +import frc.team449.robot2023.constants.drives.DifferentialConstants +import frc.team449.system.AHRS +import frc.team449.system.encoder.QuadEncoder +import frc.team449.system.motor.WrappedMotor +import frc.team449.system.motor.createSparkMax + +/** + * A differential drive (aka. tank drive). + * @param leftLeader The lead motor of the left side. + * @param rightLeader The lead motor of the right side. + * @param ahrs The gyro that is mounted on the chassis. + * @param feedForward The differential drive feed forward used for calculating the side voltages. + * @param makeSidePID Used to make two copies of the same PID controller to control both sides of the robot. + * @param trackwidth The distance between the two wheel sides of the robot. + */ +open class DifferentialDrive( + private val leftLeader: WrappedMotor, + private val rightLeader: WrappedMotor, + private val ahrs: AHRS, + private val feedForward: DifferentialDriveFeedforward, + private val makeSidePID: () -> PIDController, + private val trackwidth: Double +) : DriveSubsystem, SubsystemBase() { + init { + leftLeader.encoder.resetPosition(0.0) + rightLeader.encoder.resetPosition(0.0) + } + + /** Kinematics used to convert [ChassisSpeeds] to [DifferentialDriveWheelSpeeds] */ + val kinematics = DifferentialDriveKinematics(trackwidth) + + /** Pose estimator that estimates the robot's position as a [Pose2d]. */ + val poseEstimator = DifferentialDrivePoseEstimator( + kinematics, + ahrs.heading, + leftLeader.position, + rightLeader.position, + Pose2d() + ) + + /** Velocity PID controller for left side. */ + private val leftPID = makeSidePID() + + /** Velocity PID controller for right side. */ + private val rightPID = makeSidePID() + + var desiredWheelSpeeds = DifferentialDriveWheelSpeeds(0.0, 0.0) + + private var previousTime = Double.NaN + private var prevWheelSpeeds = DifferentialDriveWheelSpeeds(0.0, 0.0) + + private var prevLeftVel = 0.0 + private var prevRightVel = 0.0 + private var leftVel = 0.0 + private var rightVel = 0.0 + + /** Calculate left and right side speeds from given [ChassisSpeeds]. */ + override fun set(desiredSpeeds: ChassisSpeeds) { + prevWheelSpeeds = desiredWheelSpeeds + + prevLeftVel = desiredWheelSpeeds.leftMetersPerSecond + prevRightVel = desiredWheelSpeeds.rightMetersPerSecond + + desiredWheelSpeeds = kinematics.toWheelSpeeds(desiredSpeeds) + desiredWheelSpeeds.desaturate(RobotConstants.MAX_LINEAR_SPEED) + + leftVel = desiredWheelSpeeds.leftMetersPerSecond + rightVel = desiredWheelSpeeds.rightMetersPerSecond + } + + /** The (x, y, theta) position of the robot on the field. */ + override var pose: Pose2d + get() = this.poseEstimator.estimatedPosition + set(pose) { + leftLeader.encoder.resetPosition(0.0) + rightLeader.encoder.resetPosition(0.0) + this.poseEstimator.resetPosition(ahrs.heading, leftLeader.position, rightLeader.position, pose) + } + + override fun stop() { + this.set(ChassisSpeeds(0.0, 0.0, 0.0)) + } + + override fun periodic() { + val currentTime = Timer.getFPGATimestamp() + + val dt = if (previousTime.isNaN()) 0.02 else currentTime - previousTime + + leftPID.setpoint = leftVel + rightPID.setpoint = rightVel + + /** Calculates the individual side voltages using a [DifferentialDriveFeedforward]. */ + val sideVoltages = feedForward.calculate( + prevLeftVel, + leftVel, + prevRightVel, + rightVel, + dt + ) + + leftLeader.setVoltage(sideVoltages.left + leftPID.calculate(prevLeftVel)) + + rightLeader.setVoltage(sideVoltages.right + rightPID.calculate(prevRightVel)) + + this.poseEstimator.update(ahrs.heading, this.leftLeader.position, this.rightLeader.position) + + previousTime = currentTime + } + + companion object { + /** Create a [DifferentialDrive] using [DifferentialConstants]. */ + private fun makeSide( + name: String, + motorId: Int, + inverted: Boolean, + encInverted: Boolean, + wpiEnc: edu.wpi.first.wpilibj.Encoder, + followers: Map + ) = + createSparkMax( + name = name + "_Side", + id = motorId, + enableBrakeMode = true, + inverted = inverted, + encCreator = QuadEncoder.creator( + wpiEnc, + DifferentialConstants.DRIVE_EXT_ENC_CPR, + DifferentialConstants.DRIVE_UPR, + DifferentialConstants.DRIVE_GEARING, + encInverted + ), + + slaveSparks = followers, + currentLimit = DifferentialConstants.DRIVE_CURRENT_LIM + ) + + fun createDifferentialDrive(ahrs: AHRS): DifferentialDrive { + return DifferentialDrive( + leftLeader = makeSide( + "Left", + DifferentialConstants.DRIVE_MOTOR_L, + inverted = false, + encInverted = false, + wpiEnc = DifferentialConstants.DRIVE_ENC_LEFT, + followers = mapOf( + DifferentialConstants.DRIVE_MOTOR_L1 to false, + DifferentialConstants.DRIVE_MOTOR_L2 to false + ) + ), + rightLeader = makeSide( + "Right", + DifferentialConstants.DRIVE_MOTOR_R, + inverted = true, + encInverted = true, + wpiEnc = DifferentialConstants.DRIVE_ENC_RIGHT, + followers = mapOf( + DifferentialConstants.DRIVE_MOTOR_R1 to false, + DifferentialConstants.DRIVE_MOTOR_R2 to false + ) + ), + ahrs, + DifferentialConstants.DRIVE_FEED_FORWARD, + { + PIDController( + DifferentialConstants.DRIVE_KP, + DifferentialConstants.DRIVE_KI, + DifferentialConstants.DRIVE_KD + ) + }, + DifferentialConstants.TRACK_WIDTH + ) + } + +// fun simOf( +// drive: DifferentialDrive, +// kV: Double, +// kA: Double, +// angleKV: Double, +// angleKA: Double, +// wheelRadius: Double +// ): DifferentialSim { +// val drivePlant = LinearSystemId.identifyDrivetrainSystem( +// kV, +// kA, +// angleKV, +// angleKA +// ) +// val driveSim = DifferentialDrivetrainSim( +// drivePlant, +// DCMotor.getNEO(3), +// DifferentialConstants.DRIVE_GEARING, +// drive.trackwidth, +// wheelRadius, +// VecBuilder.fill(0.001, 0.001, 0.001, 0.1, 0.1, 0.005, 0.005) +// ) +// return DifferentialSim(driveSim, drive.leftLeader, drive.rightLeader, drive.ahrs, drive.feedforward, drive.makeVelPID, drive.trackwidth) +// } + } +} diff --git a/src/main/kotlin/frc/team449/control/differential/DifferentialOIs.kt b/src/main/kotlin/frc/team449/control/differential/DifferentialOIs.kt new file mode 100644 index 0000000..6fe4957 --- /dev/null +++ b/src/main/kotlin/frc/team449/control/differential/DifferentialOIs.kt @@ -0,0 +1,140 @@ +package frc.team449.control.differential + +import edu.wpi.first.math.filter.SlewRateLimiter +import edu.wpi.first.math.kinematics.ChassisSpeeds +import edu.wpi.first.math.kinematics.DifferentialDriveKinematics +import edu.wpi.first.math.kinematics.DifferentialDriveWheelSpeeds +import edu.wpi.first.wpilibj.drive.DifferentialDrive.WheelSpeeds +import frc.team449.control.OI +import frc.team449.robot2023.constants.RobotConstants + +/** + * Helper class to create OIs for a differential drivetrain (arcade, curvature, + * or tank) + */ +object DifferentialOIs { + /** + * Create an OI for arcade drive. One throttle controls forward-backward speed, + * the other; controls rotation. + * + * @param drive The drivetrain + * @param xThrottle Throttle to get forward-backward movement + * @param rotThrottle Throttle to get rotation + * @param xRamp Used for limiting forward-backward acceleration + * @param rotRamp Used for limiting rotational acceleration + */ + fun createArcade( + drive: DifferentialDrive, + xThrottle: () -> Double, + rotThrottle: () -> Double, + xRamp: SlewRateLimiter, + rotRamp: SlewRateLimiter + ): OI = OI { + scaleAndApplyRamping( + edu.wpi.first.wpilibj.drive.DifferentialDrive.arcadeDriveIK( + xThrottle(), + rotThrottle(), + false + ), + drive.kinematics, + xRamp, + rotRamp + ) + } + + /** + * Create OI for curvature drive (drives like a car). One throttle controls + * forward-backward speed, like arcade, but the other controls curvature instead + * of rotation. Ramping is still applied to rotation instead of curvature. + * + * @param drive The drivetrain + * @param xThrottle Throttle to get forward-backward movement + * @param rotThrottle Throttle to get rotation + * @param xRamp Used for limiting forward-backward acceleration + * @param rotRamp Used for limiting rotational acceleration + * @param turnInPlace When this returns true, turns in place instead of turning + * like a car + */ + fun createCurvature( + drive: DifferentialDrive, + xThrottle: () -> Double, + rotThrottle: () -> Double, + xRamp: SlewRateLimiter, + rotRamp: SlewRateLimiter, + turnInPlace: () -> Boolean + ): OI = OI { + scaleAndApplyRamping( + edu.wpi.first.wpilibj.drive.DifferentialDrive.curvatureDriveIK( + xThrottle(), + rotThrottle(), + turnInPlace() + ), + drive.kinematics, + xRamp, + rotRamp + ) + } + + /** + * Create an OI for tank drive. Each throttle controls one side of the drive + * separately. Each side is also ramped separately. + * + *

+ * Shame on you if you ever use this. + * + * @param drive The drivetrain + * @param leftThrottle Throttle to get forward-backward movement + * @param rightThrottle Throttle to get rotation + * @param leftRamp Used for limiting the left side's acceleration + * @param rightRamp Used for limiting the right side's acceleration + */ + fun createTank( + drive: DifferentialDrive, + leftThrottle: () -> Double, + rightThrottle: () -> Double, + leftRamp: SlewRateLimiter, + rightRamp: SlewRateLimiter + ): OI = OI { + drive.kinematics.toChassisSpeeds( + DifferentialDriveWheelSpeeds( + leftRamp.calculate(leftThrottle() * RobotConstants.MAX_LINEAR_SPEED), + rightRamp.calculate( + rightThrottle() * RobotConstants.MAX_LINEAR_SPEED + ) + ) + ) + } + + /** + * Scales differential drive throttles from [-1, 1] to [-maxSpeed, maxSpeed], then + * applies ramping to give the final [ChassisSpeeds]. + * + *

+ * Do note that although this is given a [DifferentialDriveWheelSpeeds] + * object, the ramping isn't applied to the left and right side but to the + * linear and rotational velocity using a [ChassisSpeeds] object. + * + * @param wheelThrottles The left and right wheel throttles + * @param kinematics Kinematics object used for turning differential drive wheel + * speeds to chassis speeds + * @param ramp Used for limiting linear/forward-back acceleration + * @param rotRamp Used for limiting rotational acceleration + */ + private fun scaleAndApplyRamping( + wheelThrottles: WheelSpeeds, + kinematics: DifferentialDriveKinematics, + ramp: SlewRateLimiter, + rotRamp: SlewRateLimiter + ): ChassisSpeeds { + val leftVel = wheelThrottles.left * RobotConstants.MAX_LINEAR_SPEED + val rightVel = wheelThrottles.right * RobotConstants.MAX_LINEAR_SPEED + val chassisSpeeds = kinematics.toChassisSpeeds( + DifferentialDriveWheelSpeeds(leftVel, rightVel) + ) + return ChassisSpeeds( + ramp.calculate(chassisSpeeds.vxMetersPerSecond), + 0.0, + rotRamp.calculate(chassisSpeeds.omegaRadiansPerSecond) + ) + } +} diff --git a/src/main/kotlin/frc/team449/control/differential/DifferentialSim.kt b/src/main/kotlin/frc/team449/control/differential/DifferentialSim.kt new file mode 100644 index 0000000..6687e6f --- /dev/null +++ b/src/main/kotlin/frc/team449/control/differential/DifferentialSim.kt @@ -0,0 +1,66 @@ +package frc.team449.control.differential + +// import edu.wpi.first.math.controller.DifferentialDriveFeedforward +// import edu.wpi.first.math.controller.PIDController +// import edu.wpi.first.math.controller.SimpleMotorFeedforward +// import edu.wpi.first.math.geometry.Pose2d +// import edu.wpi.first.math.geometry.Rotation2d +// import edu.wpi.first.wpilibj.Timer +// import edu.wpi.first.wpilibj.simulation.DifferentialDrivetrainSim +// import frc.team449.system.AHRS +// import frc.team449.system.encoder.Encoder +// import frc.team449.system.motor.WrappedMotor +// +// class DifferentialSim( +// private val driveSim: DifferentialDrivetrainSim, +// leftLeader: WrappedMotor, +// rightLeader: WrappedMotor, +// ahrs: AHRS, +// private val feedForward: DifferentialDriveFeedforward, +// makeVelPID: () -> PIDController, +// trackwidth: Double +// ) : DifferentialDrive(leftLeader, rightLeader, ahrs, feedForward, makeVelPID, trackwidth) { +// +// private val leftEncSim = Encoder.SimController(leftLeader.encoder) +// private val rightEncSim = Encoder.SimController(rightLeader.encoder) +// +// private var offset: Rotation2d = Rotation2d() +// private var lastTime = Timer.getFPGATimestamp() +// +// override fun periodic() { +// val currTime = Timer.getFPGATimestamp() +// +// val dt = currTime - lastTime +// +// val leftVel = desiredWheelSpeeds.leftMetersPerSecond +// val rightVel = desiredWheelSpeeds.rightMetersPerSecond +// +// val sideVoltages = feedForward.calculate() +// +// val leftVoltage = feedForward.calculate(leftVel) + leftPID.calculate(driveSim.leftVelocityMetersPerSecond, leftVel) +// val rightVoltage = feedForward.calculate(rightVel) + rightPID.calculate(driveSim.rightVelocityMetersPerSecond, rightVel) +// +// leftEncSim.velocity = driveSim.leftVelocityMetersPerSecond +// rightEncSim.velocity = driveSim.rightVelocityMetersPerSecond +// leftEncSim.position = driveSim.leftPositionMeters +// rightEncSim.position = driveSim.rightPositionMeters +// +// driveSim.setInputs(leftVoltage, rightVoltage) +// +// driveSim.update(dt) +// this.poseEstimator.update(heading, this.leftEncSim.position, this.rightEncSim.position) +// this.lastTime = currTime +// } +// +// override var pose: Pose2d +// get() = driveSim.pose +// set(value) { +// driveSim.pose = value +// } +// +// override var heading: Rotation2d +// get() = driveSim.heading.rotateBy(offset) +// set(value) { +// offset = value - driveSim.heading +// } +// } diff --git a/src/main/kotlin/frc/team449/control/holonomic/HolonomicDrive.kt b/src/main/kotlin/frc/team449/control/holonomic/HolonomicDrive.kt new file mode 100644 index 0000000..7856dda --- /dev/null +++ b/src/main/kotlin/frc/team449/control/holonomic/HolonomicDrive.kt @@ -0,0 +1,11 @@ +package frc.team449.control.holonomic + +import frc.team449.control.DriveSubsystem + +interface HolonomicDrive : DriveSubsystem { + /** The max speed that the drivetrain can translate at, in meters per second. */ + var maxLinearSpeed: Double + + /** The max speed that the drivetrain can turn in place at, in radians per second. */ + var maxRotSpeed: Double +} diff --git a/src/main/kotlin/frc/team449/control/holonomic/HolonomicOI.kt b/src/main/kotlin/frc/team449/control/holonomic/HolonomicOI.kt new file mode 100644 index 0000000..11e3a03 --- /dev/null +++ b/src/main/kotlin/frc/team449/control/holonomic/HolonomicOI.kt @@ -0,0 +1,141 @@ +package frc.team449.control.holonomic + +import edu.wpi.first.math.MathUtil +import edu.wpi.first.math.filter.SlewRateLimiter +import edu.wpi.first.math.geometry.Rotation2d +import edu.wpi.first.math.geometry.Translation2d +import edu.wpi.first.math.kinematics.ChassisSpeeds +import edu.wpi.first.util.sendable.Sendable +import edu.wpi.first.util.sendable.SendableBuilder +import edu.wpi.first.wpilibj.Timer +import edu.wpi.first.wpilibj.XboxController +import frc.team449.control.OI +import frc.team449.robot2023.constants.RobotConstants +import java.util.function.DoubleSupplier +import kotlin.math.abs +import kotlin.math.hypot + +/** + * Create an OI for controlling a holonomic drivetrain (probably swerve). + * The x and y axes on one joystick are used to control x and y velocity (m/s), + * while the x axis on another joystick is used to control rotational velocity (m/s). + *

The magnitude of the acceleration is clamped + *

Note that the joystick's X + * axis corresponds to the robot's/field's Y and vice versa + * + * @param drive The drivetrain this OI is controlling + * @param xThrottle The Y axis of the strafing joystick + * @param yThrottle The X axis of the strafing joystick + * @param rotThrottle The X axis of the rotating joystick + * @param rotRamp Used to ramp angular velocity + * @param maxAccel Max accel, used for ramping + * @param fieldOriented Whether the OI x and y translation should + * be relative to the field rather than relative to the robot. This better be true. + */ +class HolonomicOI( + private val drive: HolonomicDrive, + private val xThrottle: DoubleSupplier, + private val yThrottle: DoubleSupplier, + private val rotThrottle: DoubleSupplier, + private val rotRamp: SlewRateLimiter, + private val maxAccel: Double, + private val fieldOriented: () -> Boolean +) : OI, Sendable { + + /** Previous X velocity (scaled and clamped). */ + private var prevX = 0.0 + + /** Previous Y velocity (scaled and clamped) */ + private var prevY = 0.0 + + private var prevTime = Double.NaN + + private var dx = 0.0 + private var dy = 0.0 + private var magAcc = 0.0 + private var dt = 0.0 + private var magAccClamped = 0.0 + + /** + * @return The [ChassisSpeeds] for the given x, y and + * rotation input from the joystick */ + override fun get(): ChassisSpeeds { + val currTime = Timer.getFPGATimestamp() + if (this.prevTime.isNaN()) { + this.prevTime = currTime - 0.02 + } + this.dt = currTime - prevTime + this.prevTime = currTime + + val xScaled = xThrottle.asDouble * drive.maxLinearSpeed + val yScaled = yThrottle.asDouble * drive.maxLinearSpeed + + // Clamp the acceleration + this.dx = xScaled - this.prevX + this.dy = yScaled - this.prevY + this.magAcc = hypot(dx / dt, dy / dt) + this.magAccClamped = MathUtil.clamp(magAcc, -this.maxAccel, this.maxAccel) + + // Scale the change in x and y the same as the acceleration + val factor = if (magAcc == 0.0) 0.0 else magAccClamped / magAcc + val dxClamped = dx * factor + val dyClamped = dy * factor + val xClamped = prevX + dxClamped + val yClamped = prevY + dyClamped + + this.prevX = xClamped + this.prevY = yClamped + + val rotRaw = rotThrottle.asDouble + val rotScaled = rotRamp.calculate(rotRaw * drive.maxRotSpeed) + + // translation velocity vector + val vel = Translation2d(xClamped, yClamped) + + return if (this.fieldOriented()) { + /** Quick fix for the velocity skewing towards the direction of rotation + * by rotating it with offset proportional to how much we are rotating + **/ + vel.rotateBy(Rotation2d(-rotScaled * dt / 2)) + ChassisSpeeds.fromFieldRelativeSpeeds( + vel.x, + vel.y, + rotScaled, + drive.heading + ) + } else { + ChassisSpeeds( + vel.x, + vel.y, + rotScaled + ) + } + } + + override fun initSendable(builder: SendableBuilder) { + builder.addDoubleProperty("currX", this.xThrottle::getAsDouble, null) + builder.addDoubleProperty("currY", this.yThrottle::getAsDouble, null) + builder.addDoubleProperty("prevX", { this.prevX }, null) + builder.addDoubleProperty("prevY", { this.prevY }, null) + builder.addDoubleProperty("dx", { this.dx }, null) + builder.addDoubleProperty("dy", { this.dy }, null) + builder.addDoubleProperty("dt", { this.dt }, null) + builder.addDoubleProperty("magAcc", { this.magAcc }, null) + builder.addDoubleProperty("magAccClamped", { this.magAccClamped }, null) + builder.addStringProperty("speeds", { this.get().toString() }, null) + } + + companion object { + fun createHolonomicOI(drive: HolonomicDrive, driveController: XboxController): HolonomicOI { + return HolonomicOI( + drive, + { if (abs(driveController.leftY) < RobotConstants.TRANSLATION_DEADBAND) .0 else -driveController.leftY }, + { if (abs(driveController.leftX) < RobotConstants.TRANSLATION_DEADBAND) .0 else -driveController.leftX }, + { if (abs(driveController.getRawAxis(4)) < RobotConstants.ROTATION_DEADBAND) .0 else -driveController.getRawAxis(4) }, + SlewRateLimiter(RobotConstants.ROT_RATE_LIMIT, RobotConstants.NEG_ROT_RATE_LIM, 0.0), + RobotConstants.MAX_ACCEL, + { true } + ) + } + } +} diff --git a/src/main/kotlin/frc/team449/control/holonomic/MecanumDrive.kt b/src/main/kotlin/frc/team449/control/holonomic/MecanumDrive.kt new file mode 100644 index 0000000..ea35e7c --- /dev/null +++ b/src/main/kotlin/frc/team449/control/holonomic/MecanumDrive.kt @@ -0,0 +1,204 @@ +package frc.team449.control.holonomic + +import edu.wpi.first.math.MatBuilder +import edu.wpi.first.math.Nat +import edu.wpi.first.math.controller.PIDController +import edu.wpi.first.math.controller.SimpleMotorFeedforward +import edu.wpi.first.math.estimator.MecanumDrivePoseEstimator +import edu.wpi.first.math.geometry.Pose2d +import edu.wpi.first.math.geometry.Translation2d +import edu.wpi.first.math.kinematics.ChassisSpeeds +import edu.wpi.first.math.kinematics.MecanumDriveKinematics +import edu.wpi.first.math.kinematics.MecanumDriveWheelPositions +import edu.wpi.first.math.kinematics.MecanumDriveWheelSpeeds +import edu.wpi.first.wpilibj.Timer +import edu.wpi.first.wpilibj2.command.SubsystemBase +import frc.team449.control.vision.VisionSubsystem +import frc.team449.robot2023.constants.RobotConstants +import frc.team449.robot2023.constants.drives.MecanumConstants +import frc.team449.robot2023.constants.vision.VisionConstants +import frc.team449.system.AHRS +import frc.team449.system.encoder.NEOEncoder +import frc.team449.system.motor.WrappedMotor +import frc.team449.system.motor.createSparkMax + +/** + * @param frontLeftMotor the front left motor + * @param frontRightMotor the front right motor + * @param backLeftMotor the back left motor + * @param backRightMotor the back right motor + * @param frontLeftLocation the offset of the front left wheel to the center of the robot + * @param frontRightLocation the offset of the front right wheel to the center of the robot + * @param backLeftLocation the offset of the back left wheel to the center of the robot + * @param backRightLocation the offset of the back right wheel to the center of the robot + * @param maxLinearSpeed the maximum translation speed of the chassis. + * @param maxRotSpeed the maximum rotation speed of the chassis + * @param feedForward the SimpleMotorFeedforward for mecanum + * @param controller the PIDController for the robot + */ +open class MecanumDrive( + private val frontLeftMotor: WrappedMotor, + private val frontRightMotor: WrappedMotor, + private val backLeftMotor: WrappedMotor, + private val backRightMotor: WrappedMotor, + frontLeftLocation: Translation2d, + frontRightLocation: Translation2d, + backLeftLocation: Translation2d, + backRightLocation: Translation2d, + private val ahrs: AHRS, + override var maxLinearSpeed: Double, + override var maxRotSpeed: Double, + private val feedForward: SimpleMotorFeedforward, + private val controller: () -> PIDController, + private val cameras: List = mutableListOf() +) : HolonomicDrive, SubsystemBase() { + + private val flController = controller() + private val frController = controller() + private val blController = controller() + private val brController = controller() + + private var lastTime = Timer.getFPGATimestamp() + + val kinematics = MecanumDriveKinematics( + frontLeftLocation, + frontRightLocation, + backLeftLocation, + backRightLocation + ) + + private val poseEstimator = MecanumDrivePoseEstimator( + kinematics, + ahrs.heading, + getPositions(), + RobotConstants.INITIAL_POSE, + MatBuilder(Nat.N3(), Nat.N1()).fill(.005, .005, .0005), // [x, y, theta] other estimates + MatBuilder(Nat.N3(), Nat.N1()).fill(.005, .005, .0005) // [x, y, theta] vision estimates + ) + + override var pose: Pose2d + get() { + return this.poseEstimator.estimatedPosition + } + set(value) { + this.poseEstimator.resetPosition(ahrs.heading, getPositions(), value) + } + + private var desiredWheelSpeeds = MecanumDriveWheelSpeeds() + + override fun set(desiredSpeeds: ChassisSpeeds) { + desiredWheelSpeeds = kinematics.toWheelSpeeds(desiredSpeeds) + desiredWheelSpeeds.desaturate(MecanumConstants.MAX_ATTAINABLE_WHEEL_SPEED) + } + + override fun stop() { + this.set(ChassisSpeeds(0.0, 0.0, 0.0)) + } + + override fun periodic() { + val currTime = Timer.getFPGATimestamp() + + val frontLeftPID = flController.calculate(frontLeftMotor.velocity, desiredWheelSpeeds.frontLeftMetersPerSecond) + val frontRightPID = frController.calculate(frontRightMotor.velocity, desiredWheelSpeeds.frontRightMetersPerSecond) + val backLeftPID = blController.calculate(backLeftMotor.velocity, desiredWheelSpeeds.rearLeftMetersPerSecond) + val backRightPID = brController.calculate(backRightMotor.velocity, desiredWheelSpeeds.rearRightMetersPerSecond) + + val frontLeftFF = feedForward.calculate( + desiredWheelSpeeds.frontLeftMetersPerSecond + ) + val frontRightFF = feedForward.calculate( + desiredWheelSpeeds.frontRightMetersPerSecond + ) + val backLeftFF = feedForward.calculate( + desiredWheelSpeeds.rearLeftMetersPerSecond + ) + val backRightFF = feedForward.calculate( + desiredWheelSpeeds.rearRightMetersPerSecond + ) + + frontLeftMotor.setVoltage(frontLeftPID + frontLeftFF) + frontRightMotor.setVoltage(frontRightPID + frontRightFF) + backLeftMotor.setVoltage(backLeftPID + backLeftFF) + backRightMotor.setVoltage(backRightPID + backRightFF) + + if (cameras.isNotEmpty()) localize() + + this.poseEstimator.update( + ahrs.heading, + getPositions() + ) + + lastTime = currTime + } + + /** + * @return the position readings of the wheels bundled into one object (meters) + */ + private fun getPositions(): MecanumDriveWheelPositions = + MecanumDriveWheelPositions( + frontLeftMotor.position, + frontRightMotor.position, + backLeftMotor.position, + backRightMotor.position + ) + + /** + * @return the velocity readings of the wheels bundled into one object (meters/s) + */ + private fun getSpeeds(): MecanumDriveWheelSpeeds = + MecanumDriveWheelSpeeds( + frontLeftMotor.velocity, + frontRightMotor.velocity, + backLeftMotor.velocity, + backRightMotor.velocity + ) + + private fun localize() { + for (camera in cameras) { + val result = camera.estimatedPose(Pose2d(pose.x, pose.y, ahrs.heading)) + if (result.isPresent) { + poseEstimator.addVisionMeasurement( + result.get().estimatedPose.toPose2d(), + result.get().timestampSeconds + ) + } + } + } + + companion object { + + /** Helper method to create a motor for each wheel */ + private fun createCorner(name: String, motorID: Int, inverted: Boolean): WrappedMotor { + return createSparkMax( + name, + motorID, + NEOEncoder.creator( + MecanumConstants.DRIVE_UPR, + MecanumConstants.DRIVE_GEARING + ), + inverted = inverted, + currentLimit = MecanumConstants.CURRENT_LIM + ) + } + + /** Create a new Mecanum Drive from DriveConstants */ + fun createMecanum(ahrs: AHRS): MecanumDrive { + return MecanumDrive( + createCorner("frontLeft", MecanumConstants.DRIVE_MOTOR_FL, false), + createCorner("frontRight", MecanumConstants.DRIVE_MOTOR_FR, true), + createCorner("backLeft", MecanumConstants.DRIVE_MOTOR_BL, false), + createCorner("backRight", MecanumConstants.DRIVE_MOTOR_BR, true), + Translation2d(MecanumConstants.WHEELBASE / 2, MecanumConstants.TRACKWIDTH / 2), + Translation2d(MecanumConstants.WHEELBASE / 2, -MecanumConstants.TRACKWIDTH / 2), + Translation2d(-MecanumConstants.WHEELBASE / 2, MecanumConstants.TRACKWIDTH / 2), + Translation2d(-MecanumConstants.WHEELBASE / 2, -MecanumConstants.TRACKWIDTH / 2), + ahrs, + RobotConstants.MAX_LINEAR_SPEED, + RobotConstants.MAX_ROT_SPEED, + SimpleMotorFeedforward(MecanumConstants.DRIVE_KS, MecanumConstants.DRIVE_KV, MecanumConstants.DRIVE_KA), + { PIDController(MecanumConstants.DRIVE_KP, MecanumConstants.DRIVE_KI, MecanumConstants.DRIVE_KD) }, + VisionConstants.ESTIMATORS + ) + } + } +} diff --git a/src/main/kotlin/frc/team449/control/holonomic/SwerveDrive.kt b/src/main/kotlin/frc/team449/control/holonomic/SwerveDrive.kt new file mode 100644 index 0000000..0ea3208 --- /dev/null +++ b/src/main/kotlin/frc/team449/control/holonomic/SwerveDrive.kt @@ -0,0 +1,482 @@ +package frc.team449.control.holonomic + +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.estimator.SwerveDrivePoseEstimator +import edu.wpi.first.math.geometry.Pose2d +import edu.wpi.first.math.geometry.Rotation2d +import edu.wpi.first.math.geometry.Transform2d +import edu.wpi.first.math.geometry.Translation2d +import edu.wpi.first.math.kinematics.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.util.sendable.SendableBuilder +import edu.wpi.first.wpilibj.DriverStation +import edu.wpi.first.wpilibj.RobotBase.isReal +import edu.wpi.first.wpilibj.smartdashboard.Field2d +import edu.wpi.first.wpilibj2.command.SubsystemBase +import frc.team449.control.vision.VisionSubsystem +import frc.team449.robot2023.constants.RobotConstants +import frc.team449.robot2023.constants.drives.SwerveConstants +import frc.team449.robot2023.constants.vision.VisionConstants +import frc.team449.system.AHRS +import frc.team449.system.encoder.AbsoluteEncoder +import frc.team449.system.encoder.NEOEncoder +import frc.team449.system.motor.createSparkMax +import kotlin.math.* + +/** + * A Swerve Drive chassis. + * @param modules An array of [SwerveModule]s that are on the drivetrain. + * @param ahrs The gyro that is mounted on the chassis. + * @param maxLinearSpeed The maximum translation speed of the chassis. + * @param maxRotSpeed The maximum rotation speed of the chassis. + * @param cameras The cameras that help estimate the robot's pose. + * @param field The SmartDashboard [Field2d] widget that shows the robot's pose. + */ +open class SwerveDrive( + protected val modules: List, + protected val ahrs: AHRS, + override var maxLinearSpeed: Double, + override var maxRotSpeed: Double, + protected val cameras: List = mutableListOf(), + protected val field: Field2d +) : SubsystemBase(), HolonomicDrive { + + /** Vision statistics */ + protected var numTargets = DoubleArray(cameras.size) + protected var tagDistance = DoubleArray(cameras.size) + protected var avgAmbiguity = DoubleArray(cameras.size) + protected var heightError = DoubleArray(cameras.size) + protected var usedVision = BooleanArray(cameras.size) + + /** The kinematics that convert [ChassisSpeeds] into multiple [SwerveModuleState] objects. */ + protected val kinematics = SwerveDriveKinematics( + *this.modules.map { it.location }.toTypedArray() + ) + + /** The current speed of the robot's drive. */ + var currentSpeeds = ChassisSpeeds() + + /** Current estimated vision pose */ + var visionPose = Pose2d() + + /** Pose estimator that estimates the robot's position as a [Pose2d]. */ + protected val poseEstimator = SwerveDrivePoseEstimator( + kinematics, + ahrs.heading, + getPositions(), + RobotConstants.INITIAL_POSE, + VisionConstants.ENCODER_TRUST, + VisionConstants.MULTI_TAG_TRUST + ) + + var desiredSpeeds: ChassisSpeeds = ChassisSpeeds() + + protected var maxSpeed: Double = 0.0 + + override fun set(desiredSpeeds: ChassisSpeeds) { + this.desiredSpeeds = desiredSpeeds + + // Converts the desired [ChassisSpeeds] into an array of [SwerveModuleState]. + val desiredModuleStates = + this.kinematics.toSwerveModuleStates(this.desiredSpeeds) + + // Scale down module speed if a module is going faster than the max speed, and prevent early desaturation. + normalizeDrive(desiredModuleStates, desiredSpeeds) +// SwerveDriveKinematics.desaturateWheelSpeeds( +// desiredModuleStates, +// SwerveConstants.MAX_ATTAINABLE_MK4I_SPEED +// ) + + for (i in this.modules.indices) { + this.modules[i].state = desiredModuleStates[i] + } + + for (module in modules) + module.update() + } + + // TODO: Do you notice a difference with this normalize function? + private fun normalizeDrive(desiredStates: Array, speeds: ChassisSpeeds) { + val translationalK: Double = hypot(speeds.vxMetersPerSecond, speeds.vyMetersPerSecond) / RobotConstants.MAX_LINEAR_SPEED + val rotationalK: Double = abs(speeds.omegaRadiansPerSecond) / RobotConstants.MAX_ROT_SPEED + val k = max(translationalK, rotationalK) + + // Find how fast the fastest spinning drive motor is spinning + var realMaxSpeed = 0.0 + for (moduleState in desiredStates) { + realMaxSpeed = max(realMaxSpeed, abs(moduleState.speedMetersPerSecond)) + } + + val scale = + if (realMaxSpeed > 0 && k < 1) + k * SwerveConstants.MAX_ATTAINABLE_MK4I_SPEED / realMaxSpeed + else if (realMaxSpeed > 0) + SwerveConstants.MAX_ATTAINABLE_MK4I_SPEED / realMaxSpeed + else + 1.0 + + for (moduleState in desiredStates) { + moduleState.speedMetersPerSecond *= scale + } + } + + fun setVoltage(volts: Double) { + modules.forEach { + it.setVoltage(volts) + } + } + + fun getModuleVel(): Double { + var totalVel = 0.0 + modules.forEach { totalVel += it.state.speedMetersPerSecond } + return totalVel / modules.size + } + + /** The measured pitch of the robot from the gyro sensor. */ + val pitch: Rotation2d + get() = Rotation2d(MathUtil.angleModulus(ahrs.pitch.radians)) + + /** The measured roll of the robot from the gyro sensor. */ + val roll: Rotation2d + get() = Rotation2d(MathUtil.angleModulus(ahrs.roll.radians)) + + /** The (x, y, theta) position of the robot on the field. */ + override var pose: Pose2d + get() = this.poseEstimator.estimatedPosition + set(value) { + this.poseEstimator.resetPosition( + ahrs.heading, + getPositions(), + value + ) + } + + override fun periodic() { + // Updates the robot's currentSpeeds. + currentSpeeds = kinematics.toChassisSpeeds( + modules[0].state, + modules[1].state, + modules[2].state, + modules[3].state + ) + + val transVel = hypot(currentSpeeds.vxMetersPerSecond, currentSpeeds.vyMetersPerSecond) + if (transVel > maxSpeed) maxSpeed = transVel + + // Update the robot's pose using the gyro heading and the SwerveModulePositions of each module. + this.poseEstimator.update( + ahrs.heading, + getPositions() + ) + + if (cameras.isNotEmpty()) localize() + + // Sets the robot's pose and individual module rotations on the SmartDashboard [Field2d] widget. + setRobotPose() + } + + /** Stops the robot's drive. */ + override fun stop() { + this.set(ChassisSpeeds(0.0, 0.0, 0.0)) + } + + /** @return An array of [SwerveModulePosition] for each module, containing distance and angle. */ + protected fun getPositions(): Array { + return Array(modules.size) { i -> modules[i].position } + } + + /** @return An array of [SwerveModuleState] for each module, containing speed and angle. */ + private fun getStates(): Array { + return Array(modules.size) { i -> modules[i].state } + } + + protected fun setRobotPose() { + this.field.robotPose = this.pose + this.field.getObject("FL").pose = this.pose.plus(Transform2d(Translation2d(SwerveConstants.WHEELBASE / 2, SwerveConstants.TRACKWIDTH / 2), this.getPositions()[0].angle)) + this.field.getObject("FR").pose = this.pose.plus(Transform2d(Translation2d(SwerveConstants.WHEELBASE / 2, -SwerveConstants.TRACKWIDTH / 2), this.getPositions()[1].angle)) + this.field.getObject("BL").pose = this.pose.plus(Transform2d(Translation2d(-SwerveConstants.WHEELBASE / 2, SwerveConstants.TRACKWIDTH / 2), this.getPositions()[2].angle)) + this.field.getObject("BR").pose = this.pose.plus(Transform2d(Translation2d(-SwerveConstants.WHEELBASE / 2, -SwerveConstants.TRACKWIDTH / 2), this.getPositions()[0].angle)) + } + + protected open fun localize() = try { + for ((index, camera) in cameras.withIndex()) { + val result = camera.estimatedPose(Pose2d(pose.x, pose.y, ahrs.heading)) + if (result.isPresent) { + val presentResult = result.get() + numTargets[index] = presentResult.targetsUsed.size.toDouble() + tagDistance[index] = 0.0 + avgAmbiguity[index] = 0.0 + heightError[index] = abs(presentResult.estimatedPose.z - camera.robotToCam.z) + + for (tag in presentResult.targetsUsed) { + val tagPose = camera.estimator.fieldTags.getTagPose(tag.fiducialId) + if (tagPose.isPresent) { + val estimatedToTag = presentResult.estimatedPose.minus(tagPose.get()) + tagDistance[index] += sqrt(estimatedToTag.x.pow(2) + estimatedToTag.y.pow(2)) / numTargets[index] + avgAmbiguity[index] = tag.poseAmbiguity / numTargets[index] + } else { + tagDistance[index] = Double.MAX_VALUE + avgAmbiguity[index] = Double.MAX_VALUE + break + } + } + + visionPose = presentResult.estimatedPose.toPose2d() + + if (presentResult.timestampSeconds > 0 && + avgAmbiguity[index] <= VisionConstants.MAX_AMBIGUITY && + numTargets[index] < 2 && tagDistance[index] <= VisionConstants.MAX_DISTANCE_SINGLE_TAG || + numTargets[index] >= 2 && tagDistance[index] <= VisionConstants.MAX_DISTANCE_MULTI_TAG * (1 + (numTargets[index] - 2) * VisionConstants.TAG_MULTIPLIER) && + heightError[index] < VisionConstants.MAX_HEIGHT_ERR_METERS + ) { + poseEstimator.addVisionMeasurement( + visionPose, + presentResult.timestampSeconds, + camera.getEstimationStdDevs(numTargets[index].toInt(), tagDistance[index]) + ) + usedVision[index] = true + } else { + usedVision[index] = false + } + } + } + } catch (e: Error) { + DriverStation.reportError( + "!!!!!!!!! VISION ERROR !!!!!!!", + e.stackTrace + ) + } + + override fun initSendable(builder: SendableBuilder) { + builder.publishConstString("1.0", "Poses and ChassisSpeeds") + builder.addDoubleArrayProperty("1.1 Estimated Pose", { doubleArrayOf(pose.x, pose.y, pose.rotation.radians) }, null) + builder.addDoubleArrayProperty("1.2 Vision Pose", { doubleArrayOf(visionPose.x, visionPose.y, visionPose.rotation.radians) }, null) + builder.addDoubleArrayProperty("1.3 Current Chassis Speeds", { doubleArrayOf(currentSpeeds.vxMetersPerSecond, currentSpeeds.vyMetersPerSecond, currentSpeeds.omegaRadiansPerSecond) }, null) + builder.addDoubleArrayProperty("1.4 Desired Chassis Speeds", { doubleArrayOf(desiredSpeeds.vxMetersPerSecond, desiredSpeeds.vyMetersPerSecond, desiredSpeeds.omegaRadiansPerSecond) }, null) + builder.addDoubleProperty("1.5 Max Recorded Speed", { maxSpeed }, null) + + builder.publishConstString("2.0", "Vision Stats") + builder.addBooleanArrayProperty("2.1 Used Last Vision Estimate?", { usedVision }, null) + builder.addDoubleArrayProperty("2.2 Number of Targets", { numTargets }, null) + builder.addDoubleArrayProperty("2.3 Avg Tag Distance", { tagDistance }, null) + builder.addDoubleArrayProperty("2.4 Average Ambiguity", { avgAmbiguity }, null) + builder.addDoubleArrayProperty("2.5 Cam Height Error", { heightError }, null) + + builder.publishConstString("3.0", "Steering Rot (Std Order FL, FR, BL, BR)") + builder.addDoubleArrayProperty( + "3.1 Current Rotation", + { DoubleArray(modules.size) { index -> modules[index].position.angle.rotations } }, + null + ) + builder.addDoubleArrayProperty( + "3.2 Desired Rotation", + { DoubleArray(modules.size) { index -> modules[index].desiredState.angle.rotations } }, + null + ) + + builder.publishConstString("4.0", "Module Driving Speeds (Std Order FL, FR, BL, BR)") + builder.addDoubleArrayProperty( + "4.1 Desired Speed", + { DoubleArray(modules.size) { index -> modules[index].desiredState.speedMetersPerSecond } }, + null + ) + builder.addDoubleArrayProperty( + "4.2 Current Speeds", + { DoubleArray(modules.size) { index -> modules[index].state.speedMetersPerSecond } }, + null + ) + + builder.publishConstString("5.0", "Last Module Voltages (Standard Order, FL, FR, BL, BR)") + builder.addDoubleArrayProperty( + "5.1 Driving", + { DoubleArray(modules.size) { index -> modules[index].lastDrivingVoltage() } }, + null + ) + builder.addDoubleArrayProperty( + "5.2 Steering", + { DoubleArray(modules.size) { index -> modules[index].lastSteeringVoltage() } }, + null + ) + + builder.publishConstString("6.0", "AHRS Values") + builder.addDoubleProperty("6.1 Heading Degrees", { ahrs.heading.degrees }, null) + builder.addDoubleProperty("6.2 Pitch Degrees", { ahrs.pitch.degrees }, null) + builder.addDoubleProperty("6.3 Roll Degrees", { ahrs.roll.degrees }, null) + builder.addDoubleProperty("6.4 Angular X Vel", { ahrs.angularXVel() }, null) + + // Note: You should also tune UPR too + builder.publishConstString("7.0", "Tuning Values") + builder.addDoubleProperty("7.1 FL Drive P", { modules[0].driveController.p }, { value -> modules[0].driveController.p = value }) + builder.addDoubleProperty("7.2 FL Drive D", { modules[0].driveController.d }, { value -> modules[0].driveController.d = value }) + builder.addDoubleProperty("7.3 FL Turn P", { modules[0].turnController.p }, { value -> modules[0].turnController.p = value }) + builder.addDoubleProperty("7.4 FL Turn D", { modules[0].turnController.d }, { value -> modules[0].turnController.d = value }) + builder.addDoubleProperty("7.5 FR Drive P", { modules[1].driveController.p }, { value -> modules[1].driveController.p = value }) + builder.addDoubleProperty("7.6 FR Drive D", { modules[1].driveController.d }, { value -> modules[1].driveController.d = value }) + builder.addDoubleProperty("7.8 FR Turn P", { modules[1].turnController.p }, { value -> modules[1].turnController.p = value }) + builder.addDoubleProperty("7.9 FR Turn D", { modules[1].turnController.d }, { value -> modules[1].turnController.d = value }) + builder.addDoubleProperty("7.10 BL Drive P", { modules[2].driveController.p }, { value -> modules[2].driveController.p = value }) + builder.addDoubleProperty("7.11 BL Drive D", { modules[2].driveController.d }, { value -> modules[2].driveController.d = value }) + builder.addDoubleProperty("7.12 BL Turn P", { modules[2].turnController.p }, { value -> modules[2].turnController.p = value }) + builder.addDoubleProperty("7.13 BL Turn D", { modules[2].turnController.d }, { value -> modules[2].turnController.d = value }) + builder.addDoubleProperty("7.14 BR Drive P", { modules[3].driveController.p }, { value -> modules[3].driveController.p = value }) + builder.addDoubleProperty("7.15 BR Drive D", { modules[3].driveController.d }, { value -> modules[3].driveController.d = value }) + builder.addDoubleProperty("7.16 BR Turn P", { modules[3].turnController.p }, { value -> modules[3].turnController.p = value }) + builder.addDoubleProperty("7.17 BR Turn D", { modules[3].turnController.d }, { value -> modules[3].turnController.d = value }) + } + + companion object { + /** Create a [SwerveDrive] using [SwerveConstants]. */ + fun createSwerve(ahrs: AHRS, field: Field2d): SwerveDrive { + val driveMotorController = { PIDController(SwerveConstants.DRIVE_KP, SwerveConstants.DRIVE_KI, SwerveConstants.DRIVE_KD) } + val turnMotorController = { PIDController(SwerveConstants.TURN_KP, SwerveConstants.TURN_KI, SwerveConstants.TURN_KD) } + val driveFeedforward = SimpleMotorFeedforward(SwerveConstants.DRIVE_KS, SwerveConstants.DRIVE_KV, SwerveConstants.DRIVE_KA) + val modules = listOf( + SwerveModule.create( + "FLModule", + makeDrivingMotor( + "FL", + SwerveConstants.DRIVE_MOTOR_FL, + inverted = false + ), + makeTurningMotor( + "FL", + SwerveConstants.TURN_MOTOR_FL, + inverted = true, + sensorPhase = false, + SwerveConstants.TURN_ENC_CHAN_FL, + SwerveConstants.TURN_ENC_OFFSET_FL + ), + driveMotorController(), + turnMotorController(), + driveFeedforward, + Translation2d(SwerveConstants.WHEELBASE / 2, SwerveConstants.TRACKWIDTH / 2) + ), + SwerveModule.create( + "FRModule", + makeDrivingMotor( + "FR", + SwerveConstants.DRIVE_MOTOR_FR, + inverted = false + ), + makeTurningMotor( + "FR", + SwerveConstants.TURN_MOTOR_FR, + inverted = true, + sensorPhase = false, + SwerveConstants.TURN_ENC_CHAN_FR, + SwerveConstants.TURN_ENC_OFFSET_FR + ), + driveMotorController(), + turnMotorController(), + driveFeedforward, + Translation2d(SwerveConstants.WHEELBASE / 2, -SwerveConstants.TRACKWIDTH / 2) + ), + SwerveModule.create( + "BLModule", + makeDrivingMotor( + "BL", + SwerveConstants.DRIVE_MOTOR_BL, + inverted = false + ), + makeTurningMotor( + "BL", + SwerveConstants.TURN_MOTOR_BL, + inverted = true, + sensorPhase = false, + SwerveConstants.TURN_ENC_CHAN_BL, + SwerveConstants.TURN_ENC_OFFSET_BL + ), + driveMotorController(), + turnMotorController(), + driveFeedforward, + Translation2d(-SwerveConstants.WHEELBASE / 2, SwerveConstants.TRACKWIDTH / 2) + ), + SwerveModule.create( + "BRModule", + makeDrivingMotor( + "BR", + SwerveConstants.DRIVE_MOTOR_BR, + inverted = false + ), + makeTurningMotor( + "BR", + SwerveConstants.TURN_MOTOR_BR, + inverted = true, + sensorPhase = false, + SwerveConstants.TURN_ENC_CHAN_BR, + SwerveConstants.TURN_ENC_OFFSET_BR + ), + driveMotorController(), + turnMotorController(), + driveFeedforward, + Translation2d(-SwerveConstants.WHEELBASE / 2, -SwerveConstants.TRACKWIDTH / 2) + ) + ) + return if (isReal()) { + SwerveDrive( + modules, + ahrs, + RobotConstants.MAX_LINEAR_SPEED, + RobotConstants.MAX_ROT_SPEED, + VisionConstants.ESTIMATORS, + field + ) + } else { + SwerveSim( + modules, + ahrs, + RobotConstants.MAX_LINEAR_SPEED, + RobotConstants.MAX_ROT_SPEED, + VisionConstants.ESTIMATORS, + field + ) + } + } + + /** Helper to make turning motors for swerve. */ + private fun makeDrivingMotor( + name: String, + motorId: Int, + inverted: Boolean + ) = + createSparkMax( + name = name + "Drive", + id = motorId, + enableBrakeMode = true, + inverted = inverted, + encCreator = + NEOEncoder.creator( + SwerveConstants.DRIVE_UPR, + SwerveConstants.DRIVE_GEARING + ), + currentLimit = SwerveConstants.DRIVE_CURRENT_LIM + ) + + /** Helper to make turning motors for swerve. */ + private fun makeTurningMotor( + name: String, + motorId: Int, + inverted: Boolean, + sensorPhase: Boolean, + encoderChannel: Int, + offset: Double + ) = + createSparkMax( + name = name + "Turn", + id = motorId, + enableBrakeMode = false, + inverted = inverted, + encCreator = AbsoluteEncoder.creator( + encoderChannel, + offset, + SwerveConstants.TURN_UPR, + sensorPhase + ), + currentLimit = SwerveConstants.STEERING_CURRENT_LIM + ) + } +} diff --git a/src/main/kotlin/frc/team449/control/holonomic/SwerveModule.kt b/src/main/kotlin/frc/team449/control/holonomic/SwerveModule.kt new file mode 100644 index 0000000..82a7578 --- /dev/null +++ b/src/main/kotlin/frc/team449/control/holonomic/SwerveModule.kt @@ -0,0 +1,206 @@ +package frc.team449.control.holonomic + +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.geometry.Translation2d +import edu.wpi.first.math.kinematics.SwerveModulePosition +import edu.wpi.first.math.kinematics.SwerveModuleState +import edu.wpi.first.wpilibj.RobotBase +import edu.wpi.first.wpilibj.Timer +import frc.team449.robot2023.constants.drives.SwerveConstants +import frc.team449.system.encoder.Encoder +import frc.team449.system.motor.WrappedMotor +import kotlin.math.PI +import kotlin.math.abs +import kotlin.math.sign + +/** + * Controls a Swerve Module. + * @param name The name of the module (used for logging). + * @param drivingMotor The motor that controls the speed of the module. + * @param turningMotor The motor that controls the angle of the module + * @param driveController The velocity control for speed of the module + * @param turnController The position control for the angle of the module + * @param driveFeedforward The voltage predicting equation for a given speed of the module. + * @param location The location of the module in reference to the center of the robot. + * NOTE: In relation to the robot [+X is forward, +Y is left, and +THETA is Counter Clock-Wise]. + */ +open class SwerveModule( + private val name: String, + private val drivingMotor: WrappedMotor, + private val turningMotor: WrappedMotor, + val driveController: PIDController, + val turnController: PIDController, + private val driveFeedforward: SimpleMotorFeedforward, + val location: Translation2d +) { + init { + turnController.enableContinuousInput(.0, 2 * PI) + driveController.reset() + turnController.reset() + } + + val desiredState = SwerveModuleState( + 0.0, + Rotation2d() + ) + + /** The module's [SwerveModuleState], containing speed and angle. */ + open var state: SwerveModuleState + get() { + return SwerveModuleState( + drivingMotor.velocity, + Rotation2d(turningMotor.position) + ) + } + set(desState) { + if (abs(desState.speedMetersPerSecond) < .001) { + stop() + return + } + /** Ensure the module doesn't turn more than 90 degrees. */ + val optimizedState = SwerveModuleState.optimize( + desState, + Rotation2d(turningMotor.position) + ) + + turnController.setpoint = optimizedState.angle.radians + driveController.setpoint = optimizedState.speedMetersPerSecond + desiredState.speedMetersPerSecond = optimizedState.speedMetersPerSecond + desiredState.angle = optimizedState.angle + } + + /** The module's [SwerveModulePosition], containing distance and angle. */ + open val position: SwerveModulePosition + get() { + return SwerveModulePosition( + drivingMotor.position, + Rotation2d(turningMotor.position) + ) + } + + fun setVoltage(volts: Double) { + driveController.setpoint = 0.0 + desiredState.speedMetersPerSecond = 0.0 + turnController.setpoint = 0.0 + + turningMotor.setVoltage(turnController.calculate(turningMotor.position)) + drivingMotor.setVoltage(volts) + } + + fun lastDrivingVoltage(): Double { + return drivingMotor.lastVoltage + } + + fun lastSteeringVoltage(): Double { + return turningMotor.lastVoltage + } + + /** Set module speed to zero but keep module angle the same. */ + fun stop() { + turnController.setpoint = turningMotor.position + desiredState.speedMetersPerSecond = 0.0 + } + + open fun update() { + /** CONTROL speed of module */ + val drivePid = driveController.calculate( + drivingMotor.velocity + ) + val driveFF = driveFeedforward.calculate( + desiredState.speedMetersPerSecond + ) + drivingMotor.setVoltage(drivePid + driveFF) + + /** CONTROL direction of module */ + val turnPid = turnController.calculate( + turningMotor.position + ) + + turningMotor.set( + turnPid + + sign(desiredState.angle.radians - turningMotor.position) * + SwerveConstants.STEER_KS + ) + } + + companion object { + /** Create a real or simulated [SwerveModule] based on the simulation status of the robot. */ + fun create( + name: String, + drivingMotor: WrappedMotor, + turningMotor: WrappedMotor, + driveController: PIDController, + turnController: PIDController, + driveFeedforward: SimpleMotorFeedforward, + location: Translation2d + ): SwerveModule { + if (RobotBase.isReal()) { + return SwerveModule( + name, + drivingMotor, + turningMotor, + driveController, + turnController, + driveFeedforward, + location + ) + } else { + return SwerveModuleSim( + name, + drivingMotor, + turningMotor, + driveController, + turnController, + driveFeedforward, + location + ) + } + } + } +} + +/** A "simulated" swerve module. Immediately reaches to its desired state. */ +class SwerveModuleSim( + name: String, + drivingMotor: WrappedMotor, + turningMotor: WrappedMotor, + driveController: PIDController, + turnController: PIDController, + driveFeedforward: SimpleMotorFeedforward, + location: Translation2d +) : SwerveModule( + name, + drivingMotor, + turningMotor, + driveController, + turnController, + driveFeedforward, + location +) { + private val turningMotorEncoder = Encoder.SimController(turningMotor.encoder) + private val driveEncoder = Encoder.SimController(drivingMotor.encoder) + private var prevTime = Timer.getFPGATimestamp() + override var state: SwerveModuleState + get() = SwerveModuleState( + driveEncoder.velocity, + Rotation2d(turningMotorEncoder.position) + ) + set(desiredState) { + super.state = desiredState + turningMotorEncoder.position = desiredState.angle.radians + driveEncoder.velocity = desiredState.speedMetersPerSecond + } + override val position: SwerveModulePosition + get() = SwerveModulePosition( + driveEncoder.position, + Rotation2d(turningMotorEncoder.position) + ) + + override fun update() { + val currTime = Timer.getFPGATimestamp() + driveEncoder.position += driveEncoder.velocity * (currTime - prevTime) + prevTime = currTime + } +} diff --git a/src/main/kotlin/frc/team449/control/holonomic/SwerveOrthogonalCommand.kt b/src/main/kotlin/frc/team449/control/holonomic/SwerveOrthogonalCommand.kt new file mode 100644 index 0000000..dee846d --- /dev/null +++ b/src/main/kotlin/frc/team449/control/holonomic/SwerveOrthogonalCommand.kt @@ -0,0 +1,189 @@ +package frc.team449.control.holonomic + +import edu.wpi.first.math.MathUtil +import edu.wpi.first.math.filter.SlewRateLimiter +import edu.wpi.first.math.geometry.Rotation2d +import edu.wpi.first.math.geometry.Translation2d +import edu.wpi.first.math.kinematics.ChassisSpeeds +import edu.wpi.first.util.sendable.SendableBuilder +import edu.wpi.first.wpilibj.DriverStation +import edu.wpi.first.wpilibj.Timer +import edu.wpi.first.wpilibj.XboxController +import edu.wpi.first.wpilibj2.command.Command +import frc.team449.robot2023.constants.RobotConstants +import frc.team449.robot2023.constants.drives.SwerveConstants +import kotlin.jvm.optionals.getOrNull +import kotlin.math.* + +class SwerveOrthogonalCommand( + private val drive: SwerveDrive, + private val controller: XboxController, + private val fieldOriented: () -> Boolean = { true } +) : Command() { + + private var prevX = 0.0 + private var prevY = 0.0 + + private var prevTime = 0.0 + + private var dx = 0.0 + private var dy = 0.0 + private var magAcc = 0.0 + private var dt = 0.0 + private var magAccClamped = 0.0 + + private var rotScaled = 0.0 + private val allianceCompensation = { if (DriverStation.getAlliance().getOrNull() == DriverStation.Alliance.Red) 0.0 else PI } + private val directionCompensation = { if (DriverStation.getAlliance().getOrNull() == DriverStation.Alliance.Red) -1.0 else 1.0 } + + private var atGoal = true + + private var rotRamp = SlewRateLimiter(RobotConstants.ROT_RATE_LIMIT) + + private val timer = Timer() + + private val rotCtrl = RobotConstants.ORTHOGONAL_CONTROLLER + + private var skewConstant = 11.5 + + private var desiredVel = doubleArrayOf(0.0, 0.0, 0.0) + + init { + addRequirements(drive) + rotCtrl.enableContinuousInput(-PI, PI) + rotCtrl.setTolerance(0.025) + } + + override fun initialize() { + timer.restart() + + prevX = drive.currentSpeeds.vxMetersPerSecond + prevY = drive.currentSpeeds.vyMetersPerSecond + prevTime = 0.0 + dx = 0.0 + dy = 0.0 + magAcc = 0.0 + dt = 0.0 + magAccClamped = 0.0 + + rotRamp = SlewRateLimiter( + RobotConstants.ROT_RATE_LIMIT, + RobotConstants.NEG_ROT_RATE_LIM, + drive.currentSpeeds.omegaRadiansPerSecond + ) + + var atGoal = true + } + + override fun execute() { + val currTime = timer.get() + dt = currTime - prevTime + prevTime = currTime + + val ctrlX = if (abs(controller.leftY) < RobotConstants.TRANSLATION_DEADBAND) .0 else -controller.leftY + val ctrlY = if (abs(controller.leftX) < RobotConstants.TRANSLATION_DEADBAND) .0 else -controller.leftX + + val ctrlRadius = sqrt(ctrlX.pow(2) + ctrlY.pow(2)).pow(SwerveConstants.JOYSTICK_FILTER_ORDER) + + val ctrlTheta = atan2(ctrlY, ctrlX) + + val xScaled = ctrlRadius * cos(ctrlTheta) * RobotConstants.MAX_LINEAR_SPEED + val yScaled = ctrlRadius * sin(ctrlTheta) * RobotConstants.MAX_LINEAR_SPEED + + dx = xScaled - prevX + dy = yScaled - prevY + magAcc = hypot(dx / dt, dy / dt) + magAccClamped = MathUtil.clamp(magAcc, -RobotConstants.MAX_ACCEL, RobotConstants.MAX_ACCEL) + + val factor = if (magAcc == 0.0) 0.0 else magAccClamped / magAcc + val dxClamped = dx * factor + val dyClamped = dy * factor + val xClamped = prevX + dxClamped + val yClamped = prevY + dyClamped + + prevX = xClamped + prevY = yClamped + + if (controller.xButtonPressed) { + val desAngleA = MathUtil.angleModulus(7 * PI / 4 + allianceCompensation.invoke()) + if (abs(desAngleA - drive.heading.radians) > 0.075 && abs(desAngleA - drive.heading.radians) < 2 * PI - 0.075) { + atGoal = false + rotCtrl.setpoint = desAngleA + } + } else if (controller.yButtonPressed) { + val desAngleY = MathUtil.angleModulus(3 * PI / 2 + allianceCompensation.invoke()) + if (abs(desAngleY - drive.heading.radians) > 0.075 && abs(desAngleY - drive.heading.radians) < 2 * PI - 0.075) { + atGoal = false + rotCtrl.setpoint = desAngleY + } + } + + if (atGoal) { + rotScaled = rotRamp.calculate( + (if (abs(controller.rightX) < RobotConstants.ROTATION_DEADBAND) .0 else -controller.rightX) * + drive.maxRotSpeed + ) + } else { + rotScaled = MathUtil.clamp( + rotCtrl.calculate(drive.heading.radians), + -RobotConstants.ALIGN_ROT_SPEED, + RobotConstants.ALIGN_ROT_SPEED + ) + atGoal = rotCtrl.atSetpoint() + } + + val vel = Translation2d(xClamped, yClamped) + + if (fieldOriented.invoke()) { + /** Quick fix for the velocity skewing towards the direction of rotation + * by rotating it with offset proportional to how much we are rotating + **/ + vel.rotateBy(Rotation2d(-rotScaled * dt * skewConstant)) + + val desVel = ChassisSpeeds.fromFieldRelativeSpeeds( + vel.x * directionCompensation.invoke(), + vel.y * directionCompensation.invoke(), + rotScaled, + drive.heading + ) + drive.set( + desVel + ) + + desiredVel[0] = desVel.vxMetersPerSecond + desiredVel[1] = desVel.vyMetersPerSecond + desiredVel[2] = desVel.omegaRadiansPerSecond + } else { + drive.set( + ChassisSpeeds( + vel.x, + vel.y, + rotScaled + ) + ) + } + } + + override fun initSendable(builder: SendableBuilder) { + builder.publishConstString("1.0", "Controller X and Y Values") + builder.addDoubleProperty("1.1 currX", { if (abs(controller.leftY) < RobotConstants.TRANSLATION_DEADBAND) .0 else -controller.leftY }, null) + builder.addDoubleProperty("1.2 currY", { if (abs(controller.leftX) < RobotConstants.TRANSLATION_DEADBAND) .0 else -controller.leftX }, null) + builder.addDoubleProperty("1.3 prevX", { prevX }, null) + builder.addDoubleProperty("1.4 prevY", { prevY }, null) + + builder.publishConstString("2.0", "Delta X, Y, Time over one loop") + builder.addDoubleProperty("2.1 dx", { dx }, null) + builder.addDoubleProperty("2.2 dy", { dy }, null) + builder.addDoubleProperty("2.3 dt", { dt }, null) + + builder.publishConstString("3.0", "Magnitude of Acceleration") + builder.addDoubleProperty("3.1 magAcc", { magAcc }, null) + builder.addDoubleProperty("3.2 magAccClamped", { magAccClamped }, null) + + builder.publishConstString("4.0", "Turning Skew") + builder.addDoubleProperty("4.1 skew constant", { skewConstant }, { k: Double -> skewConstant = k }) + + builder.publishConstString("5.0", "Given Speeds") + builder.addDoubleArrayProperty("Chassis Speed", { desiredVel }, null) + } +} diff --git a/src/main/kotlin/frc/team449/control/holonomic/SwerveSim.kt b/src/main/kotlin/frc/team449/control/holonomic/SwerveSim.kt new file mode 100644 index 0000000..fbee4da --- /dev/null +++ b/src/main/kotlin/frc/team449/control/holonomic/SwerveSim.kt @@ -0,0 +1,139 @@ +package frc.team449.control.holonomic + +import edu.wpi.first.math.geometry.Pose2d +import edu.wpi.first.math.geometry.Rotation2d +import edu.wpi.first.math.kinematics.SwerveDriveOdometry +import edu.wpi.first.wpilibj.DriverStation +import edu.wpi.first.wpilibj.Timer.getFPGATimestamp +import edu.wpi.first.wpilibj.smartdashboard.Field2d +import frc.team449.control.vision.VisionSubsystem +import frc.team449.robot2023.constants.vision.VisionConstants +import frc.team449.system.AHRS +import kotlin.math.abs +import kotlin.math.hypot +import kotlin.math.pow +import kotlin.math.sqrt + +class SwerveSim( + modules: List, + ahrs: AHRS, + maxLinearSpeed: Double, + maxRotSpeed: Double, + cameras: List, + field: Field2d +) : SwerveDrive(modules, ahrs, maxLinearSpeed, maxRotSpeed, cameras, field) { + + private var lastTime = getFPGATimestamp() + var odoPose = Pose2d() + var currHeading = Rotation2d() + + private val odometry = SwerveDriveOdometry( + kinematics, + currHeading, + getPositions() + ) + + /** The (x, y, theta) position of the robot on the field. */ + override var pose: Pose2d + get() = this.poseEstimator.estimatedPosition + set(value) { + this.poseEstimator.resetPosition( + currHeading, + getPositions(), + value + ) + + odometry.resetPosition( + currHeading, + getPositions(), + value + ) + } + + override fun localize() = try { + for ((index, camera) in cameras.withIndex()) { + val result = camera.estimatedPose(Pose2d(pose.x, pose.y, currHeading)) + if (result.isPresent) { + val presentResult = result.get() + numTargets[index] = presentResult.targetsUsed.size.toDouble() + tagDistance[index] = 0.0 + avgAmbiguity[index] = 0.0 + heightError[index] = abs(presentResult.estimatedPose.z) + + for (tag in presentResult.targetsUsed) { + val tagPose = camera.estimator.fieldTags.getTagPose(tag.fiducialId) + if (tagPose.isPresent) { + val estimatedToTag = presentResult.estimatedPose.minus(tagPose.get()) + tagDistance[index] += sqrt(estimatedToTag.x.pow(2) + estimatedToTag.y.pow(2)) / numTargets[index] + avgAmbiguity[index] = tag.poseAmbiguity / numTargets[index] + } else { + tagDistance[index] = Double.MAX_VALUE + avgAmbiguity[index] = Double.MAX_VALUE + break + } + } + + visionPose = presentResult.estimatedPose.toPose2d() + + if (presentResult.timestampSeconds > 0 && + avgAmbiguity[index] <= VisionConstants.MAX_AMBIGUITY && + numTargets[index] < 2 && tagDistance[index] <= VisionConstants.MAX_DISTANCE_SINGLE_TAG || + numTargets[index] >= 2 && tagDistance[index] <= VisionConstants.MAX_DISTANCE_MULTI_TAG * (1 + (numTargets[index] - 2) * VisionConstants.TAG_MULTIPLIER) && + heightError[index] < VisionConstants.MAX_HEIGHT_ERR_METERS + ) { + poseEstimator.addVisionMeasurement( + visionPose, + presentResult.timestampSeconds, + camera.getEstimationStdDevs(numTargets[index].toInt(), tagDistance[index]) + ) + usedVision[index] = true + } else { + usedVision[index] = false + } + } + } + } catch (e: Error) { + DriverStation.reportError( + "!!!!!!!!! VISION ERROR !!!!!!!", + e.stackTrace + ) + } + + override fun periodic() { + val currTime = getFPGATimestamp() + + currHeading = currHeading.plus(Rotation2d(super.desiredSpeeds.omegaRadiansPerSecond * (currTime - lastTime))) + this.lastTime = currTime + + set(super.desiredSpeeds) + + // Updates the robot's currentSpeeds. + currentSpeeds = kinematics.toChassisSpeeds( + modules[0].state, + modules[1].state, + modules[2].state, + modules[3].state + ) + + val transVel = hypot(currentSpeeds.vxMetersPerSecond, currentSpeeds.vyMetersPerSecond) + if (transVel > maxSpeed) maxSpeed = transVel + + // Update the robot's pose using the gyro heading and the SwerveModulePositions of each module. + this.poseEstimator.update( + currHeading, + getPositions() + ) + + if (cameras.isNotEmpty()) localize() + + // Sets the robot's pose and individual module rotations on the SmartDashboard [Field2d] widget. + setRobotPose() + + odoPose = odometry.update( + currHeading, + getPositions() + ) + + field.getObject("odo").pose = odoPose + } +} diff --git a/src/main/kotlin/frc/team449/control/vision/VisionEstimator.kt b/src/main/kotlin/frc/team449/control/vision/VisionEstimator.kt new file mode 100644 index 0000000..8598165 --- /dev/null +++ b/src/main/kotlin/frc/team449/control/vision/VisionEstimator.kt @@ -0,0 +1,303 @@ +package frc.team449.control.vision + +import edu.wpi.first.apriltag.AprilTag +import edu.wpi.first.apriltag.AprilTagFieldLayout +import edu.wpi.first.math.Matrix +import edu.wpi.first.math.geometry.* +import edu.wpi.first.math.numbers.N1 +import edu.wpi.first.math.numbers.N3 +import edu.wpi.first.math.numbers.N5 +import edu.wpi.first.wpilibj.DriverStation +import frc.team449.robot2023.constants.vision.VisionConstants +import org.photonvision.EstimatedRobotPose +import org.photonvision.PhotonCamera +import org.photonvision.PhotonPoseEstimator +import org.photonvision.estimation.OpenCVHelp +import org.photonvision.targeting.PNPResults +import org.photonvision.targeting.PhotonPipelineResult +import org.photonvision.targeting.PhotonTrackedTarget +import org.photonvision.targeting.TargetCorner +import java.util.Optional +import kotlin.math.PI +import kotlin.math.abs + +/** + * This class uses normal multi-tag PNP and lowest ambiguity using the gyro rotation + * for the internal cam-to-tag transform as a fallback strategy + */ +class VisionEstimator( + private val tagLayout: AprilTagFieldLayout, + val camera: PhotonCamera, + private val robotToCam: Transform3d +) : PhotonPoseEstimator(tagLayout, PoseStrategy.MULTI_TAG_PNP_ON_RIO, camera, robotToCam) { + private val reportedErrors: HashSet = HashSet() + private var driveHeading: Rotation2d? = null + private var lastPose: Pose3d? = null + + fun estimatedPose(currPose: Pose2d): Optional { + driveHeading = currPose.rotation + lastPose = Pose3d(currPose.x, currPose.y, 0.0, Rotation3d(0.0, 0.0, currPose.rotation.radians)) + return updatePose(camera.latestResult) + } + + private fun updatePose(cameraResult: PhotonPipelineResult?): Optional { + // Time in the past -- give up, since the following if expects times > 0 + if (cameraResult!!.timestampSeconds < 0) { + return Optional.empty() + } + + // If the pose cache timestamp was set, and the result is from the same timestamp, return an + // empty result + if (poseCacheTimestampSeconds > 0 && + abs(poseCacheTimestampSeconds - cameraResult.timestampSeconds) < 1e-6 + ) { + return Optional.empty() + } + + // Remember the timestamp of the current result used + poseCacheTimestampSeconds = cameraResult.timestampSeconds + + // If no targets seen, trivial case -- return empty result + return if (!cameraResult.hasTargets()) { + Optional.empty() + } else { + multiTagPNPStrategy(cameraResult) + } + } + + private fun checkBest(check: Pose3d?, opt1: Pose3d?, opt2: Pose3d?): Pose3d? { + if (check == null || opt1 == null || opt2 == null) return null + val dist1 = check.translation.toTranslation2d().getDistance(opt1.translation.toTranslation2d()) + val dist2 = check.translation.toTranslation2d().getDistance(opt2.translation.toTranslation2d()) + + return if (dist1 < dist2) { + opt1 + } else { + opt2 + } + } + + private fun multiTagPNPStrategy(result: PhotonPipelineResult): Optional { + // Arrays we need declared up front + val visCorners = ArrayList() + val knownVisTags = ArrayList() + val fieldToCams = ArrayList() + val fieldToCamsAlt = ArrayList() + val usedTargets = ArrayList() + if (result.getTargets().size < 2) { + // Run fallback strategy instead + return lowestAmbiguityStrategy(result) + } + for (target: PhotonTrackedTarget in result.getTargets()) { + val tagPoseOpt = tagLayout.getTagPose(target.fiducialId) + if (tagPoseOpt.isEmpty) { + reportFiducialPoseError(target.fiducialId) + continue + } + val tagPose = tagPoseOpt.get() + + usedTargets.add(target) + + visCorners.addAll(target.detectedCorners) + // actual layout poses of visible tags -- not exposed, so have to recreate + knownVisTags.add(AprilTag(target.fiducialId, tagPose)) + fieldToCams.add(tagPose.transformBy(target.bestCameraToTarget.inverse())) + fieldToCamsAlt.add(tagPose.transformBy(target.alternateCameraToTarget.inverse())) + } + val cameraMatrixOpt = camera.cameraMatrix + val distCoeffsOpt = camera.distCoeffs + val hasCalibData = cameraMatrixOpt.isPresent && distCoeffsOpt.isPresent + + // multi-target solvePNP + if (hasCalibData && visCorners.size == knownVisTags.size * 4 && knownVisTags.isNotEmpty()) { + val cameraMatrix = cameraMatrixOpt.get() + val distCoeffs = distCoeffsOpt.get() + val pnpResults = estimateCamPosePNP(cameraMatrix, distCoeffs, usedTargets.toList()) + val best = Pose3d() + .plus(pnpResults.best) // field-to-camera + .plus(robotToCam.inverse()) // field-to-robot + + val alternate = Pose3d() + .plus(pnpResults.alt) + .plus(robotToCam.inverse()) + + return Optional.of( + EstimatedRobotPose( + checkBest(lastPose, best, alternate) ?: best, + result.timestampSeconds, + result.targets, + PoseStrategy.MULTI_TAG_PNP_ON_RIO + ) + ) + } + + return Optional.empty() + } + + /** + * Return the estimated position of the robot with the lowest position ambiguity from a List of + * pipeline results. + * + * @param result pipeline result + * @return the estimated position of the robot in the FCS and the estimated timestamp of this + * estimation. + */ + private fun lowestAmbiguityStrategy(result: PhotonPipelineResult): Optional { + var lowestAmbiguityTarget: PhotonTrackedTarget? = null + var lowestAmbiguityScore = 10.0 + for (target: PhotonTrackedTarget in result.targets) { + val targetPoseAmbiguity = target.poseAmbiguity + // Make sure the target is a Fiducial target. + if (targetPoseAmbiguity != -1.0 && targetPoseAmbiguity < lowestAmbiguityScore) { + lowestAmbiguityScore = targetPoseAmbiguity + lowestAmbiguityTarget = target + } + } + + // Although there are confirmed to be targets, none of them may be fiducial + // targets. + if (lowestAmbiguityTarget == null) return Optional.empty() + val targetFiducialId = lowestAmbiguityTarget.fiducialId + val targetPosition = tagLayout.getTagPose(targetFiducialId) + if (targetPosition.isEmpty) { + reportFiducialPoseError(targetFiducialId) + return Optional.empty() + } + + val bestPose = targetPosition + .get() + .transformBy( + Transform3d( + lowestAmbiguityTarget.bestCameraToTarget.translation, + Rotation3d( + lowestAmbiguityTarget.bestCameraToTarget.rotation.x, + lowestAmbiguityTarget.bestCameraToTarget.rotation.y, + driveHeading!!.radians + robotToCam.rotation.z - targetPosition.get().rotation.z + ) + ).inverse() + ) + .transformBy(robotToCam.inverse()) + + val altPose = targetPosition + .get() + .transformBy( + Transform3d( + lowestAmbiguityTarget.alternateCameraToTarget.translation, + Rotation3d( + lowestAmbiguityTarget.alternateCameraToTarget.rotation.x, + lowestAmbiguityTarget.alternateCameraToTarget.rotation.y, + driveHeading!!.radians + robotToCam.rotation.z - targetPosition.get().rotation.z + ) + ).inverse() + ) + .transformBy(robotToCam.inverse()) + + val usedPose = checkBest(lastPose, bestPose, altPose) ?: bestPose + + if (usedPose == bestPose) { + val camBestPose = targetPosition + .get() + .transformBy( + Transform3d( + lowestAmbiguityTarget.bestCameraToTarget.translation, + lowestAmbiguityTarget.bestCameraToTarget.rotation + ).inverse() + ) + .transformBy(robotToCam.inverse()) + + if (abs(camBestPose.rotation.z * (180 / (2 * PI)) - driveHeading!!.degrees) > VisionConstants.SINGLE_TAG_HEADING_MAX_DEV_DEG) { + DriverStation.reportWarning("Best Single Tag Heading over Max Deviation, deviated by ${abs(camBestPose.rotation.z * (180 / (2 * PI)) - driveHeading!!.degrees)}", false) + return Optional.empty() + } + } else { + + val camAltPose = targetPosition + .get() + .transformBy( + Transform3d( + lowestAmbiguityTarget.alternateCameraToTarget.translation, + lowestAmbiguityTarget.alternateCameraToTarget.rotation + ).inverse() + ) + .transformBy(robotToCam.inverse()) + + if (abs(camAltPose.rotation.z * (180 / (2 * PI)) - driveHeading!!.degrees) > VisionConstants.SINGLE_TAG_HEADING_MAX_DEV_DEG) { + DriverStation.reportWarning("Alt Single Tag Heading over Max Deviation, deviated by ${abs(camAltPose.rotation.z * (180 / (2 * PI)) - driveHeading!!.degrees)}", false) + return Optional.empty() + } + } + + return Optional.of( + EstimatedRobotPose( + checkBest(lastPose, bestPose, altPose) ?: bestPose, + result.timestampSeconds, + mutableListOf(lowestAmbiguityTarget), + PoseStrategy.LOWEST_AMBIGUITY + ) + ) + } + + private fun reportFiducialPoseError(fiducialId: Int) { + if (!reportedErrors.contains(fiducialId)) { + DriverStation.reportError( + "[PhotonPoseEstimator] Tried to get pose of unknown AprilTag: $fiducialId", + false + ) + reportedErrors.add(fiducialId) + } + } + + private fun estimateCamPosePNP( + cameraMatrix: Matrix?, + distCoeffs: Matrix?, + visTags: List?, + ): PNPResults { + if (visTags == null || tagLayout.tags.isEmpty() || visTags.isEmpty()) { + return PNPResults() + } + val corners = java.util.ArrayList() + val knownTags = java.util.ArrayList() + // ensure these are AprilTags in our layout + for (tgt in visTags) { + val id = tgt.fiducialId + tagLayout + .getTagPose(id) + .ifPresent { pose: Pose3d? -> + knownTags.add(AprilTag(id, pose)) + corners.addAll(tgt.detectedCorners) + } + } + if (knownTags.size == 0 || corners.size == 0 || corners.size % 4 != 0) { + return PNPResults() + } + val points = OpenCVHelp.cornersToPoints(corners) + + // single-tag pnp + return if (knownTags.size == 1) { + val camToTag = OpenCVHelp.solvePNP_SQUARE(cameraMatrix, distCoeffs, VisionConstants.TAG_MODEL.vertices, points) + if (!camToTag.isPresent) return PNPResults() + val bestPose = knownTags[0].pose.transformBy(camToTag.best.inverse()) + var altPose: Pose3d? = Pose3d() + if (camToTag.ambiguity != 0.0) altPose = knownTags[0].pose.transformBy(camToTag.alt.inverse()) + val o = Pose3d() + PNPResults( + Transform3d(o, bestPose), + Transform3d(o, altPose), + camToTag.ambiguity, + camToTag.bestReprojErr, + camToTag.altReprojErr + ) + } else { + val objectTrls = java.util.ArrayList() + for (tag in knownTags) objectTrls.addAll(VisionConstants.TAG_MODEL.getFieldVertices(tag.pose)) + val camToOrigin = OpenCVHelp.solvePNP_SQPNP(cameraMatrix, distCoeffs, objectTrls, points) + if (!camToOrigin.isPresent) PNPResults() else PNPResults( + camToOrigin.best.inverse(), + camToOrigin.alt.inverse(), + camToOrigin.ambiguity, + camToOrigin.bestReprojErr, + camToOrigin.altReprojErr + ) + } + } +} diff --git a/src/main/kotlin/frc/team449/control/vision/VisionSubsystem.kt b/src/main/kotlin/frc/team449/control/vision/VisionSubsystem.kt new file mode 100644 index 0000000..1681e2d --- /dev/null +++ b/src/main/kotlin/frc/team449/control/vision/VisionSubsystem.kt @@ -0,0 +1,106 @@ +package frc.team449.control.vision + +import edu.wpi.first.apriltag.AprilTagFieldLayout +import edu.wpi.first.math.Matrix +import edu.wpi.first.math.geometry.Pose2d +import edu.wpi.first.math.geometry.Rotation2d +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.wpilibj.RobotBase +import edu.wpi.first.wpilibj.smartdashboard.Field2d +import frc.team449.robot2023.constants.vision.VisionConstants +import org.photonvision.EstimatedRobotPose +import org.photonvision.PhotonCamera +import org.photonvision.simulation.PhotonCameraSim +import org.photonvision.simulation.SimCameraProperties +import org.photonvision.simulation.VisionSystemSim +import java.util.Optional +import kotlin.math.abs +import kotlin.math.pow + + +class VisionSubsystem( + name: String, + tagLayout: AprilTagFieldLayout, + val robotToCam: Transform3d, + private val visionSystemSim: VisionSystemSim? +) { + val estimator = VisionEstimator( + tagLayout, + PhotonCamera(name), + robotToCam + ) + + private var lastEstTimestamp = 0.0 + + private var cameraSim: PhotonCameraSim? = null + + init { + if (RobotBase.isSimulation()) { + visionSystemSim!!.addAprilTags(tagLayout) + + val cameraProp = SimCameraProperties() + cameraProp.setCalibration(VisionConstants.SIM_CAMERA_WIDTH_PX, VisionConstants.SIM_CAMERA_HEIGHT_PX, Rotation2d.fromDegrees(VisionConstants.SIM_FOV_DEG)) + cameraProp.setCalibError(VisionConstants.SIM_CALIB_AVG_ERR_PX, VisionConstants.SIM_CALIB_ERR_STDDEV_PX) + cameraProp.fps = VisionConstants.SIM_FPS + cameraProp.avgLatencyMs = VisionConstants.SIM_AVG_LATENCY + cameraProp.latencyStdDevMs = VisionConstants.SIM_STDDEV_LATENCY + + cameraSim = PhotonCameraSim(estimator.camera, cameraProp) + + cameraSim!!.enableDrawWireframe(VisionConstants.ENABLE_WIREFRAME) + + visionSystemSim.addCamera(cameraSim, robotToCam) + } + } + + private fun getSimDebugField(): Field2d? { + return if (!RobotBase.isSimulation()) null else visionSystemSim!!.debugField + } + + fun estimatedPose(currPose: Pose2d): Optional { + val visionEst = estimator.estimatedPose(currPose) + val latestTimestamp = estimator.camera.latestResult.timestampSeconds + val newResult = abs(latestTimestamp - lastEstTimestamp) > 1e-4 + if (RobotBase.isSimulation()) { + visionEst.ifPresentOrElse( + { est -> + getSimDebugField()!! + .getObject("VisionEstimation").pose = est.estimatedPose.toPose2d() + } + ) { if (newResult) getSimDebugField()!!.getObject("VisionEstimation").setPoses() } + } + return if (newResult) { + lastEstTimestamp = latestTimestamp + visionEst + } else { + Optional.empty() + } + } + + fun getEstimationStdDevs(numTags: Int, avgDist: Double): Matrix { + var estStdDevs = VisionConstants.SINGLE_TAG_TRUST.copy() + + if (numTags == 0) return estStdDevs + + // Decrease std devs if multiple targets are visible + if (numTags > 1) estStdDevs = VisionConstants.MULTI_TAG_TRUST.copy() + + // Increase std devs based on (average) distance + estStdDevs.times( + 1 + avgDist.pow(VisionConstants.ORDER) * VisionConstants.PROPORTION + ) + + return estStdDevs + } + + fun simulationPeriodic(robotSimPose: Pose2d?) { + visionSystemSim!!.update(robotSimPose) + } + + /** Reset pose history of the robot in the vision system simulation. */ + fun resetSimPose(pose: Pose2d?) { + if (RobotBase.isSimulation()) visionSystemSim!!.resetRobotPose(pose) + } +} \ No newline at end of file diff --git a/src/main/kotlin/frc/team449/robot2023/Robot.kt b/src/main/kotlin/frc/team449/robot2023/Robot.kt new file mode 100644 index 0000000..c53308e --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/Robot.kt @@ -0,0 +1,40 @@ +package frc.team449.robot2023 + +import edu.wpi.first.wpilibj.PowerDistribution +import edu.wpi.first.wpilibj.SPI +import edu.wpi.first.wpilibj.XboxController +import frc.team449.RobotBase +import frc.team449.control.holonomic.SwerveDrive +import frc.team449.control.holonomic.SwerveOrthogonalCommand +import frc.team449.robot2023.constants.RobotConstants +import frc.team449.robot2023.subsystems.light.Light +import frc.team449.system.AHRS +import monologue.Logged +import monologue.Monologue.LogBoth + +class Robot : RobotBase(), Logged { + + val driveController = XboxController(0) + + val mechController = XboxController(1) + + val ahrs = AHRS(SPI.Port.kMXP) + + // Instantiate/declare PDP and other stuff here + + @LogBoth + override val powerDistribution: PowerDistribution = PowerDistribution( + RobotConstants.PDH_CAN, + PowerDistribution.ModuleType.kRev + ) + + @LogBoth + override val drive = SwerveDrive.createSwerve(ahrs, field) + + @LogBoth + override val driveCommand = SwerveOrthogonalCommand(drive, driveController) + + val light = Light.createLight() +// +// val infrared = DigitalInput(RobotConstants.IR_CHANNEL) +} diff --git a/src/main/kotlin/frc/team449/robot2023/auto/AutoUtil.kt b/src/main/kotlin/frc/team449/robot2023/auto/AutoUtil.kt new file mode 100644 index 0000000..a32c17c --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/auto/AutoUtil.kt @@ -0,0 +1,58 @@ +package frc.team449.robot2023.auto + +import edu.wpi.first.math.MatBuilder +import edu.wpi.first.math.MathUtil +import edu.wpi.first.math.numbers.N2 +import edu.wpi.first.math.numbers.N3 +import frc.team449.control.auto.ChoreoTrajectory +import frc.team449.robot2023.constants.field.FieldConstants +import kotlin.math.PI + +object AutoUtil { + fun transformForPos2(pathGroup: MutableList): MutableList { + for (index in 0 until pathGroup.size) { + for (time in pathGroup[index].objectiveTimestamps) { + val currentMatrix = pathGroup[index].stateMap.get(time) + + val newMatrix = MatBuilder(N2.instance, N3.instance).fill( + currentMatrix[0, 0], + FieldConstants.fieldWidth - currentMatrix[0, 1], + -currentMatrix[0, 2], + currentMatrix[1, 0], + -currentMatrix[1, 1], + -currentMatrix[1, 2] + ) + + pathGroup[index].stateMap.put(time, newMatrix) + } + } + + return pathGroup + } + + fun transformForRed(pathGroup: MutableList): MutableList { + for (index in 0 until pathGroup.size) { + for (time in pathGroup[index].objectiveTimestamps) { + val currentMatrix = pathGroup[index].stateMap.get(time) + + val newMatrix = MatBuilder(N2.instance, N3.instance).fill( + FieldConstants.fieldLength - currentMatrix[0, 0], + FieldConstants.fieldWidth - currentMatrix[0, 1], + MathUtil.angleModulus(PI + currentMatrix[0, 2]), + -currentMatrix[1, 0], + -currentMatrix[1, 1], + currentMatrix[1, 2] + ) + + pathGroup[index].stateMap.put(time, newMatrix) + } + } + + return pathGroup + } + + /** Add other methods that return commands that do groups of actions that are done + * across different auto routines. For Charged UP, these methods were things such as + * dropping a cone/cube, or getting in ground intake position, etc. + */ +} diff --git a/src/main/kotlin/frc/team449/robot2023/auto/routines/DoNothing.kt b/src/main/kotlin/frc/team449/robot2023/auto/routines/DoNothing.kt new file mode 100644 index 0000000..8832f55 --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/auto/routines/DoNothing.kt @@ -0,0 +1,24 @@ +package frc.team449.robot2023.auto.routines + +import edu.wpi.first.wpilibj2.command.Command +import edu.wpi.first.wpilibj2.command.InstantCommand +import frc.team449.control.auto.ChoreoRoutine +import frc.team449.control.auto.ChoreoRoutineStructure +import frc.team449.control.auto.ChoreoTrajectory +import frc.team449.robot2023.Robot + +class DoNothing( + robot: Robot +) : ChoreoRoutineStructure { + + override val routine = + ChoreoRoutine( + drive = robot.drive + ) + + override val trajectory: MutableList = mutableListOf() + + override fun createCommand(): Command { + return InstantCommand() + } +} diff --git a/src/main/kotlin/frc/team449/robot2023/auto/routines/RoutineChooser.kt b/src/main/kotlin/frc/team449/robot2023/auto/routines/RoutineChooser.kt new file mode 100644 index 0000000..6f0a047 --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/auto/routines/RoutineChooser.kt @@ -0,0 +1,23 @@ +package frc.team449.robot2023.auto.routines + +import edu.wpi.first.wpilibj.smartdashboard.SendableChooser +import edu.wpi.first.wpilibj2.command.Command +import frc.team449.robot2023.Robot + +class RoutineChooser(private val robot: Robot) : SendableChooser() { + + fun routineMap(): HashMap { + return hashMapOf( + "DoNothing" to DoNothing(robot).createCommand(), + ) + } + + init { + updateOptions(true) + } + + fun updateOptions(isRed: Boolean) { + /** Add auto options here */ + this.setDefaultOption("Do Nothing", "DoNothing") + } +} diff --git a/src/main/kotlin/frc/team449/robot2023/commands/characterization/Characterization.kt b/src/main/kotlin/frc/team449/robot2023/commands/characterization/Characterization.kt new file mode 100644 index 0000000..0169ea2 --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/commands/characterization/Characterization.kt @@ -0,0 +1,85 @@ +// 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.team449.robot2023.commands.characterization + +import edu.wpi.first.wpilibj.Timer +import edu.wpi.first.wpilibj2.command.Command +import edu.wpi.first.wpilibj2.command.Subsystem +import java.util.LinkedList +import java.util.function.DoubleConsumer +import java.util.function.DoubleSupplier +import kotlin.math.abs + +class Characterization( + subsystem: Subsystem?, + private val forward: Boolean, + private val name: String, + private val voltageConsumer: DoubleConsumer, + private val velocitySupplier: DoubleSupplier +) : Command() { + private val timer = Timer() + private val data = FeedForwardCharacterizationData(name) + + init { + addRequirements(subsystem) + } + + + // Called when the command is initially scheduled. + override fun initialize() { + timer.reset() + timer.start() + } + + // Called every time the scheduler runs while the command is scheduled. + override fun execute() { + if (timer.get() < startDelaySecs) { + voltageConsumer.accept(0.0) + } else { + val voltage = (timer.get() - startDelaySecs) * rampRateVoltsPerSec * if (forward) 1 else -1 + voltageConsumer.accept(voltage) + data.add(velocitySupplier.asDouble, voltage) + } + } + + // Called once the command ends or is interrupted. + override fun end(interrupted: Boolean) { + voltageConsumer.accept(0.0) + + timer.stop() + println("hi i ended") + if (!(data.velocityData.size == 0 || data.voltageData.size == 0)) { + val regression = Regression( + data.velocityData.stream().mapToDouble { obj: Double -> obj }.toArray(), + data.voltageData.stream().mapToDouble { obj: Double -> obj }.toArray(), + 1 + ) + println("FF Characterization Results ($name):)\n\tCount= ${data.velocityData.size}\n\tR2=${regression.R2()}\n\tkS: ${regression.beta(0)}\n\tkV: ${regression.beta(1)}") + } + } + + // Returns true when the command should end. + override fun isFinished(): Boolean { + return false + } + + class FeedForwardCharacterizationData(private val name: String) { + val velocityData: MutableList = LinkedList() + val voltageData: MutableList = LinkedList() + fun add(velocity: Double, voltage: Double) { + if (abs(velocity) > 1E-4) { + velocityData.add(abs(velocity)) + voltageData.add(abs(voltage)) + } + } + } + + companion object { + private const val startDelaySecs = 1.5 + private const val rampRateVoltsPerSec = 0.085 + } +} \ No newline at end of file diff --git a/src/main/kotlin/frc/team449/robot2023/commands/characterization/Regression.kt b/src/main/kotlin/frc/team449/robot2023/commands/characterization/Regression.kt new file mode 100644 index 0000000..fec1213 --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/commands/characterization/Regression.kt @@ -0,0 +1,184 @@ +// 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.team449.robot2023.commands.characterization + +import Jama.Matrix +import Jama.QRDecomposition +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.pow + +// NOTE: This file is available at +// http://algs4.cs.princeton.edu/14analysis/PolynomialRegression.java.html +/** + * The `PolynomialRegression` class performs a polynomial regression on an set of *N* + * data points (*yi*, *xi*). That is, it fits a polynomial + * *y* = 0 + 1 *x* + 2 + * *x*2 + ... + *d* *x**d* (where + * *y* is the response variable, *x* is the predictor variable, and the + * *i* are the regression coefficients) that minimizes the sum of squared + * residuals of the multiple regression model. It also computes associated the coefficient of + * determination *R*2. + * + * + * This implementation performs a QR-decomposition of the underlying Vandermonde matrix, so it is + * neither the fastest nor the most numerically stable way to perform the polynomial regression. + * + * @author Robert Sedgewick + * @author Kevin Wayne + */ +class Regression @JvmOverloads constructor( + x: DoubleArray, y: DoubleArray, // degree of the polynomial regression + private var degree: Int, // name of the predictor variable + private var variableName: String = "n" +) : Comparable { + private val beta // the polynomial regression coefficients + : Matrix + private val sse // sum of squares due to error + : Double + private var sst = 0.0 // total sum of squares + + init { + val n = x.size + var qr: QRDecomposition? + var matrixX: Matrix? + + // in case Vandermonde matrix does not have full rank, reduce degree until it + // does + while (true) { + + // build Vandermonde matrix + val vandermonde = Array(n) { DoubleArray(degree + 1) } + for (i in 0 until n) { + for (j in 0..degree) { + vandermonde[i][j] = x[i].pow(j.toDouble()) + } + } + matrixX = Matrix(vandermonde) + + // find least squares solution + qr = QRDecomposition(matrixX) + if (qr.isFullRank()) break + + // decrease degree and try again + degree-- + } + + // create matrix from vector + val matrixY = Matrix(y, n) + + // linear regression coefficients + beta = qr!!.solve(matrixY) + + // mean of y[] values + var sum = 0.0 + for (i in 0 until n) sum += y[i] + val mean = sum / n + + // total variation to be accounted for + for (i in 0 until n) { + val dev = y[i] - mean + sst += dev * dev + } + + // variation not accounted for + val residuals: Matrix? = matrixX!!.times(beta).minus(matrixY) + sse = residuals!!.norm2() * residuals.norm2() + } + + /** + * Returns the `j`th regression coefficient. + * + * @param j the index + * @return the `j`th regression coefficient + */ + fun beta(j: Int): Double { + // to make -0.0 print as 0.0 + return if (abs(beta.get(j, 0)) < 1E-4) 0.0 else beta.get(j, 0) + } + + /** + * Returns the degree of the polynomial to fit. + * + * @return the degree of the polynomial to fit + */ + private fun degree(): Int { + return degree + } + + /** + * Returns the coefficient of determination *R*2. + * + * @return the coefficient of determination *R*2, which is a real number between + * 0 and 1 + */ + fun R2(): Double { + return if (sst == 0.0) 1.0 else 1.0 - sse / sst // constant function + } + + /** + * Returns the expected response `y` given the value of the predictor variable `x`. + * + * @param x the value of the predictor variable + * @return the expected response `y` given the value of the predictor variable `x` + */ + fun predict(x: Double): Double { + // horner's method + var y = 0.0 + for (j in degree downTo 0) y = beta(j) + x * y + return y + } + + /** + * Returns a string representation of the polynomial regression model. + * + * @return a string representation of the polynomial regression model, including the best-fit + * polynomial and the coefficient of determination *R*2 + */ + override fun toString(): String { + var s = StringBuilder() + var j = degree + + // ignoring leading zero coefficients + while (j >= 0 && abs(beta(j)) < 1E-5) j-- + + // create remaining terms + while (j >= 0) { + when (j) { + 0 -> s.append(String.format("%.10f ", beta(j))) + 1 -> s.append(String.format("%.10f %s + ", beta(j), variableName)) + else -> s.append( + String.format( + "%.10f %s^%d + ", beta(j), + variableName, j + ) + ) + } + j-- + } + s = s.append(" (R^2 = " + String.format("%.3f", R2()) + ")") + + // replace "+ -2n" with "- 2n" + return s.toString().replace("+ -", "- ") + } + + /** Compare lexicographically. */ + override fun compareTo(other: Regression?): Int { + val EPSILON = 1E-5 + val maxDegree = max(degree().toDouble(), other!!.degree().toDouble()).toInt() + for (j in maxDegree downTo 0) { + var term1 = 0.0 + var term2 = 0.0 + if (degree() >= j) term1 = beta(j) + if (other.degree() >= j) term2 = other.beta(j) + if (abs(term1) < EPSILON) term1 = 0.0 + if (abs(term2) < EPSILON) term2 = 0.0 + if (term1 < term2) return -1 else if (term1 > term2) return +1 + } + return 0 + } +} \ No newline at end of file diff --git a/src/main/kotlin/frc/team449/robot2023/commands/driveAlign/OrbitAlign.kt b/src/main/kotlin/frc/team449/robot2023/commands/driveAlign/OrbitAlign.kt new file mode 100644 index 0000000..bc5aaa5 --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/commands/driveAlign/OrbitAlign.kt @@ -0,0 +1,55 @@ +package frc.team449.robot2023.commands.driveAlign + +import edu.wpi.first.math.MathUtil +import edu.wpi.first.math.controller.PIDController +import edu.wpi.first.math.geometry.Translation2d +import edu.wpi.first.math.kinematics.ChassisSpeeds +import edu.wpi.first.wpilibj2.command.Command +import frc.team449.control.DriveSubsystem +import frc.team449.control.OI +import frc.team449.robot2023.constants.RobotConstants +import frc.team449.robot2023.constants.auto.AutoConstants + +/** + * @param drive The holonomic drive you want to align with + * @param point The point in 2d space you want the drivetrain to face towards + * @param headingPID The non-Profiled PID controller you want to use for fixing rotational error + */ +class OrbitAlign( + private val drive: DriveSubsystem, + private val oi: OI, + private val point: Translation2d, + private val headingPID: PIDController = PIDController( + AutoConstants.ORBIT_KP, + 0.0, + 0.0 + ) +) : Command() { + + init { + addRequirements(drive) + headingPID.enableContinuousInput(-Math.PI, Math.PI) + headingPID.setTolerance(0.015) + } + + private var fieldToRobot = Translation2d() + private var robotToPoint = Translation2d() + + override fun execute() { + fieldToRobot = drive.pose.translation + robotToPoint = point - fieldToRobot + headingPID.setpoint = robotToPoint.angle.radians + + drive.set( + ChassisSpeeds( + oi.get().vxMetersPerSecond, + oi.get().vyMetersPerSecond, + MathUtil.clamp(headingPID.calculate(drive.heading.radians), -RobotConstants.MAX_ROT_SPEED, RobotConstants.MAX_ROT_SPEED) + ) + ) + } + + override fun end(interrupted: Boolean) { + drive.stop() + } +} diff --git a/src/main/kotlin/frc/team449/robot2023/commands/driveAlign/ProfiledPoseAlign.kt b/src/main/kotlin/frc/team449/robot2023/commands/driveAlign/ProfiledPoseAlign.kt new file mode 100644 index 0000000..297c0fe --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/commands/driveAlign/ProfiledPoseAlign.kt @@ -0,0 +1,119 @@ +package frc.team449.robot2023.commands.driveAlign + +import edu.wpi.first.math.controller.PIDController +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.wpilibj.Timer +import edu.wpi.first.wpilibj2.command.Command +import frc.team449.control.holonomic.SwerveDrive +import frc.team449.robot2023.constants.RobotConstants +import frc.team449.robot2023.constants.auto.AutoConstants +import kotlin.math.PI +import kotlin.math.hypot + +/** + * @param drive The holonomic drive you want to align with + * @param targetPose The pose you want to drive up to + * @param xPID The profiled PID controller with constraints you want to use for fixing X error + * @param yPID The profiled PID controller with constraints you want to use for fixing Y error + * @param headingPID The non-Profiled PID controller you want to use for fixing rotational error + * @param tolerance The allowed tolerance from the targetPose + */ +class ProfiledPoseAlign( + private val drive: SwerveDrive, + private val targetPose: Pose2d, + private val xSpeed: Double, + private val ySpeed: Double, + private val xPID: PIDController = PIDController( + AutoConstants.DEFAULT_X_KP, + 0.0, + 0.0 + ), + private val yPID: PIDController = PIDController( + AutoConstants.DEFAULT_Y_KP, + 0.0, + 0.0 + ), + private val headingPID: PIDController = PIDController( + AutoConstants.DEFAULT_ROTATION_KP, + 0.0, + 0.0 + ), + private val xProfile: TrapezoidProfile = TrapezoidProfile(TrapezoidProfile.Constraints(RobotConstants.MAX_LINEAR_SPEED - 1.25, 2.25)), + private val yProfile: TrapezoidProfile = TrapezoidProfile(TrapezoidProfile.Constraints(RobotConstants.MAX_LINEAR_SPEED - 1.25, 2.25)), + private val tolerance: Pose2d = Pose2d(0.05, 0.05, Rotation2d(0.05)), + private val speedTol: Double = 0.05, + private val speedTolRot: Double = 0.05 +) : Command() { + init { + addRequirements(drive) + } + + val timer = Timer() + + override fun initialize() { + headingPID.enableContinuousInput(-PI, PI) + + // Set tolerances from the given pose tolerance + xPID.setTolerance(tolerance.x) + yPID.setTolerance(tolerance.y) + headingPID.setTolerance(tolerance.rotation.radians) + + headingPID.setpoint = targetPose.rotation.radians + + timer.restart() + } + + fun getTime(): Double { + return maxOf(xProfile.totalTime(), yProfile.totalTime(), 0.5) + } + + override fun execute() { + val currTime = timer.get() + + // Calculate the feedback for X, Y, and theta using their respective controllers + + val xProfCalc = xProfile.calculate( + currTime, + TrapezoidProfile.State(targetPose.x, 0.0), + TrapezoidProfile.State(drive.pose.x, xSpeed) + ) + + val yProfCalc = yProfile.calculate( + currTime, + TrapezoidProfile.State(targetPose.y, 0.0), + TrapezoidProfile.State(drive.pose.y, ySpeed) + ) + + val xFeedback = xPID.calculate(drive.pose.x, xProfCalc.position) + val yFeedback = yPID.calculate(drive.pose.y, yProfCalc.position) + val headingFeedback = headingPID.calculate(drive.heading.radians) + + drive.set( + ChassisSpeeds.fromFieldRelativeSpeeds( + xFeedback + xProfCalc.velocity, + yFeedback + yProfCalc.velocity, + headingFeedback, + drive.heading + ) + ) + } + + override fun isFinished(): Boolean { + val currTime = timer.get() + + return xPID.atSetpoint() && yPID.atSetpoint() && headingPID.atSetpoint() && + xProfile.isFinished(currTime) && yProfile.isFinished(currTime) && + hypot( + drive.currentSpeeds.vxMetersPerSecond, + drive.currentSpeeds.vyMetersPerSecond + ) < speedTol && + drive.currentSpeeds.omegaRadiansPerSecond < speedTolRot + } + + override fun end(interrupted: Boolean) { + drive.stop() + } +} diff --git a/src/main/kotlin/frc/team449/robot2023/commands/light/BlairChasing.kt b/src/main/kotlin/frc/team449/robot2023/commands/light/BlairChasing.kt new file mode 100644 index 0000000..1cf81fb --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/commands/light/BlairChasing.kt @@ -0,0 +1,45 @@ +package frc.team449.robot2023.commands.light + +import edu.wpi.first.math.MathUtil +import edu.wpi.first.wpilibj2.command.Command +import frc.team449.robot2023.constants.subsystem.LightConstants +import frc.team449.robot2023.subsystems.light.Light + +/** Description: Have a linear transition from white to red */ +class BlairChasing( + private val led: Light +) : Command() { + + init { + addRequirements(led) + } + + override fun runsWhenDisabled(): Boolean { + return true + } + + private var firstSaturation1 = 0.0 + private var firstSaturation2 = 0.0 + + override fun execute() { + for (i in LightConstants.SECTION_1_START..LightConstants.SECTION_1_END) { + // This number is related to how many lights will show up between the high and low intensity (which technically also affects how fast itll cycle) + val saturation = MathUtil.inputModulus(firstSaturation1 + i * 200.0 / led.buffer.length, 0.0, 255.0) + led.setHSV(i, 0, saturation.toInt(), 255) + + // The i * 255.0 relates to how fast it will cycle in between the high and low intensity + firstSaturation1 += 0.135 + firstSaturation1 = MathUtil.inputModulus(firstSaturation1, 0.0, 255.0) + } + + for (i in LightConstants.SECTION_2_START..LightConstants.SECTION_2_END) { + // This number is related to how many lights will show up between the high and low intensity (which technically also affects how fast itll cycle) + val saturation = MathUtil.inputModulus(firstSaturation2 + (i - LightConstants.SECTION_2_START) * 200.0 / led.buffer.length, 0.0, 255.0) + led.setHSV(i, 0, saturation.toInt(), 255) + + // The i * 255.0 relates to how fast it will cycle in between the high and low intensity + firstSaturation2 += 0.135 + firstSaturation2 = MathUtil.inputModulus(firstSaturation2, 0.0, 255.0) + } + } +} diff --git a/src/main/kotlin/frc/team449/robot2023/commands/light/BreatheHue.kt b/src/main/kotlin/frc/team449/robot2023/commands/light/BreatheHue.kt new file mode 100644 index 0000000..4fc7a9f --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/commands/light/BreatheHue.kt @@ -0,0 +1,47 @@ +package frc.team449.robot2023.commands.light + +import edu.wpi.first.math.MathUtil +import edu.wpi.first.wpilibj2.command.Command +import frc.team449.robot2023.constants.subsystem.LightConstants +import frc.team449.robot2023.subsystems.light.Light + +/** Description: Have a linear transition from a darker to brighter specified hue */ +class BreatheHue( + private val led: Light, + private val hue: Int +) : Command() { + + init { + addRequirements(led) + } + + override fun runsWhenDisabled(): Boolean { + return true + } + + private var firstIntensity1 = 175.0 + private var firstIntensity2 = 175.0 + + + override fun execute() { + for (i in LightConstants.SECTION_1_START..LightConstants.SECTION_1_END) { + // This number is related to how many lights will show up between the high and low intensity + val intensity = MathUtil.inputModulus(firstIntensity1 + i * 32.5 / led.buffer.length, 125.0, 255.0) + led.setHSV(i, hue, 255, intensity.toInt()) + + // The i * 255.0 relates to how fast it will cycle in between the high and low intensity + firstIntensity1 += 0.1 + firstIntensity1 = MathUtil.inputModulus(firstIntensity1, 125.0, 255.0) + } + + for (i in LightConstants.SECTION_2_START..LightConstants.SECTION_2_END) { + // This number is related to how many lights will show up between the high and low intensity + val intensity = MathUtil.inputModulus(firstIntensity2 + (i - LightConstants.SECTION_2_START) * 32.5 / led.buffer.length, 125.0, 255.0) + led.setHSV(i, hue, 255, intensity.toInt()) + + // The i * 255.0 relates to how fast it will cycle in between the high and low intensity + firstIntensity2 += 0.1 + firstIntensity2 = MathUtil.inputModulus(firstIntensity2, 125.0, 255.0) + } + } +} diff --git a/src/main/kotlin/frc/team449/robot2023/commands/light/Crazy.kt b/src/main/kotlin/frc/team449/robot2023/commands/light/Crazy.kt new file mode 100644 index 0000000..ee3ba56 --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/commands/light/Crazy.kt @@ -0,0 +1,26 @@ +package frc.team449.robot2023.commands.light + +import edu.wpi.first.wpilibj2.command.Command +import frc.team449.robot2023.subsystems.light.Light +import kotlin.random.Random + +/** Description: Have a random color set for every led every 20ms + * a.k.a. you go blind */ +class Crazy( + private val led: Light +) : Command() { + + init { + addRequirements(led) + } + + override fun runsWhenDisabled(): Boolean { + return true + } + + override fun execute() { + for (i in 0 until led.buffer.length) { + led.setRGB(i, Random.nextInt(0, 256), Random.nextInt(0, 256), Random.nextInt(0, 256)) + } + } +} diff --git a/src/main/kotlin/frc/team449/robot2023/commands/light/PickupBlink.kt b/src/main/kotlin/frc/team449/robot2023/commands/light/PickupBlink.kt new file mode 100644 index 0000000..078315d --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/commands/light/PickupBlink.kt @@ -0,0 +1,33 @@ +package frc.team449.robot2023.commands.light + +import edu.wpi.first.wpilibj2.command.Command +import edu.wpi.first.wpilibj2.command.InstantCommand +import edu.wpi.first.wpilibj2.command.SequentialCommandGroup +import edu.wpi.first.wpilibj2.command.WaitCommand +import frc.team449.robot2023.subsystems.light.Light + +/** Description: Blink a certain color 5 times */ +class PickupBlink { + fun blinkGreen(light: Light): Command { + val cmdGroup = SequentialCommandGroup() + + cmdGroup.addRequirements(light) + + for (x in 0 until 5) { + cmdGroup.addCommands(InstantCommand({ setColor(light, 0, 255, 0) })) + cmdGroup.addCommands(WaitCommand(0.1)) + cmdGroup.addCommands(InstantCommand({ setColor(light, 0, 0, 0) })) + cmdGroup.addCommands(WaitCommand(0.1)) + } + + cmdGroup.ignoringDisable(true) + + return cmdGroup + } + + private fun setColor(led: Light, r: Int, g: Int, b: Int) { + for (i in 0 until led.buffer.length) { + led.setRGB(i, r, g, b) + } + } +} diff --git a/src/main/kotlin/frc/team449/robot2023/commands/light/Rainbow.kt b/src/main/kotlin/frc/team449/robot2023/commands/light/Rainbow.kt new file mode 100644 index 0000000..cc822cd --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/commands/light/Rainbow.kt @@ -0,0 +1,38 @@ +package frc.team449.robot2023.commands.light + +import edu.wpi.first.math.MathUtil +import edu.wpi.first.wpilibj2.command.Command +import frc.team449.robot2023.constants.subsystem.LightConstants +import frc.team449.robot2023.subsystems.light.Light + +class Rainbow( + private val led: Light +) : Command() { + + init { + addRequirements(led) + } + + override fun runsWhenDisabled(): Boolean { + return true + } + + private var firstHue = 0.0 + + override fun execute() { + for (i in LightConstants.SECTION_1_START..LightConstants.SECTION_1_END) { + val hue = MathUtil.inputModulus(firstHue + i * 180 / (LightConstants.SECTION_1_END - LightConstants.SECTION_1_START), 0.0, 180.0) + + led.setHSV(i, hue.toInt(), 255, 255) + } + + for (i in LightConstants.SECTION_2_START..LightConstants.SECTION_2_END) { + val hue = MathUtil.inputModulus(firstHue + (i - LightConstants.SECTION_2_START) * 180 / (LightConstants.SECTION_2_END - LightConstants.SECTION_2_START), 0.0, 180.0) + + led.setHSV(i, hue.toInt(), 255, 255) + } + + firstHue += 6.15 + firstHue = MathUtil.inputModulus(firstHue, 0.0, 180.0) + } +} diff --git a/src/main/kotlin/frc/team449/robot2023/commands/light/SolidColor.kt b/src/main/kotlin/frc/team449/robot2023/commands/light/SolidColor.kt new file mode 100644 index 0000000..0449d6a --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/commands/light/SolidColor.kt @@ -0,0 +1,27 @@ +package frc.team449.robot2023.commands.light + +import edu.wpi.first.wpilibj2.command.Command +import frc.team449.robot2023.subsystems.light.Light + +/** Description: Have a solid color */ +class SolidColor( + private val led: Light, + private val r: Int, + private val g: Int, + private val b: Int +) : Command() { + + init { + addRequirements(led) + } + + override fun runsWhenDisabled(): Boolean { + return true + } + + override fun execute() { + for (i in 0 until led.buffer.length) { + led.setRGB(i, r, g, b) + } + } +} diff --git a/src/main/kotlin/frc/team449/robot2023/constants/MotorConstants.kt b/src/main/kotlin/frc/team449/robot2023/constants/MotorConstants.kt new file mode 100644 index 0000000..e760563 --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/constants/MotorConstants.kt @@ -0,0 +1,12 @@ +package frc.team449.robot2023.constants + +import edu.wpi.first.math.util.Units + +object MotorConstants { + /** NEO characteristics */ + const val NOMINAL_VOLTAGE = 12.0 + const val STALL_TORQUE = 3.36 + const val STALL_CURRENT = 166.0 + const val FREE_CURRENT = 1.3 + val FREE_SPEED = Units.rotationsPerMinuteToRadiansPerSecond(5676.0) +} diff --git a/src/main/kotlin/frc/team449/robot2023/constants/RobotConstants.kt b/src/main/kotlin/frc/team449/robot2023/constants/RobotConstants.kt new file mode 100644 index 0000000..380f395 --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/constants/RobotConstants.kt @@ -0,0 +1,61 @@ +package frc.team449.robot2023.constants + +import edu.wpi.first.math.controller.PIDController +import edu.wpi.first.math.geometry.Pose2d +import edu.wpi.first.math.geometry.Rotation2d +import edu.wpi.first.math.system.plant.DCMotor +import edu.wpi.first.math.util.Units +import frc.team449.robot2023.constants.drives.SwerveConstants +import kotlin.math.PI + +object RobotConstants { + + /** Other CAN ID */ + const val PDH_CAN = 1 + + /** Controller Configurations */ + const val ROT_RATE_LIMIT = 4.0 * PI + const val NEG_ROT_RATE_LIM = -8.0 * PI + const val TRANSLATION_DEADBAND = .15 + const val ROTATION_DEADBAND = .15 + + /** In kilograms, include bumpers and battery and all */ + const val ROBOT_WEIGHT = 55 + + /** Drive configuration */ + const val MAX_LINEAR_SPEED = SwerveConstants.MAX_ATTAINABLE_MK4I_SPEED // m/s + const val MAX_ROT_SPEED = PI // rad/s + val MAX_ACCEL = 4 * DCMotor( + MotorConstants.NOMINAL_VOLTAGE, + MotorConstants.STALL_TORQUE * SwerveConstants.EFFICIENCY, + MotorConstants.STALL_CURRENT, + MotorConstants.FREE_CURRENT, + MotorConstants.FREE_SPEED, + 1 + ).getTorque(55.0) / + (Units.inchesToMeters(2.0) * ROBOT_WEIGHT * SwerveConstants.DRIVE_GEARING) // m/s/s + + val INITIAL_POSE = Pose2d(0.0, 0.0, Rotation2d.fromDegrees(0.0)) + + + init { + println("Max Accel $MAX_ACCEL") + } + + const val LOOP_TIME = 0.020 + + /** PID controller for Orthogonal turning */ + val ORTHOGONAL_CONTROLLER = PIDController( + 3.0, + 0.0, + 0.0 + ) + + const val ALIGN_ROT_SPEED = 3 * PI / 2 + + val IR_CHANNEL = 15 + + // Robot dimensions (INCLUDING BUMPERS) + val ROBOT_WIDTH = Units.inchesToMeters(27.0 + 3.25 * 2) + val ROBOT_LENGTH = Units.inchesToMeters(30.0 + 3.25 * 2) +} diff --git a/src/main/kotlin/frc/team449/robot2023/constants/auto/AutoConstants.kt b/src/main/kotlin/frc/team449/robot2023/constants/auto/AutoConstants.kt new file mode 100644 index 0000000..55f3ee0 --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/constants/auto/AutoConstants.kt @@ -0,0 +1,12 @@ +package frc.team449.robot2023.constants.auto + +import kotlin.math.PI + +object AutoConstants { + /** PID gains */ + const val DEFAULT_X_KP = 1.875 + const val DEFAULT_Y_KP = 1.875 + const val DEFAULT_ROTATION_KP = 2.0 + + const val ORBIT_KP = 2 * PI +} diff --git a/src/main/kotlin/frc/team449/robot2023/constants/drives/DifferentialConstants.kt b/src/main/kotlin/frc/team449/robot2023/constants/drives/DifferentialConstants.kt new file mode 100644 index 0000000..0092955 --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/constants/drives/DifferentialConstants.kt @@ -0,0 +1,53 @@ +package frc.team449.robot2023.constants.drives + +import edu.wpi.first.math.controller.DifferentialDriveFeedforward +import edu.wpi.first.math.util.Units +import edu.wpi.first.wpilibj.Encoder +import java.lang.Math.PI + +/** Constants for a differential drivetrain */ +object DifferentialConstants { + + /** Drive motor CAN ID */ + const val DRIVE_MOTOR_L = 2 + const val DRIVE_MOTOR_L1 = 4 + const val DRIVE_MOTOR_L2 = 3 + const val DRIVE_MOTOR_R = 1 + const val DRIVE_MOTOR_R1 = 11 + const val DRIVE_MOTOR_R2 = 7 + + /** Angular feed forward gains. */ + const val DRIVE_ANGLE_FF_KS = 0.20112 + const val DRIVE_ANGLE_FF_KV = 10.05 + const val DRIVE_ANGLE_FF_KA = 0.505 + + /** Control Constants */ + const val DRIVE_KP = .0 + const val DRIVE_KI = .0 + const val DRIVE_KD = .0 + const val DRIVE_FF_KS = 0.1908 + const val DRIVE_FF_KV = 2.5406 + const val DRIVE_FF_KA = 0.44982 + + /** Encoder Characteristics */ + val DRIVE_ENC_RIGHT = Encoder(4, 5) + val DRIVE_ENC_LEFT = Encoder(6, 7) + const val NEO_ENCODER_CPR = 1 + const val DRIVE_EXT_ENC_CPR = 256 + + /** Drive Characteristics */ + const val DRIVE_GEARING = 1.0 + val DRIVE_WHEEL_RADIUS = Units.inchesToMeters(2.0) + val DRIVE_UPR = 2 * PI * DRIVE_WHEEL_RADIUS + const val DRIVE_CURRENT_LIM = 35 + const val DRIVE_ENC_VEL_THRESHOLD = 999999.0 + const val TRACK_WIDTH = .615 // m + + val DRIVE_FEED_FORWARD = DifferentialDriveFeedforward( + DRIVE_FF_KV, + DRIVE_FF_KA, + DRIVE_ANGLE_FF_KV, + DRIVE_ANGLE_FF_KA, + TRACK_WIDTH + ) +} diff --git a/src/main/kotlin/frc/team449/robot2023/constants/drives/MecanumConstants.kt b/src/main/kotlin/frc/team449/robot2023/constants/drives/MecanumConstants.kt new file mode 100644 index 0000000..24ac303 --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/constants/drives/MecanumConstants.kt @@ -0,0 +1,30 @@ +package frc.team449.robot2023.constants.drives + +import edu.wpi.first.math.util.Units + +object MecanumConstants { + + /** Drive motor ports */ + const val DRIVE_MOTOR_FL = 1 + const val DRIVE_MOTOR_FR = 2 + const val DRIVE_MOTOR_BL = 3 + const val DRIVE_MOTOR_BR = 4 + + /** Feed forward values for driving each module */ + const val DRIVE_KS = 0.16475 + const val DRIVE_KV = 2.0909 + const val DRIVE_KA = 0.29862 + + /** PID gains for driving each module*/ + const val DRIVE_KP = 0.35 + const val DRIVE_KI = 0.0 + const val DRIVE_KD = 0.0 + + /** Drive configuration */ + const val DRIVE_GEARING = 1 / 8.0 + val DRIVE_UPR = Math.PI * Units.inchesToMeters(6.0) + const val MAX_ATTAINABLE_WHEEL_SPEED = (12 - DRIVE_KS) / DRIVE_KV + val WHEELBASE = Units.inchesToMeters(21.426) + val TRACKWIDTH = Units.inchesToMeters(21.000) + const val CURRENT_LIM = 40 +} diff --git a/src/main/kotlin/frc/team449/robot2023/constants/drives/SwerveConstants.kt b/src/main/kotlin/frc/team449/robot2023/constants/drives/SwerveConstants.kt new file mode 100644 index 0000000..3c2a48b --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/constants/drives/SwerveConstants.kt @@ -0,0 +1,61 @@ +package frc.team449.robot2023.constants.drives + +import edu.wpi.first.math.util.Units + +object SwerveConstants { + const val EFFICIENCY = 0.95 + + /** Drive motor ports */ + const val DRIVE_MOTOR_FL = 11 + const val DRIVE_MOTOR_FR = 17 + const val DRIVE_MOTOR_BL = 38 + const val DRIVE_MOTOR_BR = 22 + const val TURN_MOTOR_FL = 20 + const val TURN_MOTOR_FR = 3 + const val TURN_MOTOR_BL = 41 + const val TURN_MOTOR_BR = 45 + + /** Turning encoder channels */ + const val TURN_ENC_CHAN_FL = 6 + const val TURN_ENC_CHAN_FR = 9 + const val TURN_ENC_CHAN_BL = 5 + const val TURN_ENC_CHAN_BR = 8 + + /** Offsets for the absolute encoders in rotations */ + const val TURN_ENC_OFFSET_FL = 0.396987 + const val TURN_ENC_OFFSET_FR = 0.389095 + const val TURN_ENC_OFFSET_BL = 0.787267 - 0.5 + const val TURN_ENC_OFFSET_BR = 0.215026 + + /** PID gains for turning each module */ + const val TURN_KP = 0.85 + const val TURN_KI = 0.0 + const val TURN_KD = 0.0 + + /** Feed forward values for driving each module */ + const val DRIVE_KS = 0.2491250419322037 + const val DRIVE_KV = 2.352910954352485 + const val DRIVE_KA = 0.42824 + + // TODO: Figure out this value + const val STEER_KS = 1.0 + + /** PID gains for driving each module*/ + const val DRIVE_KP = 0.4 + const val DRIVE_KI = 0.0 + const val DRIVE_KD = 0.0 + + /** Drive configuration */ + const val DRIVE_GEARING = 1 / 6.12 + const val DRIVE_UPR = 0.31818905832 + const val TURN_UPR = 2 * Math.PI + const val MAX_ATTAINABLE_MK4I_SPEED = (12 - DRIVE_KS) / DRIVE_KV + const val DRIVE_CURRENT_LIM = 55 + const val STEERING_CURRENT_LIM = 40 + const val JOYSTICK_FILTER_ORDER = 2 + + /** Wheelbase = wheel-to-wheel distance from front to back of the robot */ + /** Trackwidth = wheel-to-wheel distance from side to side of the robot */ + val WHEELBASE = Units.inchesToMeters(24.75) // ex. FL to BL + val TRACKWIDTH = Units.inchesToMeters(21.75) // ex. BL to BR +} diff --git a/src/main/kotlin/frc/team449/robot2023/constants/field/FieldConstants.kt b/src/main/kotlin/frc/team449/robot2023/constants/field/FieldConstants.kt new file mode 100644 index 0000000..a6071ba --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/constants/field/FieldConstants.kt @@ -0,0 +1,43 @@ +package frc.team449.robot2023.constants.field + +import edu.wpi.first.math.geometry.Pose2d +import edu.wpi.first.math.geometry.Rotation2d +import edu.wpi.first.math.util.Units +import frc.team449.robot2023.constants.RobotConstants + +object FieldConstants { + val fieldLength = Units.feetToMeters(54.0) + val fieldWidth = Units.feetToMeters(27.0) + + val wallNodeY = Units.inchesToMeters(20.19) + val nodeSeparationY = Units.inchesToMeters(22.0) + + val nodeX = Units.inchesToMeters(54.05) + RobotConstants.ROBOT_LENGTH / 2 + + val midNodeFromEdge = Units.inchesToMeters(22.7) + val highNodeFromEdge = Units.inchesToMeters(39.73) + + val PlacementPositions: Map = mapOf( + TargetPosition.Position1 to Pose2d(nodeX, wallNodeY, Rotation2d()), + TargetPosition.Position2 to Pose2d(nodeX, wallNodeY + nodeSeparationY, Rotation2d()), + TargetPosition.Position3 to Pose2d(nodeX, wallNodeY + 2 * nodeSeparationY, Rotation2d()), + TargetPosition.Position4 to Pose2d(nodeX, wallNodeY + 3 * nodeSeparationY, Rotation2d()), + TargetPosition.Position5 to Pose2d(nodeX, wallNodeY + 4 * nodeSeparationY, Rotation2d()), + TargetPosition.Position6 to Pose2d(nodeX, wallNodeY + 5 * nodeSeparationY, Rotation2d()), + TargetPosition.Position7 to Pose2d(nodeX, wallNodeY + 6 * nodeSeparationY, Rotation2d()), + TargetPosition.Position8 to Pose2d(nodeX, wallNodeY + 7 * nodeSeparationY, Rotation2d()), + TargetPosition.Position9 to Pose2d(nodeX, wallNodeY + 8 * nodeSeparationY, Rotation2d()) + ) + + enum class TargetPosition { + Position1, + Position2, + Position3, + Position4, + Position5, + Position6, + Position7, + Position8, + Position9 + } +} diff --git a/src/main/kotlin/frc/team449/robot2023/constants/subsystem/LightConstants.kt b/src/main/kotlin/frc/team449/robot2023/constants/subsystem/LightConstants.kt new file mode 100644 index 0000000..7576685 --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/constants/subsystem/LightConstants.kt @@ -0,0 +1,11 @@ +package frc.team449.robot2023.constants.subsystem + +object LightConstants { + const val LIGHT_PORT = 9 + const val LIGHT_LENGTH = 24 + + const val SECTION_1_START = 0 + const val SECTION_1_END = 11 + const val SECTION_2_START = 12 + const val SECTION_2_END = 23 +} diff --git a/src/main/kotlin/frc/team449/robot2023/constants/vision/VisionConstants.kt b/src/main/kotlin/frc/team449/robot2023/constants/vision/VisionConstants.kt new file mode 100644 index 0000000..2f836b7 --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/constants/vision/VisionConstants.kt @@ -0,0 +1,93 @@ +package frc.team449.robot2023.constants.vision + +import edu.wpi.first.apriltag.AprilTag +import edu.wpi.first.apriltag.AprilTagFieldLayout +import edu.wpi.first.math.MatBuilder +import edu.wpi.first.math.Matrix +import edu.wpi.first.math.Nat +import edu.wpi.first.math.geometry.Pose3d +import edu.wpi.first.math.geometry.Rotation3d +import edu.wpi.first.math.geometry.Transform3d +import edu.wpi.first.math.geometry.Translation3d +import edu.wpi.first.math.numbers.N1 +import edu.wpi.first.math.numbers.N3 +import edu.wpi.first.math.util.Units +import edu.wpi.first.wpilibj.Filesystem +import frc.team449.control.vision.VisionSubsystem +import org.photonvision.estimation.TargetModel +import org.photonvision.simulation.VisionSystemSim + +/** Constants that have anything to do with vision */ +object VisionConstants { + /** How the tags are laid out on the field (their locations and ids) */ + private val TEST_TAG_LAYOUT = AprilTagFieldLayout( + listOf( + AprilTag(3, Pose3d()) + ), + 16.4846, + 8.1026 + ) + + /** WPILib's AprilTagFieldLayout for the 2023 Charged Up Game */ + val TAG_LAYOUT: AprilTagFieldLayout = AprilTagFieldLayout( + Filesystem.getDeployDirectory().absolutePath.plus("/vision/Bunnybots2023.json") + ) + +// AprilTagFieldLayout.loadFromResource( +// AprilTagFields.k2023ChargedUp.m_resourceFile +// ) + + /** Robot to Camera distance */ + val robotToCamera = Transform3d( + Translation3d(Units.inchesToMeters(-11.48657), Units.inchesToMeters(0.0), Units.inchesToMeters(8.3416)), + Rotation3d(0.0, Units.degreesToRadians(15.0), Units.degreesToRadians(180.0)) + ) + + val TAG_MODEL = TargetModel( + Units.inchesToMeters(6.5), + Units.inchesToMeters(6.5) + ) + + /** Filtering Constants */ + const val MAX_AMBIGUITY = 0.325 + const val MAX_DISTANCE_SINGLE_TAG = 3.45 + const val MAX_DISTANCE_MULTI_TAG = 4.5 + const val SINGLE_TAG_HEADING_MAX_DEV_DEG = 5.0 + const val MAX_HEIGHT_ERR_METERS = 0.075 + const val TAG_MULTIPLIER = 0.5 + + /** Std Dev Calculation Constants */ + const val ORDER = 2 + const val PROPORTION = 1 / 25 + + val VISION_SIM = VisionSystemSim( + "main" + ) + + /** Vision Sim Setup Constants */ + const val SIM_FPS = 25.0 + const val SIM_CAMERA_HEIGHT_PX = 720 + const val SIM_CAMERA_WIDTH_PX = 1280 + const val SIM_FOV_DEG = 75.0 + const val SIM_CALIB_AVG_ERR_PX = 0.35 + const val SIM_CALIB_ERR_STDDEV_PX = 0.10 + const val SIM_AVG_LATENCY = 50.0 + const val SIM_STDDEV_LATENCY = 10.0 + const val ENABLE_WIREFRAME = true + + + /** List of cameras that we want to use */ + val ESTIMATORS: ArrayList = arrayListOf( + VisionSubsystem( + "arducam", + TAG_LAYOUT, + robotToCamera, + VISION_SIM + ) + ) + + val ENCODER_TRUST: Matrix = MatBuilder(Nat.N3(), Nat.N1()).fill(.085, .085, .015) + val SINGLE_TAG_TRUST: Matrix = MatBuilder(Nat.N3(), Nat.N1()).fill(.125, .125, .80) + val MULTI_TAG_TRUST: Matrix = MatBuilder(Nat.N3(), Nat.N1()).fill(.0275, .0275, .30) + +} diff --git a/src/main/kotlin/frc/team449/robot2023/subsystems/ControllerBindings.kt b/src/main/kotlin/frc/team449/robot2023/subsystems/ControllerBindings.kt new file mode 100644 index 0000000..66855b7 --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/subsystems/ControllerBindings.kt @@ -0,0 +1,53 @@ +package frc.team449.robot2023.subsystems + +import edu.wpi.first.math.geometry.Rotation2d +import edu.wpi.first.wpilibj.XboxController +import edu.wpi.first.wpilibj2.command.* +import edu.wpi.first.wpilibj2.command.button.JoystickButton +import edu.wpi.first.wpilibj2.command.button.Trigger +import frc.team449.robot2023.Robot +import frc.team449.robot2023.commands.characterization.Characterization +import frc.team449.robot2023.constants.RobotConstants +import kotlin.math.PI + + +class ControllerBindings( + private val driveController: XboxController, + private val mechanismController: XboxController, + private val robot: Robot +) { + + fun bindButtons() { + // slow drive + Trigger { driveController.rightTriggerAxis >= .75 }.onTrue( + InstantCommand({ robot.drive.maxLinearSpeed = 1.0 }).andThen( + InstantCommand({ robot.drive.maxRotSpeed = PI / 4 }) + ) + ).onFalse( + InstantCommand({ robot.drive.maxLinearSpeed = RobotConstants.MAX_LINEAR_SPEED }).andThen( + InstantCommand({ robot.drive.maxRotSpeed = RobotConstants.MAX_ROT_SPEED }) + ) + ) + + // reset gyro + JoystickButton(driveController, XboxController.Button.kStart.value).onTrue( + InstantCommand({ + robot.drive.heading = Rotation2d() + }) + ) + + // characterize + JoystickButton(driveController, XboxController.Button.kA.value).onTrue( + Characterization( + robot.drive, + true, + "swerve drive", + robot.drive::setVoltage, + robot.drive::getModuleVel + ).withInterruptBehavior(Command.InterruptionBehavior.kCancelIncoming) + ).onFalse( + robot.driveCommand + ) + + } +} diff --git a/src/main/kotlin/frc/team449/robot2023/subsystems/light/Light.kt b/src/main/kotlin/frc/team449/robot2023/subsystems/light/Light.kt new file mode 100644 index 0000000..4ca5771 --- /dev/null +++ b/src/main/kotlin/frc/team449/robot2023/subsystems/light/Light.kt @@ -0,0 +1,88 @@ +package frc.team449.robot2023.subsystems.light + +import edu.wpi.first.wpilibj.AddressableLED +import edu.wpi.first.wpilibj.AddressableLEDBuffer +import edu.wpi.first.wpilibj.RobotBase +import edu.wpi.first.wpilibj2.command.SubsystemBase +import frc.team449.robot2023.constants.subsystem.LightConstants + +/** + * Controls an LED strip. + * @param port The PWM port of the LED strip. + * @param length The length of the LED strip. + */ + +class Light( + port: Int, + length: Int +) : SubsystemBase() { + + private val lightStrip = AddressableLED(port) + val buffer = AddressableLEDBuffer(length) + + init { + lightStrip.setLength(buffer.length) + lightStrip.setData(buffer) + lightStrip.start() + } + + override fun periodic() { + lightStrip.setData(buffer) + } + + /** Copy the AddressableLEDBuffer setHsv() except to support + * GRB LED strips */ + fun setHSV(index: Int, h: Int, s: Int, v: Int) { + if (RobotBase.isReal()) { + if (s == 0) { + buffer.setRGB(index, v, v, v) + return + } + + // Difference between highest and lowest value of any rgb component + val chroma = s * v / 255 + + // Because hue is 0-180 rather than 0-360 use 30 not 60 + val region = h / 30 % 6 + + // Remainder converted from 0-30 to 0-255 + val remainder = Math.round(h % 30 * (255 / 30.0)).toInt() + + // Value of the lowest rgb component + val m = v - chroma + + // Goes from 0 to chroma as hue increases + val X = chroma * remainder shr 8 + when (region) { + 0 -> buffer.setRGB(index, X + m, v, m) + 1 -> buffer.setRGB(index, v, v - X, m) + 2 -> buffer.setRGB(index, v, m, X + m) + 3 -> buffer.setRGB(index, v - X, m, v) + 4 -> buffer.setRGB(index, m, X + m, v) + else -> buffer.setRGB(index, m, v, v - X) + } + } else { + buffer.setHSV(index, h, s, v) + } + } + + /** Copy the AddressableLEDBuffer setRGB() except to support + * GRB LED strips */ + fun setRGB(index: Int, r: Int, g: Int, b: Int) { + if (RobotBase.isReal()) { + buffer.setRGB(index, g, r, b) + } else { + buffer.setRGB(index, r, g, b) + } + } + + companion object { + /** Create an LED strip controller using [LightConstants]. */ + fun createLight(): Light { + return Light( + LightConstants.LIGHT_PORT, + LightConstants.LIGHT_LENGTH + ) + } + } +} diff --git a/src/main/kotlin/frc/team449/system/AHRS.kt b/src/main/kotlin/frc/team449/system/AHRS.kt new file mode 100644 index 0000000..f10503f --- /dev/null +++ b/src/main/kotlin/frc/team449/system/AHRS.kt @@ -0,0 +1,83 @@ +package frc.team449.system + +import edu.wpi.first.math.filter.LinearFilter +import edu.wpi.first.math.geometry.Rotation2d +import edu.wpi.first.wpilibj.SPI +import edu.wpi.first.wpilibj.Timer +import edu.wpi.first.wpilibj.simulation.SimDeviceSim +import frc.team449.util.simBooleanProp +import frc.team449.util.simDoubleProp + +class AHRS( + private val navx: com.kauailabs.navx.frc.AHRS +) { + + var prevPos = Double.NaN + var prevTime = Double.NaN + + private val filter = LinearFilter.movingAverage(5) + + /** The current reading of the gyro with the offset included */ + val heading: Rotation2d + get() { + return -Rotation2d.fromDegrees(navx.fusedHeading.toDouble()) + } + + val pitch: Rotation2d + get() { + return -Rotation2d.fromDegrees(navx.pitch.toDouble()) + } + + val roll: Rotation2d + get() { + return -Rotation2d.fromDegrees(navx.roll.toDouble()) + } + + fun angularXVel(): Double { + val currPos = navx.roll.toDouble() + val currTime = Timer.getFPGATimestamp() + + val vel = if (prevPos.isNaN()) { + 0.0 + } else { + val dt = currTime - prevTime + val dx = currPos - prevPos + dx / dt + } + this.prevTime = currTime + this.prevPos = currPos + return filter.calculate(vel) + } + + constructor( + port: SPI.Port = SPI.Port.kMXP + ) : this( + com.kauailabs.navx.frc.AHRS(port) + ) + + fun calibrated(): Boolean { + return navx.isMagnetometerCalibrated + } + + /** + * Used to set properties of an [AHRS] object during simulation. See + * https://pdocs.kauailabs.com/navx-mxp/softwa + * re/roborio-libraries/java/ + * + * @param devName The name of the simulated device. + * @param index The NavX index. + */ + class SimController(devName: String = "navX-Sensor", index: Int = 0) { + private val deviceSim = SimDeviceSim(devName, index) + + var isConnected by simBooleanProp(deviceSim.getBoolean("Connected")) + var yaw by simDoubleProp(deviceSim.getDouble("Yaw")) + var pitch by simDoubleProp(deviceSim.getDouble("Pitch")) + var roll by simDoubleProp(deviceSim.getDouble("Roll")) + var compassHeading by simDoubleProp(deviceSim.getDouble("CompassHeading")) + var fusedHeading by simDoubleProp(deviceSim.getDouble("FusedHeading")) + var linearWorldAccelX by simDoubleProp(deviceSim.getDouble("LinearWorldAccelX")) + var linearWorldAccelY by simDoubleProp(deviceSim.getDouble("LinearWorldAccelX")) + var linearWorldAccelZ by simDoubleProp(deviceSim.getDouble("LinearWorldAccelY")) + } +} diff --git a/src/main/kotlin/frc/team449/system/SimBattery.kt b/src/main/kotlin/frc/team449/system/SimBattery.kt new file mode 100644 index 0000000..463e17d --- /dev/null +++ b/src/main/kotlin/frc/team449/system/SimBattery.kt @@ -0,0 +1,27 @@ +package frc.team449.system + +import edu.wpi.first.wpilibj.simulation.BatterySim +import edu.wpi.first.wpilibj.simulation.RoboRioSim + +/** + * Used for simulating battery voltage and current based off drawn currents from other mechanisms + */ +class SimBattery { + private val currentDrawers = mutableListOf<() -> Double>() + + /** + * Register a simulated mechanism drawing current + */ + fun addCurrentDrawer(currentDrawer: () -> Double) { + currentDrawers.add(currentDrawer) + } + + /** + * Update the simulation with a newly calculated battery current and voltage + */ + fun update() { + val currents = currentDrawers.map { it() } + RoboRioSim.setVInCurrent(currents.sum()) + RoboRioSim.setVInVoltage(BatterySim.calculateDefaultBatteryLoadedVoltage(*currents.toDoubleArray())) + } +} diff --git a/src/main/kotlin/frc/team449/system/SparkUtil.kt b/src/main/kotlin/frc/team449/system/SparkUtil.kt new file mode 100644 index 0000000..b62928d --- /dev/null +++ b/src/main/kotlin/frc/team449/system/SparkUtil.kt @@ -0,0 +1,79 @@ +package frc.team449.system + +import com.revrobotics.* +import edu.wpi.first.wpilibj.RobotController + +object SparkUtil { + fun applySparkSettings( + + // spark max information + sparkMax: CANSparkMax, + enableBrakeMode: Boolean = true, + inverted: Boolean = false, + currentLimit: Int = 0, + enableVoltageComp: Boolean = false, + slaveSparks: Map = mapOf(), + controlFrameRateMillis: Int = -1, + statusFrameRatesMillis: Map = mapOf(), + + // encoder information + encoder: MotorFeedbackSensor, + unitPerRotation: Double, + gearing: Double, + offset: Double = 0.0, + encInverted: Boolean = false + ) { + sparkMax.restoreFactoryDefaults() + sparkMax.idleMode = if (enableBrakeMode) CANSparkMax.IdleMode.kBrake else CANSparkMax.IdleMode.kCoast + sparkMax.inverted = inverted + if (currentLimit > 0) sparkMax.setSmartCurrentLimit(currentLimit) + if (enableVoltageComp) sparkMax.enableVoltageCompensation(RobotController.getBatteryVoltage()) else sparkMax.disableVoltageCompensation() + if (controlFrameRateMillis >= 1) sparkMax.setControlFramePeriodMs(controlFrameRateMillis) // Must be between 1 and 100 ms. + for ((statusFrame, period) in statusFrameRatesMillis) { + sparkMax.setPeriodicFramePeriod(statusFrame, period) + } + + for ((slavePort, slaveInverted) in slaveSparks) { + val slave = CANSparkMax(slavePort, CANSparkMaxLowLevel.MotorType.kBrushless) + slave.restoreFactoryDefaults() + slave.follow(sparkMax, slaveInverted) + slave.idleMode = sparkMax.idleMode + if (currentLimit > 0) slave.setSmartCurrentLimit(currentLimit) + slave.burnFlash() + } + + when (encoder) { + is SparkMaxAbsoluteEncoder -> { + encoder.positionConversionFactor = unitPerRotation * gearing + encoder.velocityConversionFactor = unitPerRotation * gearing / 60 + encoder.zeroOffset = offset + encoder.setInverted(encInverted) + } + + is SparkMaxRelativeEncoder -> { + encoder.positionConversionFactor = unitPerRotation * gearing + encoder.velocityConversionFactor = unitPerRotation * gearing / 60 + } + + is SparkMaxAlternateEncoder -> { + encoder.positionConversionFactor = unitPerRotation * gearing + encoder.velocityConversionFactor = unitPerRotation * gearing / 60 + encoder.setInverted(encInverted) + } + + else -> throw IllegalStateException("UNSUPPORTED ENCODER PLUGGED INTO SPARK MAX.") + } + + sparkMax.pidController.setFeedbackDevice(encoder) + + sparkMax.burnFlash() + } + + fun enableContinuousInput(sparkMax: CANSparkMax, min: Double, max: Double) { + val controller = sparkMax.pidController + + controller.setPositionPIDWrappingEnabled(true) + controller.setPositionPIDWrappingMinInput(min) + controller.setPositionPIDWrappingMaxInput(max) + } +} diff --git a/src/main/kotlin/frc/team449/system/encoder/AbsoluteEncoder.kt b/src/main/kotlin/frc/team449/system/encoder/AbsoluteEncoder.kt new file mode 100644 index 0000000..4016e90 --- /dev/null +++ b/src/main/kotlin/frc/team449/system/encoder/AbsoluteEncoder.kt @@ -0,0 +1,105 @@ +package frc.team449.system.encoder + +import edu.wpi.first.math.MathUtil +import edu.wpi.first.math.filter.MedianFilter +import edu.wpi.first.wpilibj.DutyCycleEncoder +import edu.wpi.first.wpilibj.Timer +import edu.wpi.first.wpilibj.motorcontrol.MotorController + +/** + * This class uses an absolute encoder, gear ratio and UPR to give the absolute position of the module or rotational velocity of the module. + * + * @param offset This must be in rotations of how much the offset of the ENCODER should be. + */ +open class AbsoluteEncoder( + name: String, + private val enc: DutyCycleEncoder, + unitPerRotation: Double, + private val inverted: Boolean, + private var offset: Double, + pollTime: Double = .02, + samplesPerAverage: Int = 1 +) : Encoder(name, 1, unitPerRotation, 1.0, pollTime) { + private var prevPos = Double.NaN + private var prevTime = Double.NaN + private val filter = MedianFilter(samplesPerAverage) + + private val frequency = enc.frequency + + /** This returns the absolute position of the module */ + override fun getPositionNative(): Double { + return if (inverted) { + filter.calculate( + MathUtil.inputModulus( + 1 - (enc.absolutePosition - offset), + -0.5, + 0.5 + ) + ) + } else { + filter.calculate( + MathUtil.inputModulus( + (enc.absolutePosition - offset), + -0.5, + 0.5 + ) + + ) + } + } + + override fun resetPosition(pos: Double) { + offset += getPositionNative() - pos + } + + /** This returns the rotational velocity (on vertical axis) of the module */ + override fun pollVelocityNative(): Double { + val currPos = + if (inverted) { + -enc.distance + } else { + enc.distance + } + + val currTime = Timer.getFPGATimestamp() + + val vel = + if (prevPos.isNaN()) { + 0.0 + } else { + val dt = currTime - prevTime + val dx = currPos - prevPos + dx / dt + } + this.prevPos = currPos + this.prevTime = currTime + + return vel + } + + companion object { + /** + * @param + * @param channel The DutyCycleEncoder port + * @param offset The position to put into DutyCycleEncoder's setPositionOffset + * @param unitPerRotation units measured when done one rotation (e.g 360 degrees per rotation) + * @param inverted If the encoder needs to be inverted or not + */ + fun creator( + channel: Int, + offset: Double, + unitPerRotation: Double, + inverted: Boolean + ): EncoderCreator = + EncoderCreator { name, _, _ -> + val enc = AbsoluteEncoder( + name, + DutyCycleEncoder(channel), + unitPerRotation, + inverted, + offset + ) + enc + } + } +} diff --git a/src/main/kotlin/frc/team449/system/encoder/BackupEncoder.kt b/src/main/kotlin/frc/team449/system/encoder/BackupEncoder.kt new file mode 100644 index 0000000..bd1d2e7 --- /dev/null +++ b/src/main/kotlin/frc/team449/system/encoder/BackupEncoder.kt @@ -0,0 +1,60 @@ +package frc.team449.system.encoder + +import edu.wpi.first.wpilibj.motorcontrol.MotorController +import kotlin.math.abs + +/** + * A wrapper to use when you have one external encoder that's more accurate but may be unplugged and + * an integrated encoder that's less accurate but is less likely to be unplugged. + * + *

If the primary encoder's velocity is 0 but the integrated encoder's is above a given + * threshold, it concludes that the primary encoder is broken and switches to using the + * fallback/integrated encoder. + */ +class BackupEncoder( + private val primary: Encoder, + private val fallback: Encoder, + private val velThreshold: Double, + pollTime: Double = .02 +) : Encoder(primary.name, 1, 1.0, 1.0, pollTime) { + + /** Whether the primary encoder's stopped working */ + private var useFallback = false + + override fun getPositionNative(): Double { + return if (useFallback) { + fallback.position + } else { + primary.position + } + } + + override fun pollVelocityNative(): Double { + val fallbackVel = fallback.velocity + return if (useFallback) { + fallbackVel + } else { + val primaryVel = primary.velocity + if (primaryVel == 0.0 && abs(fallbackVel) > velThreshold) { + this.useFallback = true + fallbackVel + } else { + primaryVel + } + } + } + + companion object { + fun creator( + primaryCreator: EncoderCreator, + fallbackCreator: EncoderCreator, + velThreshold: Double + ): EncoderCreator = EncoderCreator { name, motor, inverted -> + BackupEncoder( + primaryCreator.create("primary_$name", motor, inverted), + fallbackCreator.create("fallback_$name", motor, inverted), + velThreshold + ) + } + } +} diff --git a/src/main/kotlin/frc/team449/system/encoder/Encoder.kt b/src/main/kotlin/frc/team449/system/encoder/Encoder.kt new file mode 100644 index 0000000..25a95da --- /dev/null +++ b/src/main/kotlin/frc/team449/system/encoder/Encoder.kt @@ -0,0 +1,116 @@ +package frc.team449.system.encoder + +import edu.wpi.first.wpilibj.Timer + +/** + * A wrapper around encoders. Allows resetting encoders to a position. + * + *

Don't instantiate its subclasses directly. Instead, use their static creator methods + * @param encoderCPR Counts per rotation of the encoder + * @param unitPerRotation Meters traveled per rotation of the motor + * @param gearing The factor the output changes by after being measured by the encoder + * (should be >= 1, not a reciprocal), e.g. this would be 70 if there were + * a 70:1 gearing between the encoder and the final output + */ +abstract class Encoder( + val name: String, + encoderCPR: Int, + unitPerRotation: Double, + gearing: Double, + private val pollTime: Double +) { + /** + * Factor to multiply by to turn native encoder units into meters or whatever units are actually + * wanted + */ + private val encoderToUnit = unitPerRotation * gearing / encoderCPR + + /** An offset added to the position to allow resetting position. */ + private var positionOffset = 0.0 + + /** Whether this encoder is being simulated */ + private var simulated = false + + /** Simulated position set by SimEncoderController */ + private var simPos = 0.0 + + /** Simulated position set by SimEncoderController */ + private var simVel = 0.0 + + /** Used to cache the velocity, because we want to update only when dt >= 20ms **/ + private var cachedVel = Double.NaN + private var cachedTime = Double.NaN + + /** Current position in encoder's units */ + protected abstract fun getPositionNative(): Double + + /** Current velocity in encoder's units */ + protected abstract fun pollVelocityNative(): Double + + /** Position in meters or whatever unit you set */ + val position: Double + get() { + val posUnits = if (simulated) simPos else this.getPositionDirect() + return positionOffset + posUnits + } + + /** Velocity in meters per second or whatever unit you set */ + val velocity: Double + get() { + return if (simulated) simVel else getVelocityNative() * encoderToUnit + } + + /** + * Update the position offset to treat the current position as [pos] + */ + open fun resetPosition(pos: Double) { + this.positionOffset = pos - this.getPositionDirect() + } + + /** + * Get the position in units without adding [positionOffset] + */ + private fun getPositionDirect() = this.getPositionNative() * encoderToUnit + + /** + * Get the native velocity in units every loopTime + */ + private fun getVelocityNative(): Double { + val currTime = Timer.getFPGATimestamp() + // update if it has been at least 20 ms since the last update + if (cachedTime.isNaN() || currTime - cachedTime >= pollTime) { + cachedVel = this.pollVelocityNative() + cachedTime = currTime + } + return cachedVel + } + + /** + * Used to control [Encoder]s. Only one [SimController] can be used per encoder + * object. + * + * @param enc The encoder to control. + */ + class SimController(private val enc: Encoder) { + init { + if (enc.simulated) { + throw IllegalStateException("${enc.name} is already being simulated.") + } + enc.simulated = true + } + + /** Set the position of the [Encoder] this is controlling. */ + var position: Double + get() = enc.simPos + set(pos) { + enc.simPos = pos + } + + /** Set the velocity of the [Encoder] this is controlling. */ + var velocity: Double + get() = enc.simVel + set(vel) { + enc.simVel = vel + } + } +} diff --git a/src/main/kotlin/frc/team449/system/encoder/EncoderCreator.kt b/src/main/kotlin/frc/team449/system/encoder/EncoderCreator.kt new file mode 100644 index 0000000..95ec928 --- /dev/null +++ b/src/main/kotlin/frc/team449/system/encoder/EncoderCreator.kt @@ -0,0 +1,12 @@ +package frc.team449.system.encoder + +import edu.wpi.first.wpilibj.motorcontrol.MotorController + +/** + * Create an encoder given a motor controller and its configuration + * + * @param The type of the motor controller + */ +fun interface EncoderCreator { + fun create(encName: String, motor: M, inverted: Boolean): Encoder +} diff --git a/src/main/kotlin/frc/team449/system/encoder/NEOEncoder.kt b/src/main/kotlin/frc/team449/system/encoder/NEOEncoder.kt new file mode 100644 index 0000000..69e9579 --- /dev/null +++ b/src/main/kotlin/frc/team449/system/encoder/NEOEncoder.kt @@ -0,0 +1,33 @@ +package frc.team449.system.encoder + +import com.revrobotics.CANSparkMax +import com.revrobotics.RelativeEncoder + +/** A NEO integrated encoder plugged into a Spark */ +class NEOEncoder( + name: String, + private val enc: RelativeEncoder, + unitPerRotation: Double, + gearing: Double, + pollTime: Double = .02 +) : Encoder(name, NEO_ENCODER_CPR, 1.0, 1.0, pollTime) { + + init { + // Let the underlying encoder do the conversions + enc.positionConversionFactor = unitPerRotation * gearing + // Divide by 60 because it's originally in RPM + enc.velocityConversionFactor = unitPerRotation * gearing / 60 + } + + override fun getPositionNative() = enc.position + + override fun pollVelocityNative(): Double = enc.velocity + + companion object { + const val NEO_ENCODER_CPR = 1 + + fun creator(unitPerRotation: Double, gearing: Double): EncoderCreator = EncoderCreator { name, motor, _ -> + NEOEncoder(name, motor.encoder, unitPerRotation, gearing) + } + } +} diff --git a/src/main/kotlin/frc/team449/system/encoder/QuadEncoder.kt b/src/main/kotlin/frc/team449/system/encoder/QuadEncoder.kt new file mode 100644 index 0000000..5b53e82 --- /dev/null +++ b/src/main/kotlin/frc/team449/system/encoder/QuadEncoder.kt @@ -0,0 +1,37 @@ +package frc.team449.system.encoder + +import edu.wpi.first.wpilibj.motorcontrol.MotorController + +/** An external quadrature encoder */ +class QuadEncoder( + name: String, + private val encoder: edu.wpi.first.wpilibj.Encoder, + encoderCPR: Int, + unitPerRotation: Double, + gearing: Double, + pollTime: Double = .02 +) : Encoder(name, 1, 1.0, 1.0, pollTime) { + init { + // Let the WPI encoder handle the distance scaling + encoder.distancePerPulse = unitPerRotation * gearing / encoderCPR + encoder.samplesToAverage = 5 + } + + override fun getPositionNative() = encoder.distance + + override fun pollVelocityNative() = encoder.rate + + companion object { + fun creator( + encoder: edu.wpi.first.wpilibj.Encoder, + encoderCPR: Int, + unitPerRotation: Double, + gearing: Double, + inverted: Boolean + ): EncoderCreator = + EncoderCreator { name, _, _ -> + encoder.setReverseDirection(inverted) + QuadEncoder(name, encoder, encoderCPR, unitPerRotation, gearing) + } + } +} diff --git a/src/main/kotlin/frc/team449/system/encoder/TalonEncoder.kt b/src/main/kotlin/frc/team449/system/encoder/TalonEncoder.kt new file mode 100644 index 0000000..0e697d7 --- /dev/null +++ b/src/main/kotlin/frc/team449/system/encoder/TalonEncoder.kt @@ -0,0 +1,29 @@ +package frc.team449.system.encoder +// +// import com.ctre.phoenix.motorcontrol.can.BaseTalon +// import edu.wpi.first.wpilibj.motorcontrol.MotorController +// +// /** An encoder plugged into a TalonSRX */ +// class TalonEncoder( +// name: String, +// private val talon: BaseTalon, +// encoderCPR: Int, +// unitPerRotation: Double, +// gearing: Double, +// pollTime: Double = .02 +// ) : Encoder(name, encoderCPR * 4, unitPerRotation, gearing, pollTime) { +// +// override fun getPositionNative() = talon.getSelectedSensorPosition(0) +// +// override fun pollVelocityNative() = talon.getSelectedSensorVelocity(0) +// +// companion object { +// fun creator( +// encoderCPR: Int, +// unitPerRotation: Double, +// gearing: Double +// ): EncoderCreator where T : MotorController, T : BaseTalon = EncoderCreator { name, motor, _ -> +// TalonEncoder(name, motor, encoderCPR, unitPerRotation, gearing) +// } +// } +// } diff --git a/src/main/kotlin/frc/team449/system/motor/SparkMaxCreator.kt b/src/main/kotlin/frc/team449/system/motor/SparkMaxCreator.kt new file mode 100644 index 0000000..6cf7614 --- /dev/null +++ b/src/main/kotlin/frc/team449/system/motor/SparkMaxCreator.kt @@ -0,0 +1,127 @@ +package frc.team449.system.motor + +import com.revrobotics.CANSparkMax +import com.revrobotics.CANSparkMaxLowLevel +import com.revrobotics.REVLibError +import com.revrobotics.SparkMaxLimitSwitch +import edu.wpi.first.wpilibj.RobotController +import frc.team449.system.encoder.EncoderCreator + +/** + * Create a Spark Max with the given configurations + * + * @param name The motor's name + * @param id The motor's CAN ID + */ + +// TODO: Test if disabling voltage compensation helps reduce brownouts +fun createSparkMax( + name: String, + id: Int, + encCreator: EncoderCreator, + enableBrakeMode: Boolean = true, + inverted: Boolean = false, + currentLimit: Int = 0, + enableVoltageComp: Boolean = false, + slaveSparks: Map = mapOf(), + controlFrameRateMillis: Int = -1, + statusFrameRatesMillis: Map = mapOf() +): WrappedMotor { + val motor = CANSparkMax( + id, + CANSparkMaxLowLevel.MotorType.kBrushless + ) + if (motor.lastError != REVLibError.kOk) { + println( + "Motor could not be constructed on port " + + id + + " due to error " + + motor.lastError + ) + } + + motor.restoreFactoryDefaults() + + val enc = encCreator.create(name + "Enc", motor, inverted) + + val brakeMode = + if (enableBrakeMode) { + CANSparkMax.IdleMode.kBrake + } else { + CANSparkMax.IdleMode.kCoast + } + + motor.inverted = inverted + // Set brake mode + motor.idleMode = brakeMode + + // Set frame rates + if (controlFrameRateMillis >= 1) { + // Must be between 1 and 100 ms. + motor.setControlFramePeriodMs(controlFrameRateMillis) + } + + for ((statusFrame, period) in statusFrameRatesMillis) { + motor.setPeriodicFramePeriod(statusFrame, period) + } + + // Set the current limit if it was given + if (currentLimit > 0) { + motor.setSmartCurrentLimit(currentLimit) + } + + if (enableVoltageComp) { + motor.enableVoltageCompensation(RobotController.getBatteryVoltage()) + } else { + motor.disableVoltageCompensation() + } + + for ((slavePort, slaveInverted) in slaveSparks) { + val slave = createFollowerSpark(slavePort) + slave.restoreFactoryDefaults() + slave.follow(motor, slaveInverted) + slave.idleMode = brakeMode + if (currentLimit > 0) { + slave.setSmartCurrentLimit(currentLimit) + } + slave.burnFlash() + } + + motor.burnFlash() + + return WrappedMotor(motor, enc) +} + +/** + * Create a Spark that will follow another Spark + * + * @param port The follower's CAN ID + */ +private fun createFollowerSpark(port: Int): CANSparkMax { + val follower = CANSparkMax( + port, + CANSparkMaxLowLevel.MotorType.kBrushless + ) + + follower + .getForwardLimitSwitch(SparkMaxLimitSwitch.Type.kNormallyOpen) + .enableLimitSwitch(false) + follower + .getReverseLimitSwitch(SparkMaxLimitSwitch.Type.kNormallyOpen) + .enableLimitSwitch(false) + + follower.setPeriodicFramePeriod( + CANSparkMaxLowLevel.PeriodicFrame.kStatus0, + 100 + ) + follower.setPeriodicFramePeriod( + CANSparkMaxLowLevel.PeriodicFrame.kStatus1, + 100 + ) + follower.setPeriodicFramePeriod( + CANSparkMaxLowLevel.PeriodicFrame.kStatus2, + 100 + ) + + return follower +} diff --git a/src/main/kotlin/frc/team449/system/motor/TalonCreator.kt b/src/main/kotlin/frc/team449/system/motor/TalonCreator.kt new file mode 100644 index 0000000..4a58f9a --- /dev/null +++ b/src/main/kotlin/frc/team449/system/motor/TalonCreator.kt @@ -0,0 +1,254 @@ +package frc.team449.system.motor +// +// import com.ctre.phoenix.motorcontrol.* +// import com.ctre.phoenix.motorcontrol.can.BaseTalon +// import com.ctre.phoenix.motorcontrol.can.VictorSPX +// import com.ctre.phoenix.sensors.SensorVelocityMeasPeriod +// import edu.wpi.first.wpilibj.motorcontrol.MotorController +// import frc.team449.system.encoder.EncoderCreator +// +// /** Wrap a TalonSRX or TalonFX in a WrappedMotor object +// * @see configureSlaveTalon +// * @see createSlaveVictor +// */ +// fun createTalon( +// name: String, +// motor: T, +// encCreator: EncoderCreator, +// enableBrakeMode: Boolean = true, +// inverted: Boolean = false, +// currentLimit: Int = 0, +// enableVoltageComp: Boolean = true, +// voltageCompSamples: Int = 32, +// slaveTalons: List = listOf(), +// slaveVictors: List = listOf(), +// reverseSensor: Boolean = false, +// controlFrameRatesMillis: Map = mapOf(), +// statusFrameRatesMillis: Map = mapOf(), +// feedbackDevice: FeedbackDevice? = null +// ): WrappedMotor where T : MotorController, T : BaseTalon { +// val enc = encCreator.create(name + "Enc", motor, inverted) +// motor.setInverted(inverted) +// // Set brake mode +// val idleMode = if (enableBrakeMode) NeutralMode.Brake else NeutralMode.Coast +// motor.setNeutralMode(idleMode) +// for ((frame, period) in controlFrameRatesMillis) { +// motor.setControlFramePeriod(frame, period) +// } +// for ((frame, period) in statusFrameRatesMillis) { +// motor.setStatusFramePeriod(frame, period) +// } +// +// // Setup feedback device if it exists +// if (feedbackDevice != null) { +// // CTRE encoder use RPM instead of native units, and can be used as +// // QuadEncoders, so we switch +// // them to avoid having to support RPM. +// if (feedbackDevice == FeedbackDevice.CTRE_MagEncoder_Absolute || +// feedbackDevice == FeedbackDevice.CTRE_MagEncoder_Relative +// ) { +// motor.configSelectedFeedbackSensor(FeedbackDevice.QuadEncoder, 0, 0) +// } else { +// motor.configSelectedFeedbackSensor(feedbackDevice, 0, 0) +// } +// motor.setSensorPhase(reverseSensor) +// } else { +// motor.configSelectedFeedbackSensor(FeedbackDevice.None, 0, 0) +// } +// +// // Set the current limit if it was given +// if (currentLimit > 0) { +// motor.configSupplyCurrentLimit( +// SupplyCurrentLimitConfiguration(true, currentLimit.toDouble(), 0.0, 0.0), +// 0 +// ) +// } else { +// // If we don't have a current limit, disable current limiting. +// motor.configSupplyCurrentLimit(SupplyCurrentLimitConfiguration(), 0) +// } +// +// // Enable or disable voltage comp +// if (enableVoltageComp) { +// motor.enableVoltageCompensation(true) +// motor.configVoltageCompSaturation(12.0, 0) +// } +// motor.configVoltageMeasurementFilter(voltageCompSamples, 0) +// +// // Use slot 0 +// motor.selectProfileSlot(0, 0) +// val port = motor.deviceID +// // Set up slaves. +// for (slave in slaveTalons) { +// setMasterForTalon( +// slave, +// port, +// enableBrakeMode, +// currentLimit, +// if (enableVoltageComp) voltageCompSamples else null +// ) +// } +// for (slave in slaveVictors) { +// setMasterForVictor( +// slave, +// motor, +// enableBrakeMode, +// if (enableVoltageComp) voltageCompSamples else null +// ) +// } +// motor.configVelocityMeasurementPeriod(SensorVelocityMeasPeriod.Period_10Ms) +// motor.configVelocityMeasurementWindow(10) +// +// // Set max voltage +// motor.configPeakOutputForward(1.0, 0) +// motor.configPeakOutputReverse(1.0, 0) +// +// motor.configNominalOutputForward(0.0, 0) +// motor.configNominalOutputReverse(0.0, 0) +// +// return WrappedMotor(name, motor, enc) +// } +// +// /** @param slaveTalon Takes a slave TalonSRX or TalonFX and configures it to act as a slave +// * @return The same Talon +// */ +// fun configureSlaveTalon( +// slaveTalon: BaseTalon, +// invertType: InvertType +// ): BaseTalon { +// // Turn off features we don't want a slave to have +// slaveTalon.setInverted(invertType) +// slaveTalon.configForwardLimitSwitchSource( +// LimitSwitchSource.Deactivated, +// LimitSwitchNormal.Disabled, +// 0 +// ) +// slaveTalon.configReverseLimitSwitchSource( +// LimitSwitchSource.Deactivated, +// LimitSwitchNormal.Disabled, +// 0 +// ) +// slaveTalon.configForwardSoftLimitEnable(false, 0) +// slaveTalon.configReverseSoftLimitEnable(false, 0) +// slaveTalon.configPeakOutputForward(1.0, 0) +// slaveTalon.enableVoltageCompensation(true) +// slaveTalon.configVoltageCompSaturation(12.0, 0) +// slaveTalon.configVoltageMeasurementFilter(32, 0) +// +// // Slow down frames so we don't overload the CAN bus +// slaveTalon.setStatusFramePeriod(StatusFrameEnhanced.Status_1_General, 100, 0) +// slaveTalon.setStatusFramePeriod(StatusFrameEnhanced.Status_6_Misc, 100, 0) +// slaveTalon.setStatusFramePeriod(StatusFrameEnhanced.Status_7_CommStatus, 100, 0) +// slaveTalon.setStatusFramePeriod(StatusFrameEnhanced.Status_9_MotProfBuffer, 100, 0) +// slaveTalon.setStatusFramePeriod(StatusFrameEnhanced.Status_10_MotionMagic, 100, 0) +// slaveTalon.setStatusFramePeriod(StatusFrameEnhanced.Status_12_Feedback1, 100, 0) +// slaveTalon.setStatusFramePeriod(StatusFrameEnhanced.Status_13_Base_PIDF0, 100, 0) +// slaveTalon.setStatusFramePeriod(StatusFrameEnhanced.Status_14_Turn_PIDF1, 100, 0) +// slaveTalon.setStatusFramePeriod(StatusFrameEnhanced.Status_15_FirmwareApiStatus, 100, 0) +// slaveTalon.setStatusFramePeriod(StatusFrameEnhanced.Status_3_Quadrature, 100, 0) +// slaveTalon.setStatusFramePeriod(StatusFrameEnhanced.Status_8_PulseWidth, 100, 0) +// slaveTalon.setStatusFramePeriod(StatusFrameEnhanced.Status_11_UartGadgeteer, 100, 0) +// +// return slaveTalon +// } +// +// /** +// * Creates a VictorSPX that will follow some Talon +// * +// * @param port The CAN ID of this Victor SPX. +// * @param invertType Whether to invert this relative to the master. Defaults to not inverting +// * relative to master. +// */ +// fun createSlaveVictor(port: Int, invertType: InvertType): VictorSPX { +// val victorSPX = VictorSPX(port) +// victorSPX.setInverted(invertType) +// victorSPX.configPeakOutputForward(1.0, 0) +// victorSPX.configPeakOutputReverse(-1.0, 0) +// victorSPX.enableVoltageCompensation(true) +// victorSPX.configVoltageCompSaturation(12.0, 0) +// victorSPX.configVoltageMeasurementFilter(32, 0) +// victorSPX.setStatusFramePeriod(StatusFrame.Status_1_General, 100, 0) +// victorSPX.setStatusFramePeriod(StatusFrame.Status_2_Feedback0, 100, 0) +// victorSPX.setStatusFramePeriod(StatusFrame.Status_6_Misc, 100, 0) +// victorSPX.setStatusFramePeriod(StatusFrame.Status_7_CommStatus, 100, 0) +// victorSPX.setStatusFramePeriod(StatusFrame.Status_9_MotProfBuffer, 100, 0) +// victorSPX.setStatusFramePeriod(StatusFrame.Status_10_MotionMagic, 100, 0) +// victorSPX.setStatusFramePeriod(StatusFrame.Status_12_Feedback1, 100, 0) +// victorSPX.setStatusFramePeriod(StatusFrame.Status_13_Base_PIDF0, 100, 0) +// victorSPX.setStatusFramePeriod(StatusFrame.Status_14_Turn_PIDF1, 100, 0) +// +// return victorSPX +// } +// +// /** +// * Set this Talon to follow another CAN device. +// * +// * @param port The CAN ID of the device to follow. +// * @param brakeMode Whether this Talon should be in brake mode or coast mode. +// * @param currentLimit The current limit for this Talon. Can be null for no current limit. +// * @param voltageCompSamples The number of voltage compensation samples to use, or null to not +// * compensate voltage. +// */ +// private fun setMasterForTalon( +// slaveTalon: BaseTalon, +// port: Int, +// brakeMode: Boolean, +// currentLimit: Int, +// voltageCompSamples: Int? +// ) { +// // Brake mode doesn't automatically follow master +// slaveTalon.setNeutralMode(if (brakeMode) NeutralMode.Brake else NeutralMode.Coast) +// +// // Current limiting might not automatically follow master, set it just to be +// // safe +// if (currentLimit > 0) { +// slaveTalon.configSupplyCurrentLimit( +// SupplyCurrentLimitConfiguration(true, currentLimit.toDouble(), 0.0, 0.0), +// 0 +// ) +// } else { +// // If we don't have a current limit, disable current limiting. +// slaveTalon.configSupplyCurrentLimit(SupplyCurrentLimitConfiguration(), 0) +// } +// +// // Voltage comp might not follow master either +// if (voltageCompSamples != null) { +// slaveTalon.enableVoltageCompensation(true) +// slaveTalon.configVoltageCompSaturation(12.0, 0) +// slaveTalon.configVoltageMeasurementFilter(voltageCompSamples, 0) +// } else { +// slaveTalon.enableVoltageCompensation(false) +// } +// +// // Follow the leader +// slaveTalon[ControlMode.Follower] = port.toDouble() +// } +// +// /** +// * Set this Victor to follow another CAN device. +// * +// * @param toFollow The motor controller to follow. +// * @param brakeMode Whether this Talon should be in brake mode or coast mode. +// * @param voltageCompSamples The number of voltage compensation samples to use, or null to not +// * compensate voltage. +// */ +// private fun setMasterForVictor( +// victorSPX: VictorSPX, +// toFollow: IMotorController, +// brakeMode: Boolean, +// voltageCompSamples: Int? +// ) { +// // Brake mode doesn't automatically follow master +// victorSPX.setNeutralMode(if (brakeMode) NeutralMode.Brake else NeutralMode.Coast) +// +// // Voltage comp might not follow master either +// if (voltageCompSamples != null) { +// victorSPX.enableVoltageCompensation(true) +// victorSPX.configVoltageCompSaturation(12.0, 0) +// victorSPX.configVoltageMeasurementFilter(voltageCompSamples, 0) +// } else { +// victorSPX.enableVoltageCompensation(false) +// } +// +// // Follow the leader +// victorSPX.follow(toFollow) +// } diff --git a/src/main/kotlin/frc/team449/system/motor/WrappedMotor.kt b/src/main/kotlin/frc/team449/system/motor/WrappedMotor.kt new file mode 100644 index 0000000..8301988 --- /dev/null +++ b/src/main/kotlin/frc/team449/system/motor/WrappedMotor.kt @@ -0,0 +1,35 @@ +package frc.team449.system.motor + +import edu.wpi.first.wpilibj.RobotController +import edu.wpi.first.wpilibj.motorcontrol.MotorController +import frc.team449.system.encoder.Encoder + +/** Our own wrapper grouping the motor controller and encoder for a motor */ +class WrappedMotor( + private val motor: MotorController, + val encoder: Encoder +) : MotorController by motor { + /** + * The last set voltage for this motor (through [setVoltage] or [set]) + */ + var lastVoltage = 0.0 + private set + + /** Position in meters or whatever unit you set */ + val position: Double + get() = encoder.position + + /** Velocity in meters per second or whatever unit you set */ + val velocity: Double + get() = encoder.velocity + + override fun setVoltage(volts: Double) { + motor.setVoltage(volts) + this.lastVoltage = volts + } + + override fun set(output: Double) { + motor.set(output) + this.lastVoltage = output * RobotController.getBatteryVoltage() + } +} diff --git a/src/main/kotlin/frc/team449/util/Properties.kt b/src/main/kotlin/frc/team449/util/Properties.kt new file mode 100644 index 0000000..577a5c2 --- /dev/null +++ b/src/main/kotlin/frc/team449/util/Properties.kt @@ -0,0 +1,37 @@ +package frc.team449.util + +import edu.wpi.first.hal.SimBoolean +import edu.wpi.first.hal.SimDouble +import kotlin.properties.ReadOnlyProperty +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +/** A property with a custom getter but a backing field of a different type + * @tparam B The type of the actual backing field + * @tparam F The type of the property that is delegated + */ +fun wrappedProp(backing: B, get: (B) -> F): ReadOnlyProperty { + return object : ReadOnlyProperty { + override operator fun getValue(thisRef: Any?, property: KProperty<*>) = get(backing) + } +} + +/** A property with a custom getter and setter but a backing field of a different type + * @tparam B The type of the actual backing field + * @tparam F The type of the property that is delegated + * @see simDoubleProp + * @see simBooleanProp + */ +fun wrappedProp(backing: B, get: (B) -> F, set: (B, F) -> Unit): ReadWriteProperty { + return object : ReadWriteProperty { + override operator fun getValue(thisRef: Any?, property: KProperty<*>) = get(backing) + + override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: F) = set(backing, value) + } +} + +/** A delegated property of type Double backed by a [SimDouble] */ +fun simDoubleProp(sim: SimDouble): ReadWriteProperty = wrappedProp(sim, SimDouble::get, SimDouble::set) + +/** A delegated property of type Boolean backed by a [SimBoolean] */ +fun simBooleanProp(sim: SimBoolean): ReadWriteProperty = wrappedProp(sim, SimBoolean::get, SimBoolean::set) diff --git a/src/main/resources/2023-field.png b/src/main/resources/2023-field.png new file mode 100644 index 0000000..51cc6a3 Binary files /dev/null and b/src/main/resources/2023-field.png differ diff --git a/vendordeps/NavX.json b/vendordeps/NavX.json new file mode 100644 index 0000000..2a29835 --- /dev/null +++ b/vendordeps/NavX.json @@ -0,0 +1,40 @@ +{ + "fileName": "NavX.json", + "name": "NavX", + "version": "2024.0.1-beta-4", + "uuid": "cb311d09-36e9-4143-a032-55bb2b94443b", + "frcYear": "2024", + "mavenUrls": [ + "https://dev.studica.com/maven/release/2024/" + ], + "jsonUrl": "https://dev.studica.com/releases/2024/NavX.json", + "javaDependencies": [ + { + "groupId": "com.kauailabs.navx.frc", + "artifactId": "navx-frc-java", + "version": "2024.0.1-beta-4" + } + ], + "jniDependencies": [], + "cppDependencies": [ + { + "groupId": "com.kauailabs.navx.frc", + "artifactId": "navx-frc-cpp", + "version": "2024.0.1-beta-4", + "headerClassifier": "headers", + "sourcesClassifier": "sources", + "sharedLibrary": false, + "libName": "navx_frc", + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "linuxathena", + "linuxraspbian", + "linuxarm32", + "linuxarm64", + "linuxx86-64", + "osxuniversal", + "windowsx86-64" + ] + } + ] +} \ No newline at end of file diff --git a/vendordeps/PathplannerLib.json b/vendordeps/PathplannerLib.json new file mode 100644 index 0000000..31501f6 --- /dev/null +++ b/vendordeps/PathplannerLib.json @@ -0,0 +1,38 @@ +{ + "fileName": "PathplannerLib.json", + "name": "PathplannerLib", + "version": "2024.0.0-beta-5.1", + "uuid": "1b42324f-17c6-4875-8e77-1c312bc8c786", + "frcYear": "2024", + "mavenUrls": [ + "https://3015rangerrobotics.github.io/pathplannerlib/repo" + ], + "jsonUrl": "https://3015rangerrobotics.github.io/pathplannerlib/PathplannerLib.json", + "javaDependencies": [ + { + "groupId": "com.pathplanner.lib", + "artifactId": "PathplannerLib-java", + "version": "2024.0.0-beta-5.1" + } + ], + "jniDependencies": [], + "cppDependencies": [ + { + "groupId": "com.pathplanner.lib", + "artifactId": "PathplannerLib-cpp", + "version": "2024.0.0-beta-5.1", + "libName": "PathplannerLib", + "headerClassifier": "headers", + "sharedLibrary": false, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal", + "linuxathena", + "linuxarm32", + "linuxarm64" + ] + } + ] +} diff --git a/vendordeps/PhotonLib-json-1.0.json b/vendordeps/PhotonLib-json-1.0.json new file mode 100644 index 0000000..748c671 --- /dev/null +++ b/vendordeps/PhotonLib-json-1.0.json @@ -0,0 +1,42 @@ +{ + "fileName": "photonlib.json", + "name": "photonlib", + "version": "v2024.1.1-beta-3.2", + "uuid": "515fe07e-bfc6-11fa-b3de-0242ac130004", + "frcYear": "2024", + "mavenUrls": [ + "https://maven.photonvision.org/repository/internal", + "https://maven.photonvision.org/repository/snapshots" + ], + "jsonUrl": "https://maven.photonvision.org/repository/internal/org/photonvision/PhotonLib-json/1.0/PhotonLib-json-1.0.json", + "jniDependencies": [], + "cppDependencies": [ + { + "groupId": "org.photonvision", + "artifactId": "PhotonLib-cpp", + "version": "v2024.1.1-beta-3.2", + "libName": "Photon", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxathena", + "linuxx86-64", + "osxuniversal" + ] + } + ], + "javaDependencies": [ + { + "groupId": "org.photonvision", + "artifactId": "PhotonLib-java", + "version": "v2024.1.1-beta-3.2" + }, + { + "groupId": "org.photonvision", + "artifactId": "PhotonTargeting-java", + "version": "v2024.1.1-beta-3.2" + } + ] +} diff --git a/vendordeps/REVLib-2024.json b/vendordeps/REVLib-2024.json new file mode 100644 index 0000000..8a64caf --- /dev/null +++ b/vendordeps/REVLib-2024.json @@ -0,0 +1,74 @@ +{ + "fileName": "REVLib.json", + "name": "REVLib", + "version": "2024.0.0", + "frcYear": "2024", + "uuid": "3f48eb8c-50fe-43a6-9cb7-44c86353c4cb", + "mavenUrls": [ + "https://maven.revrobotics.com/" + ], + "jsonUrl": "https://software-metadata.revrobotics.com/REVLib-2024.json", + "javaDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-java", + "version": "2024.0.0" + } + ], + "jniDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-driver", + "version": "2024.0.0", + "skipInvalidPlatforms": true, + "isJar": false, + "validPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + } + ], + "cppDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-cpp", + "version": "2024.0.0", + "libName": "REVLib", + "headerClassifier": "headers", + "sharedLibrary": false, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + }, + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-driver", + "version": "2024.0.0", + "libName": "REVLibDriver", + "headerClassifier": "headers", + "sharedLibrary": false, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + } + ] +} diff --git a/vendordeps/WPILibNewCommands.json b/vendordeps/WPILibNewCommands.json new file mode 100644 index 0000000..67bf389 --- /dev/null +++ b/vendordeps/WPILibNewCommands.json @@ -0,0 +1,38 @@ +{ + "fileName": "WPILibNewCommands.json", + "name": "WPILib-New-Commands", + "version": "1.0.0", + "uuid": "111e20f7-815e-48f8-9dd6-e675ce75b266", + "frcYear": "2024", + "mavenUrls": [], + "jsonUrl": "", + "javaDependencies": [ + { + "groupId": "edu.wpi.first.wpilibNewCommands", + "artifactId": "wpilibNewCommands-java", + "version": "wpilib" + } + ], + "jniDependencies": [], + "cppDependencies": [ + { + "groupId": "edu.wpi.first.wpilibNewCommands", + "artifactId": "wpilibNewCommands-cpp", + "version": "wpilib", + "libName": "wpilibNewCommands", + "headerClassifier": "headers", + "sourcesClassifier": "sources", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "linuxathena", + "linuxarm32", + "linuxarm64", + "windowsx86-64", + "windowsx86", + "linuxx86-64", + "osxuniversal" + ] + } + ] +}