diff --git a/examples/stage1/stage1c/snippets/.gitignore b/examples/stage1/stage1c/snippets/.gitignore new file mode 100644 index 00000000..34cbaac1 --- /dev/null +++ b/examples/stage1/stage1c/snippets/.gitignore @@ -0,0 +1,187 @@ +# 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 +networktables.json +simgui.json +*-window.json + +# Simulation data log directory +logs/ + +# Folder that has CTRE Phoenix Sim device config storage +ctre_sim/ + +# clangd +/.cache +compile_commands.json + +# Eclipse generated file for annotation processors +.factorypath diff --git a/examples/stage1/stage1c/snippets/.wpilib/wpilib_preferences.json b/examples/stage1/stage1c/snippets/.wpilib/wpilib_preferences.json new file mode 100644 index 00000000..5345dd47 --- /dev/null +++ b/examples/stage1/stage1c/snippets/.wpilib/wpilib_preferences.json @@ -0,0 +1,6 @@ +{ + "enableCppIntellisense": false, + "currentLanguage": "java", + "projectYear": "2027_alpha7", + "teamNumber": null +} diff --git a/examples/stage1/stage1c/snippets/LICENSE b/examples/stage1/stage1c/snippets/LICENSE new file mode 100644 index 00000000..8b985b07 --- /dev/null +++ b/examples/stage1/stage1c/snippets/LICENSE @@ -0,0 +1,11 @@ +Copyright 2026 FRCSoftware Contributors + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. 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. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER 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/examples/stage1/stage1c/snippets/build.gradle b/examples/stage1/stage1c/snippets/build.gradle new file mode 100644 index 00000000..35e8c98e --- /dev/null +++ b/examples/stage1/stage1c/snippets/build.gradle @@ -0,0 +1,152 @@ +plugins { + id "java" + id "application" + id "com.diffplug.spotless" version "8.0.0" + id "org.wpilib.GradleRIO" version "2027.0.0-alpha-7" +} + +java { + sourceCompatibility = JavaVersion.VERSION_25 + targetCompatibility = JavaVersion.VERSION_25 +} + +def ROBOT_MAIN_CLASS = "first.Main" + +// Define my targets (SystemCore) and artifacts (deployable files) +// This is added by GradleRIO's backing project DeployUtils. +deploy { + targets { + systemcore(getTargetTypeClass('SystemCore')) { + // 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.wpilib.getTeamNumber() + // Use the default systemcore host name. This must be called after setting team + // as happens on the line above + useDefaultSystemcoreHostName() + debug = project.wpilib.getDebugOrDefault(false) + + artifacts { + // First part is artifact name, 2nd is artifact type + // getTargetTypeClass is a shortcut to get the class type using a string + + wpilibJava(getArtifactTypeClass('WPILibJavaArtifact')) { + // Set to true to use debug including JNI, which will drastically impact performance. + debugJni = false + } + + // Static files artifact + wpilibStaticFileDeploy(getArtifactTypeClass('FileTreeArtifact')) { + files = project.fileTree('src/main/deploy') + directory = '/home/systemcore/deploy' + deleteOldFiles = false // Change to true to delete files on systemcore that no + // longer exist in deploy directory of this project + } + } + } + } +} + +def deployArtifact = deploy.targets.systemcore.artifacts.wpilibJava + +// Set to true to use debug for simulation including JNI +wpi.java.runSimWithDebugJni = 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 { + annotationProcessor wpi.java.deps.wpilibAnnotations() + implementation wpi.java.deps.wpilib() + implementation wpi.java.vendor.java() + + systemcoreDebug wpi.java.deps.wpilibJniDebug(wpi.platforms.systemcore) + systemcoreDebug wpi.java.vendor.jniDebug(wpi.platforms.systemcore) + + systemcoreRelease wpi.java.deps.wpilibJniRelease(wpi.platforms.systemcore) + systemcoreRelease wpi.java.vendor.jniRelease(wpi.platforms.systemcore) + + nativeDebug wpi.java.deps.wpilibJniDebug(wpi.platforms.desktop) + nativeDebug wpi.java.vendor.jniDebug(wpi.platforms.desktop) + simulationDebug wpi.sim.enableDebug() + + nativeRelease wpi.java.deps.wpilibJniRelease(wpi.platforms.desktop) + nativeRelease wpi.java.vendor.jniRelease(wpi.platforms.desktop) + simulationRelease wpi.sim.enableRelease() + + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +test { + useJUnitPlatform() + systemProperty 'junit.jupiter.extensions.autodetection.enabled', 'true' +} + +// Simulation configuration (e.g. environment variables). +wpi.sim.addGui().defaultEnabled = true +wpi.sim.addDriverstation() + +application.mainClass = ROBOT_MAIN_CLASS + +deployArtifact.configureApplication(application) +wpi.java.configureApplication(application) +wpi.java.configureTestTasks(test) + +jar { + from('src') { into 'backup/src' } + from('vendordeps') { into 'backup/vendordeps' } + from('build.gradle') { into 'backup' } +} + +// Configure string concat to always inline compile +tasks.withType(JavaCompile) { + options.compilerArgs.add '-XDstringConcat=inline' +} + +project.compileJava.dependsOn(spotlessApply) +spotless { + java { + target fileTree('.') { + include '**/*.java' + exclude '**/build/**', '**/build-*/**', '**/bin/**' + } + toggleOffOn() + googleJavaFormat() + removeUnusedImports() + trimTrailingWhitespace() + endWithNewline() + } + groovyGradle { + target fileTree('.') { + include '**/*.gradle' + exclude '**/build/**', '**/build-*/**' + } + greclipse() + leadingTabsToSpaces(4) + trimTrailingWhitespace() + endWithNewline() + } + format 'xml', { + target fileTree('.') { + include '**/*.xml' + exclude '**/build/**', '**/build-*/**' + } + eclipseWtp('xml') + trimTrailingWhitespace() + leadingTabsToSpaces(2) + endWithNewline() + } + format 'misc', { + target fileTree('.') { + include '**/*.md', '**/.gitignore' + exclude '**/build/**', '**/build-*/**' + } + trimTrailingWhitespace() + leadingTabsToSpaces(2) + endWithNewline() + } +} diff --git a/examples/stage1/stage1c/snippets/gradle/wrapper/gradle-wrapper.jar b/examples/stage1/stage1c/snippets/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..d997cfc6 Binary files /dev/null and b/examples/stage1/stage1c/snippets/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/stage1/stage1c/snippets/gradle/wrapper/gradle-wrapper.properties b/examples/stage1/stage1c/snippets/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..42bb5ac9 --- /dev/null +++ b/examples/stage1/stage1c/snippets/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=permwrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=permwrapper/dists diff --git a/examples/stage1/stage1c/snippets/gradlew b/examples/stage1/stage1c/snippets/gradlew new file mode 100644 index 00000000..739907df --- /dev/null +++ b/examples/stage1/stage1c/snippets/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# 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/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/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 -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || 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 + + + +# 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" ) + + 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, 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" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# 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/examples/stage1/stage1c/snippets/gradlew.bat b/examples/stage1/stage1c/snippets/gradlew.bat new file mode 100644 index 00000000..c4bdd3ab --- /dev/null +++ b/examples/stage1/stage1c/snippets/gradlew.bat @@ -0,0 +1,93 @@ +@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 +@rem SPDX-License-Identifier: Apache-2.0 +@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. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +: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/examples/stage1/stage1c/snippets/settings.gradle b/examples/stage1/stage1c/snippets/settings.gradle new file mode 100644 index 00000000..e2626a81 --- /dev/null +++ b/examples/stage1/stage1c/snippets/settings.gradle @@ -0,0 +1,30 @@ +import org.gradle.internal.os.OperatingSystem + +pluginManagement { + repositories { + String wpilibYear = '2027_alpha5' + File wpilibHome + if (OperatingSystem.current().isWindows()) { + String publicFolder = System.getenv('PUBLIC') + if (publicFolder == null) { + publicFolder = "C:\\Users\\Public" + } + def homeRoot = new File(publicFolder, "wpilib") + wpilibHome = new File(homeRoot, wpilibYear) + } else { + def userFolder = System.getProperty("user.home") + def homeRoot = new File(userFolder, "wpilib") + wpilibHome = new File(homeRoot, wpilibYear) + } + def wpilibHomeMaven = new File(wpilibHome, 'maven') + maven { + name = 'wpilibHome' + url = wpilibHomeMaven + } + mavenLocal() + gradlePluginPortal() + } +} + +Properties props = System.getProperties(); +props.setProperty("org.gradle.internal.native.headers.unresolved.dependencies.ignore", "true"); diff --git a/examples/stage1/stage1c/snippets/src/main/java/sources/PIDKitbotCTRE.java b/examples/stage1/stage1c/snippets/src/main/java/sources/PIDKitbotCTRE.java new file mode 100644 index 00000000..987b584f --- /dev/null +++ b/examples/stage1/stage1c/snippets/src/main/java/sources/PIDKitbotCTRE.java @@ -0,0 +1,205 @@ +/* + * Copyright 2026 FRCSoftware + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package first.robot.mechanisms; + +import static org.wpilib.units.Units.*; + +import com.ctre.phoenix6.CANBus; +import com.ctre.phoenix6.StatusSignal; +import com.ctre.phoenix6.configs.TalonFXConfiguration; +import com.ctre.phoenix6.controls.Follower; +import com.ctre.phoenix6.hardware.TalonFX; +import com.ctre.phoenix6.signals.InvertedValue; +import com.ctre.phoenix6.signals.MotorAlignmentValue; +import first.robot.simulation.DrivetrainSim; +import java.util.function.DoubleSupplier; +import org.wpilib.command3.Command; +import org.wpilib.command3.Mechanism; +import org.wpilib.drive.DifferentialDrive; +import org.wpilib.framework.RobotBase; +import org.wpilib.hardware.bus.CANPort; +import org.wpilib.hardware.imu.OnboardIMU; +import org.wpilib.hardware.imu.OnboardIMU.MountOrientation; +import org.wpilib.math.controller.PIDController; +import org.wpilib.math.geometry.Pose2d; +import org.wpilib.math.geometry.Rotation2d; +import org.wpilib.math.util.Units; +import org.wpilib.telemetry.Telemetry; +import org.wpilib.telemetry.TelemetryTable; +import org.wpilib.units.measure.Angle; +import org.wpilib.units.measure.AngularVelocity; + + //[checkin] +public class Drivetrain implements Mechanism { + private static final int leftLeaderID = 0, rightLeaderID = 2; + private final TalonFX leftLeader = new TalonFX(leftLeaderID, new CANBus(CANPort.CAN_S0)); + private final TalonFX leftFollower = new TalonFX(1, new CANBus(CANPort.CAN_S0)); + private final TalonFX rightLeader = new TalonFX(rightLeaderID, new CANBus(CANPort.CAN_S0)); + private final TalonFX rightFollower = new TalonFX(3, new CANBus(CANPort.CAN_S0)); + + private final OnboardIMU imu = new OnboardIMU(MountOrientation.FLAT); + private final DifferentialDrive differentialDrive = + new DifferentialDrive(leftLeader::setThrottle, rightLeader::setThrottle); + + private final DrivetrainSim drivetrainSim = new DrivetrainSim(leftLeader, rightLeader); + + // [wheel] + private static final double WHEEL_CIRCUMFERENCE = Units.inchesToMeters(6.0) * Math.PI; + // [/wheel] + // [gearRatio] + private static final double GEAR_RATIO = 10.71; + // [/gearRatio] + // [pidController] + private final PIDController driveDistanceController = new PIDController(0, 0, 0); + private final PIDController headingController = new PIDController(0, 0, 0); + // [/pidController] + // [sensors] + private final StatusSignal leftDistanceSignal = leftLeader.getPosition(); + private final StatusSignal rightDistanceSignal = rightLeader.getPosition(); + // [/sensors] + // [sensorsTwo] + private final StatusSignal leftVelocitySignal = leftLeader.getVelocity(); + private final StatusSignal rightVelocitySignal = rightLeader.getVelocity(); + // [/sensorsTwo] + + + private final TelemetryTable table = Telemetry.getTable("Drive"); + private final TelemetryTable driveDistanceTable = table.getTable("Drive Distance"); + private final TelemetryTable turnTable = table.getTable("Turn in Place"); + + public Drivetrain() { + //[gearRatio2] + var leftConfig = new TalonFXConfiguration(); + leftConfig.MotorOutput.withInverted(InvertedValue.Clockwise_Positive); + // adds the GEAR_RATIO + leftConfig.Feedback.withSensorToMechanismRatio(GEAR_RATIO); + leftLeader.getConfigurator().apply(leftConfig); + + var rightConfig = new TalonFXConfiguration(); + rightConfig.MotorOutput.withInverted(InvertedValue.CounterClockwise_Positive); + // adds the GEAR_RATIO + rightConfig.Feedback.withSensorToMechanismRatio(GEAR_RATIO); + rightLeader.getConfigurator().apply(rightConfig); + //[/gearRatio2] + leftFollower.setControl(new Follower(leftLeaderID, MotorAlignmentValue.Aligned)); + rightFollower.setControl(new Follower(rightLeaderID, MotorAlignmentValue.Aligned)); + + setDefaultCommand(idle()); + + //[pidController2] + driveDistanceController.setTolerance(0.05, 0.05); + headingController.setTolerance(0.05, 0.2); + headingController.enableContinuousInput(-Math.PI, Math.PI); + //[/pidController2] + } + //[/checkin] + public Command idle() { + return run(coroutine -> { + while (true) { + differentialDrive.arcadeDrive(0.0, 0.0); + coroutine.yield(); + } + }) + .named("Idle"); + } + + public Command arcadeDrive(DoubleSupplier forwardThrottle, DoubleSupplier rotationThrottle) { + return run(coroutine -> { + while (true) { + differentialDrive.arcadeDrive( + forwardThrottle.getAsDouble(), -rotationThrottle.getAsDouble()); + coroutine.yield(); + } + }) + .named("Drive"); + } + + public Command driveDistance(double distanceMeters) { + return run(coroutine -> { + double startingDistance = (getLeftDistance() + getRightDistance()) / 2; + double targetDistance = startingDistance + distanceMeters; + + driveDistanceController.setSetpoint(targetDistance); + driveDistanceController.reset(); + + while (!driveDistanceController.atSetpoint()) { + double currentDistance = (getLeftDistance() + getRightDistance()) / 2; + + double effort = driveDistanceController.calculate(currentDistance); + differentialDrive.arcadeDrive(effort, 0); + + driveDistanceTable.log("Target", Meters.of(targetDistance)); + driveDistanceTable.log("Current", Meters.of(currentDistance)); + driveDistanceTable.log("Error", Meters.of(driveDistanceController.getError())); + driveDistanceTable.log("Effort", effort); + + coroutine.yield(); + } + }) + .named("Drive Distance " + distanceMeters + "m"); + } + + public Command turnInPlace(Rotation2d target) { + return run(coroutine -> { + headingController.setSetpoint(target.getRadians()); + headingController.reset(); + + while (!headingController.atSetpoint()) { + double effort = headingController.calculate(getHeading().getRadians()); + + differentialDrive.arcadeDrive(0, effort); + + turnTable.log("Target", target); + turnTable.log("Current", getHeading()); + turnTable.log("Error", Radians.of(headingController.getError())); + turnTable.log("Effort", effort); + + coroutine.yield(); + } + }) + .named("Turn in Place to angle " + target.getDegrees() + "deg"); + } + + public void periodic() { + if (RobotBase.isSimulation()) { + drivetrainSim.periodic(); + } + + StatusSignal.refreshAll( + leftDistanceSignal, rightDistanceSignal, leftVelocitySignal, rightVelocitySignal); + + table.log("Left Distance", Meters.of(getLeftDistance())); + table.log("Left Velocity", MetersPerSecond.of(getLeftVelocity())); + table.log("Right Distance", Meters.of(getRightDistance())); + table.log("Right Velocity", MetersPerSecond.of(getRightVelocity())); + table.log("Heading", getHeading()); + table.log("Active Commands", getRunningCommands().stream().map(Command::name).toArray()); + } + + public double getLeftDistance() { + return leftDistanceSignal.getValue().in(Rotation) * WHEEL_CIRCUMFERENCE; + } + + public double getRightDistance() { + return rightDistanceSignal.getValue().in(Rotation) * WHEEL_CIRCUMFERENCE; + } + + public double getLeftVelocity() { + return leftVelocitySignal.getValue().in(RotationsPerSecond) * WHEEL_CIRCUMFERENCE; + } + + public double getRightVelocity() { + return rightVelocitySignal.getValue().in(RotationsPerSecond) * WHEEL_CIRCUMFERENCE; + } + + public Rotation2d getHeading() { + return imu.getRotation2d(); + } + + public void setPose(Pose2d pose) { + drivetrainSim.setPose(pose); + } +} diff --git a/examples/stage1/stage1c/snippets/src/main/java/sources/PIDKitbotREV.java b/examples/stage1/stage1c/snippets/src/main/java/sources/PIDKitbotREV.java new file mode 100644 index 00000000..d082bdbf --- /dev/null +++ b/examples/stage1/stage1c/snippets/src/main/java/sources/PIDKitbotREV.java @@ -0,0 +1,198 @@ +/* + * Copyright 2026 FRCSoftware + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package first.robot.mechanisms; + +import static org.wpilib.units.Units.*; + +import com.revrobotics.PersistMode; +import com.revrobotics.RelativeEncoder; +import com.revrobotics.ResetMode; +import com.revrobotics.spark.SparkLowLevel.MotorType; +import com.revrobotics.spark.SparkMax; +import com.revrobotics.spark.config.SparkMaxConfig; +import first.robot.simulation.DrivetrainSim; +import java.util.function.DoubleSupplier; +import org.wpilib.command3.Command; +import org.wpilib.command3.Mechanism; +import org.wpilib.drive.DifferentialDrive; +import org.wpilib.framework.RobotBase; +import org.wpilib.hardware.bus.CANPort; +import org.wpilib.hardware.imu.OnboardIMU; +import org.wpilib.hardware.imu.OnboardIMU.MountOrientation; +import org.wpilib.math.controller.PIDController; +import org.wpilib.math.geometry.Pose2d; +import org.wpilib.math.geometry.Rotation2d; +import org.wpilib.math.util.Units; +import org.wpilib.telemetry.Telemetry; +import org.wpilib.telemetry.TelemetryTable; + + //[checkin] +public class Drivetrain implements Mechanism { + private final SparkMax leftLeader = new SparkMax(CANPort.CAN_S0, 0, MotorType.kBrushless); + private final SparkMax leftFollower = new SparkMax(CANPort.CAN_S0, 1, MotorType.kBrushless); + private final SparkMax rightLeader = new SparkMax(CANPort.CAN_S0, 2, MotorType.kBrushless); + private final SparkMax rightFollower = new SparkMax(CANPort.CAN_S0, 3, MotorType.kBrushless); + + private final OnboardIMU imu = new OnboardIMU(MountOrientation.FLAT); + private final DifferentialDrive differentialDrive = + new DifferentialDrive(leftLeader::setThrottle, rightLeader::setThrottle); + + private final DrivetrainSim drivetrainSim = new DrivetrainSim(leftLeader, rightLeader); + + // [wheel] + private static final double WHEEL_RADIUS = Units.inchesToMeters(3.0); + // [/wheel] + // [gearRatio] + private static final double GEAR_RATIO = 10.71; + // [/gearRatio] + // [pidController] + private final PIDController driveDistanceController = new PIDController(0, 0, 0); + private final PIDController headingController = new PIDController(0, 0, 0); + // [/pidController] + // [sensors] + private final RelativeEncoder leftEncoder = leftLeader.getEncoder(); + private final RelativeEncoder rightEncoder = rightLeader.getEncoder(); + // [/sensors] + + private final TelemetryTable table = Telemetry.getTable("Drive"); + private final TelemetryTable driveDistanceTable = table.getTable("Drive Distance"); + private final TelemetryTable turnTable = table.getTable("Turn in Place"); + + public Drivetrain() { + var leftConfig = new SparkMaxConfig().inverted(true); + leftLeader.configure( + leftConfig, ResetMode.kResetSafeParameters, PersistMode.kPersistParameters); + leftFollower.configure( + leftConfig.follow(leftLeader), + ResetMode.kResetSafeParameters, + PersistMode.kPersistParameters); + + var rightConfig = new SparkMaxConfig().inverted(false); + rightLeader.configure( + rightConfig, ResetMode.kResetSafeParameters, PersistMode.kPersistParameters); + rightFollower.configure( + rightConfig.follow(rightLeader), + ResetMode.kResetSafeParameters, + PersistMode.kPersistParameters); + + setDefaultCommand(idle()); + + //[pidController2] + driveDistanceController.setTolerance(0.05, 0.05); + headingController.setTolerance(0.05, 0.2); + headingController.enableContinuousInput(-Math.PI, Math.PI); + //[/pidController2] + } + + //[/checkin] + public Command idle() { + return run(coroutine -> { + while (true) { + differentialDrive.arcadeDrive(0.0, 0.0); + coroutine.yield(); + } + }) + .named("Idle"); + } + + public Command arcadeDrive(DoubleSupplier forwardThrottle, DoubleSupplier rotationThrottle) { + return run(coroutine -> { + while (true) { + differentialDrive.arcadeDrive( + forwardThrottle.getAsDouble(), -rotationThrottle.getAsDouble()); + coroutine.yield(); + } + }) + .named("Drive"); + } + + public Command driveDistance(double distanceMeters) { + return run(coroutine -> { + double startingDistance = (getLeftDistance() + getRightDistance()) / 2; + double targetDistance = startingDistance + distanceMeters; + + driveDistanceController.setSetpoint(targetDistance); + driveDistanceController.reset(); + + while (!driveDistanceController.atSetpoint()) { + double currentDistance = (getLeftDistance() + getRightDistance()) / 2; + + double effort = driveDistanceController.calculate(currentDistance); + differentialDrive.arcadeDrive(effort, 0); + + driveDistanceTable.log("Target", Meters.of(targetDistance)); + driveDistanceTable.log("Current", Meters.of(currentDistance)); + driveDistanceTable.log("Error", Meters.of(driveDistanceController.getError())); + driveDistanceTable.log("Effort", effort); + + coroutine.yield(); + } + }) + .named("Drive Distance " + distanceMeters + "m"); + } + + public Command turnInPlace(Rotation2d target) { + return run(coroutine -> { + headingController.setSetpoint(target.getRadians()); + headingController.reset(); + + while (!headingController.atSetpoint()) { + double effort = headingController.calculate(getHeading().getRadians()); + + differentialDrive.arcadeDrive(0, effort); + + turnTable.log("Target", target); + turnTable.log("Current", getHeading()); + turnTable.log("Error", Radians.of(headingController.getError())); + turnTable.log("Effort", effort); + + coroutine.yield(); + } + }) + .named("Turn in Place to angle " + target.getDegrees() + "deg"); + } + + public void periodic() { + if (RobotBase.isSimulation()) { + drivetrainSim.periodic(); + } + + table.log("Left Distance", Meters.of(getLeftDistance())); + table.log("Left Velocity", MetersPerSecond.of(getLeftVelocity())); + table.log("Right Distance", Meters.of(getRightDistance())); + table.log("Right Velocity", MetersPerSecond.of(getRightVelocity())); + table.log("Heading", getHeading()); + table.log("Active Commands", getRunningCommands().stream().map(Command::name).toArray()); + } + + public double getLeftDistance() { + return Units.rotationsToRadians(leftEncoder.getPosition().get()) / GEAR_RATIO * WHEEL_RADIUS; + } + + public double getRightDistance() { + return Units.rotationsToRadians(rightEncoder.getPosition().get()) / GEAR_RATIO * WHEEL_RADIUS; + } + + public double getLeftVelocity() { + return Units.rotationsPerMinuteToRadiansPerSecond(leftEncoder.getVelocity().get()) + / GEAR_RATIO + * WHEEL_RADIUS; + } + + public double getRightVelocity() { + return Units.rotationsPerMinuteToRadiansPerSecond(rightEncoder.getVelocity().get()) + / GEAR_RATIO + * WHEEL_RADIUS; + } + + public Rotation2d getHeading() { + return imu.getRotation2d(); + } + + public void setPose(Pose2d pose) { + drivetrainSim.setPose(pose); + } +} diff --git a/public/learning-course/stage1/stage1c/graph.webp b/public/learning-course/stage1/stage1c/graph.webp new file mode 100644 index 00000000..966be50f Binary files /dev/null and b/public/learning-course/stage1/stage1c/graph.webp differ diff --git a/public/learning-course/stage1/stage1c/pid_d.webm b/public/learning-course/stage1/stage1c/pid_d.webm new file mode 100644 index 00000000..ff512982 Binary files /dev/null and b/public/learning-course/stage1/stage1c/pid_d.webm differ diff --git a/public/learning-course/stage1/stage1c/pid_i.webm b/public/learning-course/stage1/stage1c/pid_i.webm new file mode 100644 index 00000000..b49625de Binary files /dev/null and b/public/learning-course/stage1/stage1c/pid_i.webm differ diff --git a/public/learning-course/stage1/stage1c/pid_p.webm b/public/learning-course/stage1/stage1c/pid_p.webm new file mode 100644 index 00000000..fcd9eb53 Binary files /dev/null and b/public/learning-course/stage1/stage1c/pid_p.webm differ diff --git a/src/config/sidebarConfig.ts b/src/config/sidebarConfig.ts index 08fdee28..da32c267 100644 --- a/src/config/sidebarConfig.ts +++ b/src/config/sidebarConfig.ts @@ -217,6 +217,14 @@ const learningCourse: SidebarTopic = { label: 'Stage 1C Introduction', slug: 'learning-course/stage1/stage1c/stage-overview', }, + { + label: 'PID Intro', + slug: 'learning-course/stage1/stage1c/pid-intro', + }, + { + label: 'Exercise - Adding PID to Kibot', + slug: 'learning-course/stage1/stage1c/pid-kitbot-pt1', + }, ], }, ], diff --git a/src/content/docs/learning-course/index.mdx b/src/content/docs/learning-course/index.mdx index fa87fc2b..a9241dfb 100644 --- a/src/content/docs/learning-course/index.mdx +++ b/src/content/docs/learning-course/index.mdx @@ -6,6 +6,7 @@ description: A guided curriculum for learning FRC Programming import CourseSection from '@components/CourseSection.astro'; Welcome to FRCSoftware's learning course! + The purpose of this course is to help students learn how to program an FRC robot no matter their skill level. The progression of the course moves from learning how to write Java to programming robots of increasing complexity. We start with individual mechanisms of robots, using the FRC Kitbot, then move to more complex robots. diff --git a/src/content/docs/learning-course/stage1/stage1c/pid-intro.mdx b/src/content/docs/learning-course/stage1/stage1c/pid-intro.mdx new file mode 100644 index 00000000..337994f3 --- /dev/null +++ b/src/content/docs/learning-course/stage1/stage1c/pid-intro.mdx @@ -0,0 +1,183 @@ +--- +title: Controls Introduction +description: An overview of Feedback Controllers and PID +--- + +import ContentImage from '@components/ContentImage.astro'; + +With FRC games, it's common for teams to have a mechanism move to a desired position or move the robot to a desired position or angle. +One example is auto aligning. +Robots auto align by turning a certain amount of degrees towards the goal then take the steps needed to score the game piece. +This helps makes scoring more efficient. +In 2026, the kitbot might auto align to the goal by turning to a certain angle on the field. +The kitbot can't turn too far to the left or too far to the right because then it might miss the goal. +It has to turn just the right amount. +How do teams make sure that happens? +It's with a feedback controller called PID. + +## Feedback Controller + +A feedback controller uses sensors and equations to figure out what the mechanism is currently doing, +and controls the mechanism's output to make it do what we want it do. +The mechanisms's current state is called the **output**. +The desired state of the mechanism, where we want it to be, is called the **setpoint**. + +In the case of the kitbot's auto align, the **output** is the information from the sensor on what the robot's current angle is. +The **setpoint** is a target angle, perhaps pointing the kitbot towards the goal. + + + +Feedback controllers are also used outside of FRC. +One example is an air conditioner. +An air conditioner has a sensor that compares the current temperature with the desired temperature. +Then the air conditioner directs the airflow in a way which allows the air to get to the desired temperature. + +A type of feedback controller is PID. +This stage will use PID to help the kitbot auto align and drive towards the goal. + +## PID + +PID is a type of feedback controller that uses three terms: proportional, integral, and derivative. +Together the three terms are P, I, and D, which is how PID gets its name. + +PID works by constantly comparing the current output to the setpoint and changing the control effort, or in our case, the voltage, commanded to the motor. +The difference between the outpoint to the setpoint is called the error. + +- Proportional (P) is what gets you close to the setpoint. + It's proportional to the error. +- Integral (I) helps carefully creep the rest of the way to the setpoint. + It's the integral of the error. +- Derivative (D) is a braking force to stop or “dampen” any oscillations. + It's the derivative of the error + +In code, the three terms are commonly noted as `kp`, `ki`, and `kd`. +The k stands for constants. + +### How does P, I, and D work together? + +PID is constantly comparing where you are to where you want to be and adjusting. +This is possible because each term is working to help adjust. +The example shows the three terms working to get the output to match the setpoint. + +First we start off with tuning P. +As P increases, the output overshoots and oscillates. +Overshooting is when the output passes the setpoint, and oscillating means it passes the setpoint repeatedly. + + + Photo Credit: Wikipedia + + +P gets the output close to the setpoint but it's not there yet. +Therefore the example introduces I. +As I increases, the oscillations get bigger. + + + + + Photo Credit: Wikipedia + + +P and I have both been added but the output doesn't match the setpoint. +However, there are still oscillations and the output still overshoots. +To solve this, the example introduces D. +As D increases, the oscillations smooth out and the output slowly stops overshooting. +Now the output matches up with the setpoint! + + + Photo Credit: Wikipedia + + +### Tuning PID + +Adjusting the three terms to get the desired output is called **tuning**. +Tuning PID is necessary because every mechanism is different and behaves differently. +Even two theoretically identical robots may require different PID constants, as they may be small mechanical differences that impact how mechanisms behave. + + + +When tuning PID, the first step is to set all three terms to 0. +The first value that you want to tune is P. +Increase P until the output is near the setpoint but it starts to oscillate. + +If the output is close to the target but just misses it, I can be increased to push it towards the set point. +However, in FRC, this part is commonly skipped. + +If there are any oscillations, D can be introduced. +By increasing D, it can help smooth out the oscillations. +However, make sure that it isn't increased so much that it causes jittering. + +Similar to the examples, graphs are used to tune PID. +By using a graph, we can see the mechanism's output, compare it to the setpoint, and adjust the different terms until +the output matches the setpoint. + +### PID Exercise + +In the simulation below, there is a water tank that is filling up with water. +The water tank has a fill line to show where the water should stop, this is the setpoint. +The graph below shows the output (labeled "actual") and the setpoint. +To ensure that the water stops at the setpoint, PID can be used to adjust the flow into the tank. +Tune the P, I, and D values until the water stops at the water line. +The graph can be used to help! + +
+