├── .classpath ├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ └── build_release.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── pictures ├── StuyLib Logo.svg ├── StuyLibLogo.png └── StuyLibREADME.png ├── settings.gradle ├── src └── com │ └── stuypulse │ └── stuylib │ ├── control │ ├── Controller.java │ ├── ControllerGroup.java │ ├── angle │ │ ├── AngleController.java │ │ ├── AngleControllerGroup.java │ │ ├── feedback │ │ │ └── AnglePIDController.java │ │ └── feedforward │ │ │ ├── AngleArmFeedforward.java │ │ │ └── AnglePositionFeedforwardController.java │ ├── feedback │ │ └── PIDController.java │ ├── feedforward │ │ ├── ArmFeedforward.java │ │ ├── ElevatorFeedforward.java │ │ ├── MotorFeedforward.java │ │ ├── PositionFeedforwardController.java │ │ └── VelocityFeedforwardController.java │ └── readme.md │ ├── input │ ├── Gamepad.java │ ├── WPIGamepad.java │ ├── gamepads │ │ ├── AutoGamepad.java │ │ ├── Logitech.java │ │ ├── PS4.java │ │ ├── Xbox.java │ │ └── readme.md │ └── readme.md │ ├── math │ ├── Angle.java │ ├── SLMath.java │ ├── Vector2D.java │ └── readme.md │ ├── network │ ├── SmartBoolean.java │ ├── SmartNumber.java │ ├── SmartString.java │ └── readme.md │ ├── readme.md │ ├── streams │ ├── angles │ │ ├── AStick.java │ │ ├── AStream.java │ │ ├── FilteredAStream.java │ │ ├── PollingAStream.java │ │ └── filters │ │ │ ├── AFilter.java │ │ │ ├── AFilterGroup.java │ │ │ ├── AHighPassFilter.java │ │ │ ├── ALowPassFilter.java │ │ │ ├── AMotionProfile.java │ │ │ └── ARateLimit.java │ ├── booleans │ │ ├── BStream.java │ │ ├── FilteredBStream.java │ │ ├── PollingBStream.java │ │ └── filters │ │ │ ├── BButton.java │ │ │ ├── BButtonRC.java │ │ │ ├── BDebounce.java │ │ │ ├── BDebounceRC.java │ │ │ ├── BFilter.java │ │ │ └── BFilterGroup.java │ ├── numbers │ │ ├── FilteredIStream.java │ │ ├── IStream.java │ │ ├── NumberStream.java │ │ ├── PollingIStream.java │ │ └── filters │ │ │ ├── Derivative.java │ │ │ ├── HighPassFilter.java │ │ │ ├── IFilter.java │ │ │ ├── IFilterGroup.java │ │ │ ├── LowPassFilter.java │ │ │ ├── MotionProfile.java │ │ │ ├── RateLimit.java │ │ │ ├── TimedMovingAverage.java │ │ │ └── readme.md │ ├── readme.md │ └── vectors │ │ ├── FilteredVStream.java │ │ ├── PollingVStream.java │ │ ├── VStream.java │ │ └── filters │ │ ├── VClamp.java │ │ ├── VDeadZone.java │ │ ├── VDerivative.java │ │ ├── VFilter.java │ │ ├── VFilterGroup.java │ │ ├── VHighPassFilter.java │ │ ├── VLowPassFilter.java │ │ ├── VMotionProfile.java │ │ ├── VRateLimit.java │ │ ├── VTimedMovingAverage.java │ │ └── XYFilter.java │ └── util │ ├── AngleVelocity.java │ ├── Conversion.java │ ├── StopWatch.java │ └── readme.md └── vendordeps └── WPILibNewCommands.json /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | custom: https://stuypulse.com/donate/ 3 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, fork, pull_request] 4 | 5 | env: 6 | JAVA_VERSION: 1.17 7 | GRADLE_VERSION: 8.12.1 8 | 9 | jobs: 10 | spotless-check: 11 | name: Spotless Apply 12 | 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: Set up JDK ${{ env.JAVA_VERSION }} 19 | uses: actions/setup-java@v1 20 | with: 21 | java-version: ${{ env.JAVA_VERSION }} 22 | 23 | - name: Spotless Check - Gradle ${{ env.GRADLE_VERSION }} 24 | uses: gradle/gradle-build-action@v2.3.0-beta.1 25 | with: 26 | gradle-version: ${{ env.GRADLE_VERSION }} 27 | arguments: spotlessCheck 28 | 29 | javadocs-check: 30 | name: Javadocs 31 | 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v3 36 | 37 | - name: Set up JDK ${{ env.JAVA_VERSION }} 38 | uses: actions/setup-java@v1 39 | with: 40 | java-version: ${{ env.JAVA_VERSION }} 41 | 42 | - name: Build Javadocs - Gradle ${{ env.GRADLE_VERSION }} 43 | uses: gradle/gradle-build-action@v2.3.0-beta.1 44 | with: 45 | arguments: javadoc 46 | 47 | final-build: 48 | name: Finalize 49 | 50 | needs: 51 | - spotless-check 52 | - javadocs-check 53 | 54 | runs-on: ubuntu-latest 55 | steps: 56 | - name: Checkout 57 | uses: actions/checkout@v3 58 | 59 | - name: Set up JDK ${{ env.JAVA_VERSION }} 60 | uses: actions/setup-java@v1 61 | with: 62 | java-version: ${{ env.JAVA_VERSION }} 63 | 64 | - name: Build - Gradle ${{ env.GRADLE_VERSION }} 65 | uses: gradle/gradle-build-action@v2.3.0-beta.1 66 | with: 67 | arguments: build 68 | 69 | - name: Upload StuyLib Build 70 | uses: actions/upload-artifact@v4.0.0 71 | with: 72 | name: StuyLib-Release 73 | path: ./build/libs/*.jar 74 | retention-days: 90 75 | -------------------------------------------------------------------------------- /.github/workflows/build_release.yml: -------------------------------------------------------------------------------- 1 | name: Prepare Release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | permissions: 8 | contents: write 9 | pages: write 10 | id-token: write 11 | 12 | env: 13 | JAVA_VERSION: 1.17 14 | GRADLE_VERSION: 8.12.1 15 | 16 | jobs: 17 | pages: 18 | name: Deploy Javadocs 19 | environment: 20 | name: github-pages 21 | url: ${{ steps.deployment.outputs.page_url }} 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v3 26 | 27 | - name: Set up JDK ${{ env.JAVA_VERSION }} 28 | uses: actions/setup-java@v1 29 | with: 30 | java-version: ${{ env.JAVA_VERSION }} 31 | 32 | - name: Build Javadocs - Gradle ${{ env.GRADLE_VERSION }} 33 | uses: gradle/gradle-build-action@v2.11.1 34 | with: 35 | arguments: javadoc -Pversion=${{ github.event.release.tag_name }} 36 | 37 | - name: Setup Pages 38 | uses: actions/configure-pages@v2.0.0 39 | 40 | - name: Upload artifact 41 | uses: actions/upload-pages-artifact@v4.0.0 42 | with: 43 | path: './docs/' 44 | name: github-pages 45 | 46 | - name: Deploy to GitHub Pages 47 | id: deployment 48 | uses: actions/deploy-pages@v4.0.3 49 | with: 50 | artifact_name: github-pages 51 | 52 | deploy: 53 | name: Prepare Jar & JitPack 54 | 55 | runs-on: ubuntu-latest 56 | steps: 57 | - name: Checkout 58 | uses: actions/checkout@v3 59 | 60 | - name: Set up JDK ${{ env.JAVA_VERSION }} 61 | uses: actions/setup-java@v1 62 | with: 63 | java-version: ${{ env.JAVA_VERSION }} 64 | 65 | - name: Build - Gradle ${{ env.GRADLE_VERSION }} 66 | uses: gradle/gradle-build-action@v2.11.1 67 | with: 68 | arguments: build -Pversion=${{ github.event.release.tag_name }} 69 | 70 | - name: Upload Assets to Release 71 | uses: AButler/upload-release-assets@v2.0 72 | with: 73 | files: ./build/libs/*.jar 74 | repo-token: ${{ secrets.GITHUB_TOKEN }} 75 | 76 | - name: Update JitPack 77 | uses: badasintended/autojitpack@v1 78 | with: 79 | version: ${{ github.event.release.tag_name }} 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything 2 | * 3 | !.gitignore 4 | 5 | # Folders / Formats used for code 6 | !.github/ 7 | !.github/**/* 8 | 9 | !gradle/ 10 | !gradle/**/* 11 | 12 | !pictures/ 13 | !pictures/**/* 14 | 15 | !vendordeps/ 16 | !vendordeps/**/* 17 | 18 | !src/ 19 | !src/**/* 20 | 21 | # Files reauired by gradle 22 | !build.gradle 23 | !gradlew 24 | !gradlew.bat 25 | !settings.gradle 26 | 27 | # Github Files 28 | !LICENSE 29 | !README.md 30 | 31 | # Files used for JitPack CI 32 | !.classpath 33 | !.travis.yml 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk17 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 StuyPulse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![(Stuylib Logo)](https://github.com/StuyPulse/StuyLib/blob/main/pictures/StuyLibREADME.png?raw=true) 2 | 3 | [![Jitpack Website](https://jitpack.io/v/StuyPulse/StuyLib.svg)](https://jitpack.io/#StuyPulse/StuyLib) 4 | [![Build](https://github.com/StuyPulse/StuyLib/actions/workflows/build.yml/badge.svg)](https://github.com/StuyPulse/StuyLib/actions/workflows/build.yml) 5 | 6 | --- 7 | 8 | # What is StuyLib? 9 | 10 | StuyLib is an FRC library / toolkit which includes many different utilities for programming. It includes things like a Gamepad Library, a Limelight Library, a Digitial Filter / Streams Library, and many other utilities relating to math and programming. 11 | 12 | # Awards 13 | 14 | - IStreams / IStreamFilters: 15 | - [Innovation in Control Award sponsored by Rockwell Automation](https://www.thebluealliance.com/event/2020scmb#awards) 16 | - Limelight + Encoder Fusion / Drive Filters: 17 | - [Innovation in Control Award](https://www.thebluealliance.com/event/2022nyro#awards) 18 | - Clean & Concise Code / Smart Network Library: 19 | - [Innovation in Control Award](https://www.thebluealliance.com/event/2022nyli2#awards) 20 | - Usage of Encoders / Limelight + Encoder Fusion: 21 | - [Innovation in Control Award](https://www.thebluealliance.com/event/2022nyny#awards) 22 | 23 | # How do I use StuyLib? 24 | 25 | ### Setting up StuyLib 26 | 27 | [Instructions from JitPack.io](https://jitpack.io/#StuyPulse/StuyLib) 28 | 29 | Step 0. This is done by default for robot projects, but if your doing a local project you must remember to include this at the top 30 | ```groovy 31 | plugins { 32 | id "java" 33 | 34 | // You can use different versions of GradleRIO between your robot 35 | // code and Stuylib as long as the updates don't break anything 36 | id "edu.wpi.first.GradleRIO" version "2020.2.2" 37 | } 38 | ``` 39 | 40 | Step 1. Add the JitPack repository to your `build.gradle` 41 | 42 | ```groovy 43 | allprojects { 44 | repositories { 45 | maven { url 'https://jitpack.io' } 46 | } 47 | } 48 | ``` 49 | 50 | - Step 2. Add the dependency to `build.gradle` 51 | 52 | [![Jitpack Website](https://jitpack.io/v/StuyPulse/StuyLib.svg)](https://jitpack.io/#StuyPulse/StuyLib) 53 | ```groovy 54 | dependencies { 55 | // check tag above for most recent version 56 | compile 'com.github.StuyPulse:StuyLib:VERSION' 57 | } 58 | ``` 59 | 60 | 61 | ### Other Dependencies 62 | 63 | If you want the NetworkTable classes to work on something other than the robot, then you need to include other dependencies. 64 | 65 | If you are using network tables anywhere else than the robot, ie. on a laptop, you will need to include the jar files in the `./lib` folder. For example, when using NetKeyboard, you need to run a program to send the keyboard input, [here is the program](https://github.com/Sam-Belliveau/NetworkKeyboardServer). This program uses these jar files to send network table data to the robot. It is otherwise pretty rare that you would need to use these 66 | 67 | # Criteria for Code Contribution 68 | 69 | When contributing code, as always, make a branch and a pull request. 70 | 71 | All code MUST be commented, and no single class should be too complicated. 72 | 73 | At the top of each class, give a brief description of the class, along with an author tag which contains an email just in case anybody has any questions on how to use it. 74 | 75 | # Star History 76 | 77 | [![Star History Chart](https://api.star-history.com/svg?repos=StuyPulse/StuyLib&type=Date)](https://star-history.com/#StuyPulse/StuyLib&Date) 78 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.WPI_VER = "2025.2.1" 3 | } 4 | 5 | plugins { 6 | id "java" 7 | 8 | id "maven-publish" 9 | 10 | id "edu.wpi.first.GradleRIO" version "2025.2.1" 11 | id "com.diffplug.spotless" version "6.22.0" 12 | id "com.github.spotbugs" version "5.2.0" 13 | } 14 | 15 | group = 'com.stuypulse' 16 | 17 | java { 18 | sourceCompatibility = JavaVersion.VERSION_17 19 | targetCompatibility = JavaVersion.VERSION_17 20 | } 21 | 22 | sourceSets { 23 | main { 24 | java { 25 | srcDir "src" 26 | } 27 | } 28 | } 29 | 30 | configurations { 31 | jaxDoclet 32 | } 33 | 34 | dependencies { 35 | implementation wpi.java.deps.wpilib() 36 | implementation wpi.java.vendor.java() 37 | 38 | nativeDebug wpi.java.deps.wpilibJniDebug(wpi.platforms.desktop) 39 | nativeDebug wpi.java.vendor.jniDebug(wpi.platforms.desktop) 40 | simulationDebug wpi.sim.enableDebug() 41 | 42 | nativeRelease wpi.java.deps.wpilibJniRelease(wpi.platforms.desktop) 43 | nativeRelease wpi.java.vendor.jniRelease(wpi.platforms.desktop) 44 | simulationRelease wpi.sim.enableRelease() 45 | 46 | implementation "edu.wpi.first.ntcore:ntcore-java:${WPI_VER}" 47 | implementation "edu.wpi.first.wpiutil:wpiutil-java:${WPI_VER}" 48 | runtimeOnly "edu.wpi.first.ntcore:ntcore-jni:${WPI_VER}:windowsx86" 49 | runtimeOnly "edu.wpi.first.ntcore:ntcore-jni:${WPI_VER}:windowsx86-64" 50 | runtimeOnly "edu.wpi.first.ntcore:ntcore-jni:${WPI_VER}:linuxx86-64" 51 | runtimeOnly "edu.wpi.first.ntcore:ntcore-jni:${WPI_VER}:osxx86-64" 52 | 53 | implementation group: 'org.knowm.xchart', name: 'xchart', version: '3.2.2' 54 | 55 | testImplementation 'junit:junit:4.12' 56 | } 57 | 58 | publishing { 59 | publications { 60 | maven(MavenPublication) { 61 | groupId = 'com.stuypulse.stuylib' 62 | artifactId = 'library' 63 | version = "${version}" 64 | 65 | from components.java 66 | } 67 | } 68 | } 69 | 70 | final YEAR = new Date().format('yyyy') 71 | 72 | spotless { 73 | java { 74 | licenseHeader """ 75 | /* Copyright (c) ${YEAR} StuyPulse Robotics. All rights reserved. */ 76 | /* This work is licensed under the terms of the MIT license */ 77 | /* found in the root directory of this project. */ 78 | 79 | """ 80 | 81 | googleJavaFormat('1.13.0').aosp() 82 | 83 | trimTrailingWhitespace() 84 | endWithNewline() 85 | 86 | importOrder('com.stuypulse.stuylib', 'com.stuypulse', 'edu.wpi') 87 | removeUnusedImports() 88 | } 89 | } 90 | 91 | // Example to configure HTML report 92 | spotbugsMain { 93 | reports { 94 | html { 95 | enabled = true 96 | destination = file("$buildDir/reports/spotbugs/main/spotbugs.html") 97 | stylesheet = 'fancy-hist.xsl' 98 | } 99 | } 100 | } 101 | 102 | spotbugs { 103 | ignoreFailures = true 104 | effort = 'max' 105 | } 106 | 107 | javadoc { 108 | destinationDir = file('docs/') 109 | } 110 | 111 | build { 112 | dependsOn javadoc 113 | } 114 | 115 | jar { 116 | from sourceSets.main.allSource 117 | } 118 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StuyPulse/StuyLib/2bba1f56355ca82b72959593f4adc5c22ab98abc/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=permwrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=permwrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /pictures/StuyLibLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StuyPulse/StuyLib/2bba1f56355ca82b72959593f4adc5c22ab98abc/pictures/StuyLibLogo.png -------------------------------------------------------------------------------- /pictures/StuyLibREADME.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StuyPulse/StuyLib/2bba1f56355ca82b72959593f4adc5c22ab98abc/pictures/StuyLibREADME.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | import org.gradle.internal.os.OperatingSystem 2 | 3 | pluginManagement { 4 | repositories { 5 | mavenLocal() 6 | gradlePluginPortal() 7 | String frcYear = '2025' 8 | File frcHome 9 | if (OperatingSystem.current().isWindows()) { 10 | String publicFolder = System.getenv('PUBLIC') 11 | if (publicFolder == null) { 12 | publicFolder = "C:\\Users\\Public" 13 | } 14 | def homeRoot = new File(publicFolder, "wpilib") 15 | frcHome = new File(homeRoot, frcYear) 16 | } else { 17 | def userFolder = System.getProperty("user.home") 18 | def homeRoot = new File(userFolder, "wpilib") 19 | frcHome = new File(homeRoot, frcYear) 20 | } 21 | def frcHomeMaven = new File(frcHome, 'maven') 22 | maven { 23 | name 'frcHome' 24 | url frcHomeMaven 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/control/Controller.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.control; 6 | 7 | import com.stuypulse.stuylib.streams.numbers.filters.IFilter; 8 | 9 | /** 10 | * A controller calculates an output variable given a setpoint and measurement of a single variable. 11 | * 12 | *

This base class can be used to represent single-input single-output control (SISO) algorithms 13 | * (commonly PID and feedforward). 14 | * 15 | *

For any controller, digital filters can be applied to the incoming setpoints and measurements, 16 | * or the outgoing outputs. This allows for the easy application of filters often involved with 17 | * control theory, like motion profile filters for setpoints and low-pass filters for noisy 18 | * measurements. *These filters are already provided in the StuyLib filters library.* 19 | * 20 | *

Different control schemes that share the same setpoint and measurement can also be concisely 21 | * composed together if they all implement this class. 22 | * 23 | * @author Myles Pasetsky (myles.pasetsky@gmail.com) 24 | */ 25 | public abstract class Controller { 26 | 27 | /** The most recent setpoint of the controller */ 28 | private double mSetpoint; 29 | 30 | /** The most recent measurement of the controller */ 31 | private double mMeasurement; 32 | 33 | /** The most recent output of the controller */ 34 | private double mOutput; 35 | 36 | /** The filter that is applied to the supplied setpoints */ 37 | private IFilter mSetpointFilter; 38 | 39 | /** The filter that is applied to the supplied measurements */ 40 | private IFilter mMeasurementFilter; 41 | 42 | /** The filter that is applied to the calculated outputs */ 43 | private IFilter mOutputFilter; 44 | 45 | /** Default initialization of a controller */ 46 | public Controller() { 47 | mSetpoint = 0.0; 48 | mMeasurement = 0.0; 49 | mOutput = 0.0; 50 | 51 | mSetpointFilter = x -> x; 52 | mMeasurementFilter = x -> x; 53 | mOutputFilter = x -> x; 54 | } 55 | 56 | /** 57 | * Set the setpoint filter of the controller 58 | * 59 | * @param setpointFilter setpoint filters 60 | * @return reference to the controller 61 | */ 62 | public final Controller setSetpointFilter(IFilter... setpointFilter) { 63 | mSetpointFilter = IFilter.create(setpointFilter); 64 | return this; 65 | } 66 | 67 | /** 68 | * Set the measurement filter of the controller 69 | * 70 | * @param measurementFilter measurement filters 71 | * @return reference to the controller 72 | */ 73 | public final Controller setMeasurementFilter(IFilter... measurementFilter) { 74 | mMeasurementFilter = IFilter.create(measurementFilter); 75 | return this; 76 | } 77 | 78 | /** 79 | * Set the output filter of the controller 80 | * 81 | * @param outputFilter output filters 82 | * @return reference to the controller 83 | */ 84 | public final Controller setOutputFilter(IFilter... outputFilter) { 85 | mOutputFilter = IFilter.create(outputFilter); 86 | return this; 87 | } 88 | 89 | /** @return the most recent setpoint of the controller */ 90 | public final double getSetpoint() { 91 | return mSetpoint; 92 | } 93 | 94 | /** @return the most recent measurement of the controller */ 95 | public final double getMeasurement() { 96 | return mMeasurement; 97 | } 98 | 99 | /** @return the most recent output of the controller */ 100 | public final double getOutput() { 101 | return mOutput; 102 | } 103 | 104 | /** @return the most recent error of the controller */ 105 | public final double getError() { 106 | return getSetpoint() - getMeasurement(); 107 | } 108 | 109 | /** 110 | * Determines if the controller is finished based on an acceptable error. 111 | * 112 | * @param acceptableError acceptable error for the controller 113 | * @return whether or not the controller is done 114 | */ 115 | public final boolean isDone(double acceptableError) { 116 | return Math.abs(getError()) < acceptableError; 117 | } 118 | 119 | /** 120 | * Combines this controller into a group with other controllers that share the same setpoint and 121 | * measurement. 122 | * 123 | * @param other the other controllers 124 | * @return the group of controllers that 125 | */ 126 | public final ControllerGroup add(Controller... other) { 127 | return new ControllerGroup(this, other); 128 | } 129 | 130 | /** 131 | * Updates the state of the controller. 132 | * 133 | *

Applies filters to setpoint and measurement, calculates output with filtered values, 134 | * filters and returns output 135 | * 136 | * @param setpoint setpoint of the variable being controlled 137 | * @param measurement measurement of the variable being controlled 138 | * @return the final output 139 | */ 140 | public final double update(double setpoint, double measurement) { 141 | mSetpoint = mSetpointFilter.get(setpoint); 142 | mMeasurement = mMeasurementFilter.get(measurement); 143 | 144 | return mOutput = mOutputFilter.get(calculate(mSetpoint, mMeasurement)); 145 | } 146 | 147 | /** 148 | * Calculates the output of the controller given a setpoint and measurement. 149 | * 150 | * @param setpoint setpoint 151 | * @param measurement measurement 152 | * @return calculated output 153 | */ 154 | protected abstract double calculate(double setpoint, double measurement); 155 | } 156 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/control/ControllerGroup.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.control; 6 | 7 | /** 8 | * Controllers can be grouped together in a "controller group" if they have the same setpoint and 9 | * measurement. 10 | * 11 | *

This allows different controller implementations that are controlling the same variable to be 12 | * concisely composed together. 13 | * 14 | * @author Myles Pasetsky (myles.pasetsky@gmail.com) 15 | */ 16 | public class ControllerGroup extends Controller { 17 | 18 | /** Controller part of the group */ 19 | private final Controller mController; 20 | 21 | /** Controllers part of the group */ 22 | private final Controller[] mControllers; 23 | 24 | /** Create a controller group */ 25 | public ControllerGroup(Controller controller, Controller... controllers) { 26 | mController = controller; 27 | mControllers = controllers; 28 | } 29 | 30 | /** 31 | * Updates the internal controllers with the setpoint and measurement and returns their combined 32 | * output. 33 | * 34 | * @param setpoint setpoint 35 | * @param measurement measurement 36 | * @return summed output of the interal controllers 37 | */ 38 | @Override 39 | protected double calculate(double setpoint, double measurement) { 40 | double output = mController.update(setpoint, measurement); 41 | 42 | for (Controller controller : mControllers) { 43 | output += controller.update(setpoint, measurement); 44 | } 45 | 46 | return output; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/control/angle/AngleController.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.control.angle; 6 | 7 | import com.stuypulse.stuylib.math.Angle; 8 | import com.stuypulse.stuylib.streams.angles.filters.AFilter; 9 | import com.stuypulse.stuylib.streams.numbers.filters.IFilter; 10 | 11 | /** 12 | * Base class of controller classes of continuous systems. This means that both the setpoint and 13 | * measurement are angles, as opposed to just numbers. 14 | * 15 | *

Other than this, works the same as the Controller class. 16 | * 17 | * @see com.stuypulse.stuylib.math.Angle 18 | * @see com.stuypulse.stuylib.control.Controller 19 | * @author Myles Pasetsky (myles.pasetsky@gmail.com) 20 | */ 21 | public abstract class AngleController { 22 | 23 | /** The most recent setpoint of the controller */ 24 | private Angle mSetpoint; 25 | 26 | /** The most recent measurement of the controller */ 27 | private Angle mMeasurement; 28 | 29 | /** The most recent output of the controller */ 30 | private double mOutput; 31 | 32 | /** The filter that is applied to the supplied setpoints */ 33 | private AFilter mSetpointFilter; 34 | 35 | /** The filter that is applied to the supplied measurements */ 36 | private AFilter mMeasurementFilter; 37 | 38 | /** The filter that is applied to the calculated outputs */ 39 | private IFilter mOutputFilter; 40 | 41 | /** Default initialization of an angle controller */ 42 | public AngleController() { 43 | mSetpoint = Angle.kZero; 44 | mMeasurement = Angle.kZero; 45 | mOutput = 0.0; 46 | 47 | mSetpointFilter = x -> x; 48 | mMeasurementFilter = x -> x; 49 | mOutputFilter = x -> x; 50 | } 51 | 52 | /** 53 | * Set the setpoint filter of the controller 54 | * 55 | * @param setpointFilter angular setpoint filters 56 | * @return reference to the controller 57 | */ 58 | public final AngleController setSetpointFilter(AFilter... setpointFilter) { 59 | mSetpointFilter = AFilter.create(setpointFilter); 60 | return this; 61 | } 62 | 63 | /** 64 | * Set the measurement filter of the controller 65 | * 66 | * @param measurementFilter angular measurement filters 67 | * @return reference to the controller 68 | */ 69 | public final AngleController setMeasurementFilter(AFilter... measurementFilter) { 70 | mMeasurementFilter = AFilter.create(measurementFilter); 71 | return this; 72 | } 73 | 74 | /** 75 | * Set the output filter of the controller 76 | * 77 | * @param outputFilter output filters 78 | * @return reference to the controller 79 | */ 80 | public final AngleController setOutputFilter(IFilter... outputFilter) { 81 | mOutputFilter = IFilter.create(outputFilter); 82 | return this; 83 | } 84 | 85 | /** @return the most recent setpoint of the controller */ 86 | public final Angle getSetpoint() { 87 | return mSetpoint; 88 | } 89 | 90 | /** @return the most recent measurement of the controller */ 91 | public final Angle getMeasurement() { 92 | return mMeasurement; 93 | } 94 | 95 | /** @return the most recent output of the controller */ 96 | public final double getOutput() { 97 | return mOutput; 98 | } 99 | 100 | /** @return the most recent error of the controller */ 101 | public final Angle getError() { 102 | return getSetpoint().sub(getMeasurement()); 103 | } 104 | 105 | /** 106 | * Determines if the controller is finished based on an acceptable radian error. 107 | * 108 | * @param acceptableError acceptable error for the controller 109 | * @return whether or not the controller is done 110 | */ 111 | public final boolean isDoneRadians(double acceptableError) { 112 | return Math.abs(getError().toRadians()) < acceptableError; 113 | } 114 | 115 | /** 116 | * Determines if the controller is finished based on an acceptable degree error. 117 | * 118 | * @param acceptableError acceptable error for the controller 119 | * @return whether or not the controller is done 120 | */ 121 | public final boolean isDoneDegrees(double acceptableError) { 122 | return Math.abs(getError().toDegrees()) < acceptableError; 123 | } 124 | 125 | /** 126 | * Combines this controller into a group with other controllers that share the same setpoint and 127 | * measurement. 128 | * 129 | * @param other the other controllers 130 | * @return the group of controllers that 131 | */ 132 | public final AngleControllerGroup add(AngleController... other) { 133 | return new AngleControllerGroup(this, other); 134 | } 135 | 136 | /** 137 | * Updates the state of the controller. 138 | * 139 | *

Applies filters to setpoint and measurement, calculates output with filtered values, 140 | * filters and returns output 141 | * 142 | * @param setpoint setpoint of the variable being controlled 143 | * @param measurement measurement of the variable being controlled 144 | * @return the final output 145 | */ 146 | public final double update(Angle setpoint, Angle measurement) { 147 | mSetpoint = mSetpointFilter.get(setpoint); 148 | mMeasurement = mMeasurementFilter.get(measurement); 149 | 150 | return mOutput = mOutputFilter.get(calculate(mSetpoint, mMeasurement)); 151 | } 152 | 153 | /** 154 | * Calculates the output of the controller given a setpoint and measurement. 155 | * 156 | * @param setpoint setpoint 157 | * @param measurement measurement 158 | * @return calculated output 159 | */ 160 | protected abstract double calculate(Angle setpoint, Angle measurement); 161 | } 162 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/control/angle/AngleControllerGroup.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.control.angle; 6 | 7 | import com.stuypulse.stuylib.math.Angle; 8 | 9 | /** 10 | * Angle controllers can be grouped together in a "controller group" if they have the same setpoint 11 | * and measurement. 12 | * 13 | *

This allows different controller implementations that are controlling the same variable to be 14 | * concisely composed together. 15 | * 16 | * @author Myles Pasetsky (myles.pasetsky@gmail.com) 17 | */ 18 | public class AngleControllerGroup extends AngleController { 19 | 20 | /** Controller part of the group */ 21 | private final AngleController mController; 22 | 23 | /** Controllers part of the group */ 24 | private final AngleController[] mControllers; 25 | 26 | /** Create a controller group */ 27 | public AngleControllerGroup(AngleController controller, AngleController... controllers) { 28 | mController = controller; 29 | mControllers = controllers; 30 | } 31 | 32 | /** 33 | * Updates the internal controllers with the setpoint and measurement and returns their combined 34 | * output. 35 | * 36 | * @param setpoint setpoint 37 | * @param measurement measurement 38 | * @return summed output of the interal controllers 39 | */ 40 | @Override 41 | protected double calculate(Angle setpoint, Angle measurement) { 42 | double output = mController.update(setpoint, measurement); 43 | 44 | for (AngleController controller : mControllers) { 45 | output += controller.update(setpoint, measurement); 46 | } 47 | 48 | return output; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/control/angle/feedback/AnglePIDController.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.control.angle.feedback; 6 | 7 | import com.stuypulse.stuylib.control.angle.AngleController; 8 | import com.stuypulse.stuylib.math.Angle; 9 | import com.stuypulse.stuylib.math.SLMath; 10 | import com.stuypulse.stuylib.network.SmartNumber; 11 | import com.stuypulse.stuylib.streams.numbers.filters.IFilter; 12 | import com.stuypulse.stuylib.streams.numbers.filters.IFilterGroup; 13 | import com.stuypulse.stuylib.util.StopWatch; 14 | 15 | /** 16 | * This PID controller is built by extending the AngleController class. It has a dynamic rate, so it 17 | * can detect how much time has passed between each update. It is made to be easy to use and simple 18 | * to understand while still being accurate. 19 | * 20 | *

The angle PID controller class is ALWAYS in units of *RADIANS* 21 | * 22 | * @author Sam (sam.belliveau@gmail.com) 23 | */ 24 | public class AnglePIDController extends AngleController { 25 | 26 | /** 27 | * Amount of time in between .update() calls that is aloud before the controller resets the 28 | * system 29 | */ 30 | private static final double kMaxTimeBeforeReset = 0.5; 31 | 32 | // Internal timer used by Controller 33 | private StopWatch mTimer; 34 | 35 | // Constants used by the PID controller 36 | private Number mP; 37 | private Number mI; 38 | private Number mD; 39 | 40 | // The Integral of the errors and filter for the I Component 41 | private double mIntegral; 42 | private IFilter mIFilter; 43 | 44 | // The Derivative of the error and the filter for the D Component 45 | private Angle mLastError; 46 | private IFilter mDFilter; 47 | 48 | /** 49 | * @param p The Proportional Multiplier 50 | * @param i The Integral Multiplier 51 | * @param d The Derivative Multiplier 52 | */ 53 | public AnglePIDController(Number p, Number i, Number d) { 54 | mTimer = new StopWatch(); 55 | 56 | mLastError = Angle.kZero; 57 | setIntegratorFilter(0, 0); 58 | setDerivativeFilter(x -> x); 59 | setPID(p, i, d); 60 | reset(); 61 | } 62 | 63 | /** Creates a blank PIDController that doesn't move */ 64 | public AnglePIDController() { 65 | this(-1, -1, -1); 66 | } 67 | 68 | /** 69 | * Resets the integrator in the PIDController. This automatically gets called if the gap between 70 | * update() commands is too large 71 | */ 72 | public void reset() { 73 | mIntegral = 0; 74 | } 75 | 76 | @Override 77 | protected double calculate(Angle setpoint, Angle measurement) { 78 | // Calculate error & time step 79 | Angle error = setpoint.sub(measurement); 80 | double dt = mTimer.reset(); 81 | 82 | // Calculate P Component 83 | double p_out = error.toRadians() * getP(); 84 | 85 | // Calculate I Component 86 | mIntegral += error.toRadians() * dt; 87 | mIntegral = mIFilter.get(mIntegral); 88 | double i_out = mIntegral * getI(); 89 | 90 | // Calculate D Component 91 | double derivative = mDFilter.get(error.velocityRadians(mLastError, dt)); 92 | mLastError = error; 93 | double d_out = derivative * getD(); 94 | 95 | // Check if time passed exceeds reset limit 96 | if (dt < kMaxTimeBeforeReset) { 97 | // Return the calculated result 98 | return p_out + i_out + d_out; 99 | } else { 100 | // If time in system is broken, then reset and return p part 101 | // This is because the P part is not relative to time 102 | reset(); 103 | return p_out; 104 | } 105 | } 106 | 107 | /** @return the P value being used by the PID controller. */ 108 | public double getP() { 109 | return Math.max(mP.doubleValue(), 0.0); 110 | } 111 | 112 | /** @return the P value being used by the PID controller. */ 113 | public double getI() { 114 | return Math.max(mI.doubleValue(), 0.0); 115 | } 116 | 117 | /** @return the P value being used by the PID controller. */ 118 | public double getD() { 119 | return Math.max(mD.doubleValue(), 0.0); 120 | } 121 | 122 | /** 123 | * @param p new p value used by the PID controller. 124 | * @return reference to PIDController (so you can chain the commands together) 125 | */ 126 | public AnglePIDController setP(Number p) { 127 | mP = SmartNumber.setNumber(mP, p); 128 | return this; 129 | } 130 | 131 | /** 132 | * @param i new i value used by the PID controller. 133 | * @return reference to PIDController (so you can chain the commands together) 134 | */ 135 | public AnglePIDController setI(Number i) { 136 | mI = SmartNumber.setNumber(mI, i); 137 | return this; 138 | } 139 | 140 | /** 141 | * @param d new d value used by the PID controller. 142 | * @return reference to PIDController (so you can chain the commands together) 143 | */ 144 | public AnglePIDController setD(Number d) { 145 | mD = SmartNumber.setNumber(mD, d); 146 | return this; 147 | } 148 | 149 | /** 150 | * @param p new p value used by the PID controller. 151 | * @param i new i value used by the PID controller. 152 | * @param d new d value used by the PID controller. 153 | * @return reference to PIDController (so you can chain the commands together) 154 | */ 155 | public AnglePIDController setPID(Number p, Number i, Number d) { 156 | return setP(p).setI(i).setD(d); 157 | } 158 | 159 | /** 160 | * @param pidValues PIDController that stores the PID values 161 | * @return reference to PIDController (so you can chain the commands together) 162 | */ 163 | public AnglePIDController setPID(AnglePIDController pidValues) { 164 | return setPID(pidValues.getP(), pidValues.getI(), pidValues.getD()); 165 | } 166 | 167 | /** 168 | * Add constraints to the integral of the PID Controller 169 | * 170 | * @param range the range of error in which the integral is allowed to accumulate (0 will 171 | * disable) 172 | * @param limit the max / min the integral is allowed to accumulate to (0 will disables) 173 | * @return reference to PIDController (so you can chain the commands together) 174 | */ 175 | public AnglePIDController setIntegratorFilter(Number range, Number limit) { 176 | mIFilter = 177 | new IFilterGroup( 178 | x -> range.doubleValue() <= 0 || isDoneRadians(range.doubleValue()) ? x : 0, 179 | x -> limit.doubleValue() <= 0 ? x : SLMath.clamp(x, limit.doubleValue())); 180 | return this; 181 | } 182 | 183 | /** 184 | * Add a filter to the error velocity / derivative of the PID controller. 185 | * 186 | * @param derivativeFilter the filter to apply to derivative 187 | * @return reference to PIDController (so you can chain the commands together) 188 | */ 189 | public AnglePIDController setDerivativeFilter(IFilter... derivativeFilter) { 190 | mDFilter = IFilter.create(derivativeFilter); 191 | return this; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/control/angle/feedforward/AngleArmFeedforward.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.control.angle.feedforward; 6 | 7 | import com.stuypulse.stuylib.control.angle.AngleController; 8 | import com.stuypulse.stuylib.math.Angle; 9 | 10 | /** 11 | * A feedforward term to account for gravity for motorized arms that can move continuously (if not 12 | * use `ArmFeedforward`) 13 | * 14 | *

The motor feedforward used in the context of an arm will not account for gravity that is 15 | * acting on the arm. 16 | * 17 | *

Can be paired with MotorFeedforward or other controllers with .add 18 | * 19 | * @author Myles Pasetsky (myles.pasetsky@gmail.com) 20 | */ 21 | public class AngleArmFeedforward extends AngleController { 22 | 23 | /** voltage to hold arm horizontal */ 24 | private final Number kG; 25 | 26 | /** 27 | * Create arm feedforward 28 | * 29 | * @param kG term to hold arm vertical against gravity (volts) 30 | */ 31 | public AngleArmFeedforward(Number kG) { 32 | this.kG = kG; 33 | } 34 | 35 | /** 36 | * Calculates voltage to hold arm at the setpoint angle 37 | * 38 | * @param setpoint setpoint 39 | * @param measurement measurement 40 | * @return kG * cos(setpoint) 41 | */ 42 | @Override 43 | protected double calculate(Angle setpoint, Angle measurement) { 44 | return kG.doubleValue() * setpoint.cos(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/control/angle/feedforward/AnglePositionFeedforwardController.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.control.angle.feedforward; 6 | 7 | import com.stuypulse.stuylib.control.angle.AngleController; 8 | import com.stuypulse.stuylib.control.feedforward.MotorFeedforward; 9 | import com.stuypulse.stuylib.math.Angle; 10 | import com.stuypulse.stuylib.util.AngleVelocity; 11 | 12 | /** 13 | * A positional feedforward controller for angular systems. 14 | * 15 | * @see com.stuypulse.stuylib.control.feedforward.PositionFeedforwardController 16 | * @author Myles Pasetsky (myles.pasetsky@gmail.com) 17 | */ 18 | public class AnglePositionFeedforwardController extends AngleController { 19 | 20 | /** the feedforward model */ 21 | private final MotorFeedforward mFeedforward; 22 | 23 | /** find the derivative of angular setpoints */ 24 | private final AngleVelocity mDerivative; 25 | 26 | /** 27 | * Create an angle position feedforward controller 28 | * 29 | * @param feedforward model 30 | */ 31 | public AnglePositionFeedforwardController(MotorFeedforward feedforward) { 32 | mFeedforward = feedforward; 33 | mDerivative = new AngleVelocity(); 34 | } 35 | 36 | /** 37 | * Calculates a motor output by feeding the derivative of a positional setpoint to a feedforward 38 | * model 39 | * 40 | * @param setpoint angular positional setpoint 41 | * @param measurement angular position measurement, which is not used by the feedforward model 42 | * @return motor output from feedforward model 43 | */ 44 | @Override 45 | protected double calculate(Angle setpoint, Angle measurement) { 46 | return mFeedforward.calculate(mDerivative.get(setpoint)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/control/feedback/PIDController.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.control.feedback; 6 | 7 | import com.stuypulse.stuylib.control.Controller; 8 | import com.stuypulse.stuylib.math.SLMath; 9 | import com.stuypulse.stuylib.network.SmartNumber; 10 | import com.stuypulse.stuylib.streams.numbers.filters.IFilter; 11 | import com.stuypulse.stuylib.streams.numbers.filters.IFilterGroup; 12 | import com.stuypulse.stuylib.util.StopWatch; 13 | 14 | /** 15 | * The PID algorithm is a feedback control algorithm meant for calculating an output based on an 16 | * error between a measurement and a setpoint. 17 | * 18 | *

PID takes into account the error itself, the rate of change of error, and built-up error over 19 | * time to create a stable controller. 20 | * 21 | *

Because it's a feedback algorithm, it will only react when the system is already behind (there 22 | * must be error for there to be any output). *However, a feedback contrller can be easily combined 23 | * with a feedforward model to reduce the delay.* 24 | * 25 | *

This PID controller is built by extending the Controller class. It has a dynamic rate, so it 26 | * can detect how much time has passed between each update. It is made to be easy to use and simple 27 | * to understand while still being accurate. 28 | * 29 | * @author Sam (sam.belliveau@gmail.com) 30 | */ 31 | public class PIDController extends Controller { 32 | 33 | /** 34 | * Amount of time in between .update() calls that is aloud before the controller resets the 35 | * system 36 | */ 37 | private static final double kMaxTimeBeforeReset = 0.5; 38 | 39 | // Internal timer used by Controller 40 | private StopWatch mTimer; 41 | 42 | // Constants used by the PID controller 43 | private Number mP; 44 | private Number mI; 45 | private Number mD; 46 | 47 | // The Integral of the errors and filter for the I Component 48 | private double mIntegral; 49 | private IFilter mIFilter; 50 | 51 | // The Derivative of the error and the filter for the D Component 52 | private double mLastError; 53 | private IFilter mDFilter; 54 | 55 | /** 56 | * @param p The Proportional Multiplier 57 | * @param i The Integral Multiplier 58 | * @param d The Derivative Multiplier 59 | */ 60 | public PIDController(Number p, Number i, Number d) { 61 | mTimer = new StopWatch(); 62 | 63 | setIntegratorFilter(0, 0); 64 | setDerivativeFilter(x -> x); 65 | setPID(p, i, d); 66 | reset(); 67 | } 68 | 69 | /** Creates a blank PIDController that doesn't move */ 70 | public PIDController() { 71 | this(-1, -1, -1); 72 | } 73 | 74 | /** 75 | * Resets the integrator in the PIDController. This automatically gets called if the gap between 76 | * update() commands is too large 77 | */ 78 | public void reset() { 79 | mIntegral = 0; 80 | } 81 | 82 | @Override 83 | protected double calculate(double setpoint, double measurement) { 84 | // Calculate error & time step 85 | double error = setpoint - measurement; 86 | double dt = mTimer.reset(); 87 | 88 | // Calculate P Component 89 | double p_out = error * getP(); 90 | 91 | // Calculate I Component 92 | mIntegral += error * dt; 93 | mIntegral = mIFilter.get(mIntegral); 94 | double i_out = mIntegral * getI(); 95 | 96 | // Calculate D Component 97 | double derivative = mDFilter.get((error - mLastError) / dt); 98 | mLastError = error; 99 | double d_out = derivative * getD(); 100 | 101 | // Check if time passed exceeds reset limit 102 | if (dt < kMaxTimeBeforeReset) { 103 | // Return the calculated result 104 | return p_out + i_out + d_out; 105 | } else { 106 | // If time in system is broken, then reset and return p part 107 | // This is because the P part is not relative to time 108 | reset(); 109 | return p_out; 110 | } 111 | } 112 | 113 | /** @return the P value being used by the PID controller. */ 114 | public double getP() { 115 | return Math.max(mP.doubleValue(), 0.0); 116 | } 117 | 118 | /** @return the P value being used by the PID controller. */ 119 | public double getI() { 120 | return Math.max(mI.doubleValue(), 0.0); 121 | } 122 | 123 | /** @return the P value being used by the PID controller. */ 124 | public double getD() { 125 | return Math.max(mD.doubleValue(), 0.0); 126 | } 127 | 128 | /** 129 | * @param p new p value used by the PID controller. 130 | * @return reference to PIDController (so you can chain the commands together) 131 | */ 132 | public PIDController setP(Number p) { 133 | mP = SmartNumber.setNumber(mP, p); 134 | return this; 135 | } 136 | 137 | /** 138 | * @param i new i value used by the PID controller. 139 | * @return reference to PIDController (so you can chain the commands together) 140 | */ 141 | public PIDController setI(Number i) { 142 | mI = SmartNumber.setNumber(mI, i); 143 | return this; 144 | } 145 | 146 | /** 147 | * @param d new d value used by the PID controller. 148 | * @return reference to PIDController (so you can chain the commands together) 149 | */ 150 | public PIDController setD(Number d) { 151 | mD = SmartNumber.setNumber(mD, d); 152 | return this; 153 | } 154 | 155 | /** 156 | * @param p new p value used by the PID controller. 157 | * @param i new i value used by the PID controller. 158 | * @param d new d value used by the PID controller. 159 | * @return reference to PIDController (so you can chain the commands together) 160 | */ 161 | public PIDController setPID(Number p, Number i, Number d) { 162 | return setP(p).setI(i).setD(d); 163 | } 164 | 165 | /** 166 | * @param pidValues PIDController that stores the PID values 167 | * @return reference to PIDController (so you can chain the commands together) 168 | */ 169 | public PIDController setPID(PIDController pidValues) { 170 | return setPID(pidValues.getP(), pidValues.getI(), pidValues.getD()); 171 | } 172 | 173 | /** 174 | * Add constraints to the integral of the PID Controller 175 | * 176 | * @param range the range of error in which the integral is allowed to accumulate (0 will 177 | * disable) 178 | * @param limit the max / min the integral is allowed to accumulate to (0 will disables) 179 | * @return reference to PIDController (so you can chain the commands together) 180 | */ 181 | public PIDController setIntegratorFilter(Number range, Number limit) { 182 | mIFilter = 183 | new IFilterGroup( 184 | x -> range.doubleValue() <= 0 || isDone(range.doubleValue()) ? x : 0, 185 | x -> limit.doubleValue() <= 0 ? x : SLMath.clamp(x, limit.doubleValue())); 186 | return this; 187 | } 188 | 189 | /** 190 | * Add a filter to the error velocity / derivative of the PID controller. 191 | * 192 | * @param derivativeFilter the filter to apply to derivative 193 | * @return reference to PIDController (so you can chain the commands together) 194 | */ 195 | public PIDController setDerivativeFilter(IFilter... derivativeFilter) { 196 | mDFilter = IFilter.create(derivativeFilter); 197 | return this; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/control/feedforward/ArmFeedforward.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.control.feedforward; 6 | 7 | import com.stuypulse.stuylib.control.Controller; 8 | import com.stuypulse.stuylib.streams.numbers.filters.IFilter; 9 | 10 | /** 11 | * A feedforward term to account for gravity for motorized arms. 12 | * 13 | *

The motor feedforward used in the context of an arm will not account for gravity that is 14 | * acting on the arm. 15 | * 16 | *

Can be paired with MotorFeedforward or other controllers with .add 17 | * 18 | * @author Myles Pasetsky (myles.pasetsky@gmail.com) 19 | */ 20 | public class ArmFeedforward extends Controller { 21 | 22 | /** voltage to hold arm horizontal */ 23 | private final Number kG; 24 | 25 | /** 26 | * function to configure units of cosine (or even use sin if angles are measured differently) 27 | */ 28 | private final IFilter cosine; 29 | 30 | /** 31 | * Create arm feedforward 32 | * 33 | * @param kG term to hold arm vertical against gravity (volts) 34 | */ 35 | public ArmFeedforward(Number kG) { 36 | this(kG, x -> Math.cos(Math.toRadians(x))); 37 | } 38 | 39 | /** 40 | * Create arm feedforward 41 | * 42 | * @param kG term to hold arm vertical against gravity (volts) 43 | * @param cosine function to calculate cosine of setpoint 44 | */ 45 | public ArmFeedforward(Number kG, IFilter cosine) { 46 | this.kG = kG; 47 | this.cosine = cosine; 48 | } 49 | 50 | /** 51 | * Calculates voltage to hold arm at the setpoint angle 52 | * 53 | * @param setpoint setpoint 54 | * @param measurement measurement 55 | * @return kG * cos(setpoint) 56 | */ 57 | @Override 58 | protected double calculate(double setpoint, double measurement) { 59 | return kG.doubleValue() * cosine.get(setpoint); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/control/feedforward/ElevatorFeedforward.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.control.feedforward; 6 | 7 | import com.stuypulse.stuylib.control.Controller; 8 | 9 | /** 10 | * A feedforward term to account for gravity for motorized lifts. 11 | * 12 | *

The motor feedforward used in the context of a lift will not account for gravity that is 13 | * acting on the lift. 14 | * 15 | *

Can be paired with MotorFeedforward or other controllers with .add 16 | * 17 | * @author Myles Pasetsky (myles.pasetsky@gmail.com) 18 | */ 19 | public class ElevatorFeedforward extends Controller { 20 | 21 | /** voltage to hold arm in place against gravity */ 22 | private final Number kG; 23 | 24 | /** 25 | * Create ElevatorFeedforward 26 | * 27 | * @param kG voltage to hold lift 28 | */ 29 | public ElevatorFeedforward(Number kG) { 30 | this.kG = kG; 31 | } 32 | 33 | /** 34 | * Calculate voltage to hold elevator at setpoint height 35 | * 36 | * @param setpoint setpoint 37 | * @param measurement measurement 38 | * @return kG 39 | */ 40 | @Override 41 | protected double calculate(double setpoint, double measurement) { 42 | return kG.doubleValue(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/control/feedforward/MotorFeedforward.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.control.feedforward; 6 | 7 | import com.stuypulse.stuylib.control.Controller; 8 | import com.stuypulse.stuylib.control.angle.AngleController; 9 | import com.stuypulse.stuylib.control.angle.feedforward.AnglePositionFeedforwardController; 10 | import com.stuypulse.stuylib.streams.numbers.filters.Derivative; 11 | 12 | /** 13 | * A motor feedforward model, which can be used to calculate an output voltage given a desired 14 | * velocity of a motor. 15 | * 16 | *

A feedforward model often uses the dynamics of a system to calculate a voltage for a movement 17 | * that is specified by a desired velocity. 18 | * 19 | *

Feedforward models often take into account acceleration as well, but this is implicitly 20 | * calculated from desired velocities so that only sensible movements can be specified. 21 | * 22 | *

A motor feedforward *model* can be converted into *controllers* through 3 decorator methods: 23 | * `.velocity()`, `.position()`, and `.angle()`. `.velocity()` returns a velocity feedforward 24 | * controller, while `.position()` returns a position feedforward controller, and `.angle()` returns 25 | * an angle feedforward controller. These methods differ on what the controller's setpoint unit is. 26 | * 27 | *

For example, the velocity feedforward controller takes in velocity setpoints, which are 28 | * directly passed to the model. On the other hand, a position feedforward controller takes in 29 | * positional setpoints, which are then differentiated to get velocity and then fed to the 30 | * feedforward controller. An angle feedforward controller is a positional controller that is forced 31 | * to use angular units, so angular velocity is calculated and then fed to the feedforward model. 32 | * 33 | * @author Myles Pasetsky (myles.pasetsky@gmail.com) 34 | */ 35 | public class MotorFeedforward { 36 | 37 | /** The derivative filter that will handle calculating acceleration */ 38 | private final Derivative mDerivative; 39 | 40 | /** The constants for the feedforward equation */ 41 | private final Number kS; 42 | 43 | private final Number kV; 44 | private final Number kA; 45 | 46 | /** 47 | * Create a feedforward model for a motor 48 | * 49 | * @param kS volts, describes portion of voltage to overcome static friction 50 | * @param kV volts * seconds / distance, describes voltage needed to hold constant velocity 51 | * @param kA volts * seconds^2 / distance, describes voltage needed to move at an acceleration 52 | */ 53 | public MotorFeedforward(Number kS, Number kV, Number kA) { 54 | mDerivative = new Derivative(); 55 | 56 | this.kS = kS; 57 | this.kV = kV; 58 | this.kA = kA; 59 | } 60 | 61 | /** 62 | * Creates a controller that uses this feedforward model to calculate a motor output given 63 | * velocity setpoints. 64 | * 65 | * @return a velocity controller for this feedforward model 66 | */ 67 | public final Controller velocity() { 68 | return new VelocityFeedforwardController(this); 69 | } 70 | 71 | /** 72 | * Creates a controller that uses this feedforward model to calculate a motor output given 73 | * positional setpoints. 74 | * 75 | *

NOTE: The derivative of the position setpoints is calculated and then plugged into this 76 | * model. 77 | * 78 | * @return the position controller for this feedforward model 79 | */ 80 | public final Controller position() { 81 | return new PositionFeedforwardController(this); 82 | } 83 | 84 | /** 85 | * Creates a controller that uses this feedforward model to calculate a motor output given angle 86 | * setpoints. 87 | * 88 | *

NOTE: the angular velocity of the angle setpoints is calculated and then plugged into this 89 | * model. 90 | * 91 | * @return the angle controller for this feedforward model 92 | */ 93 | public final AngleController angle() { 94 | return new AnglePositionFeedforwardController(this); 95 | } 96 | 97 | /** 98 | * Calculates a motor output given a desired velocity 99 | * 100 | *

Implicitly determines acceleration by taking the derivative of velocity. 101 | * 102 | * @param velocity desired velocity 103 | * @return motor output, often in volts 104 | */ 105 | public final double calculate(double velocity) { 106 | double acceleration = mDerivative.get(velocity); 107 | 108 | return kS.doubleValue() * Math.signum(velocity) 109 | + kV.doubleValue() * velocity 110 | + kA.doubleValue() * acceleration; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/control/feedforward/PositionFeedforwardController.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.control.feedforward; 6 | 7 | import com.stuypulse.stuylib.control.Controller; 8 | import com.stuypulse.stuylib.streams.numbers.filters.Derivative; 9 | 10 | /** 11 | * A positional controller that uses a feedforward model to calculate motor outputs given positional 12 | * setpoints. 13 | * 14 | *

The feedforward model takes in velocity setpoints, so the derivative of the positional 15 | * setpoints must be calculated. This is done implicitly by this controller though. 16 | * 17 | * @author Myles Pasetsky (myles.pasetsky@gmail.com) 18 | */ 19 | public class PositionFeedforwardController extends Controller { 20 | 21 | /** Filter to implicitly calculate velocity from position setpoints */ 22 | private final Derivative mVelocity; 23 | 24 | /** Internal feedforward model */ 25 | private final MotorFeedforward mFeedforward; 26 | 27 | /** 28 | * Create a position controller with a feedforward model 29 | * 30 | * @param feedforward feedforward model 31 | */ 32 | public PositionFeedforwardController(MotorFeedforward feedforward) { 33 | mVelocity = new Derivative(); 34 | mFeedforward = feedforward; 35 | } 36 | 37 | /** 38 | * Calculates a motor output by feeding the derivative of a positional setpoint to a feedforward 39 | * model 40 | * 41 | * @param setpoint positional setpoint 42 | * @param measurement position measurement, which is not used by the feedforward model 43 | * @return motor output from feedforward model 44 | */ 45 | @Override 46 | protected double calculate(double setpoint, double measurement) { 47 | double velocity = mVelocity.get(setpoint); 48 | return mFeedforward.calculate(velocity); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/control/feedforward/VelocityFeedforwardController.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.control.feedforward; 6 | 7 | import com.stuypulse.stuylib.control.Controller; 8 | 9 | /** 10 | * A velocity controller that uses a feedforward model to calculate motor outputs given velocity 11 | * setpoints. 12 | * 13 | * @author Myles Pasetsky (myles.pasetsky@gmail.com) 14 | */ 15 | public class VelocityFeedforwardController extends Controller { 16 | 17 | /** Internal feedforward model */ 18 | private final MotorFeedforward mFeedforward; 19 | 20 | /** 21 | * Create a velocity controller with a feedforward model 22 | * 23 | * @param feedforward the model 24 | */ 25 | public VelocityFeedforwardController(MotorFeedforward feedforward) { 26 | mFeedforward = feedforward; 27 | } 28 | 29 | /** 30 | * Calculates a motor output by feeding a velocity setpoint to a feedforward model. 31 | * 32 | * @param setpoint velocity setpoint of the controller 33 | * @param measurement measurement, which is not used in calculating an output 34 | * @return motor output from feedforward model 35 | */ 36 | @Override 37 | protected double calculate(double setpoint, double measurement) { 38 | return mFeedforward.calculate(setpoint); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/control/readme.md: -------------------------------------------------------------------------------- 1 | # StuyLib Control Library 2 | 3 | A rudimentary control theory library designed to create controllers using feedback, feedforward, and signal processing algorithms. 4 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/input/WPIGamepad.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.input; 6 | 7 | import edu.wpi.first.wpilibj.Joystick; 8 | import edu.wpi.first.wpilibj2.command.button.Trigger; 9 | 10 | /** 11 | * WPI Gamepad extends Gamepad and adds functions that makes interacting with the underlying 12 | * Joystick class easy. The WPI joystick class is kept in a separate class to make it easy to update 13 | * if WPI updates. 14 | * 15 | *

If you would like to get a button id that is not defined, or an axis id that is not defined, 16 | * use {@link #getRawButton(int)} or {@link #getRawAxis(int)}. If you want it to return a button for 17 | * an unimplemented button, type {@link #getButton(int)}. 18 | * 19 | *

To initialize this class, pass in a Joystick or an int set to the port number. This will be 20 | * the Joystick that the Gamepad class will interact with. 21 | * 22 | *

If you do not initialize with a Joystick, everything will still work except for - {@link 23 | * #getRawButton(int)} - {@link #getRawAxis(int)} - {@link #getButton(int)} which will return either 24 | * false or 0.0 25 | * 26 | *

The difference between the implementations of the Gamepad class is how it interacts with the 27 | * underlying Joystick class. 28 | * 29 | * @author Sam (sam.belliveau@gmail.com) 30 | */ 31 | public class WPIGamepad extends Gamepad { 32 | 33 | /** Underlying Joystick Class */ 34 | private Joystick mJoy; 35 | 36 | /*******************/ 37 | /*** CONSTRUCTOR ***/ 38 | /*******************/ 39 | 40 | /** @param joystick WPI Joystick that will be stored in this class */ 41 | public WPIGamepad(Joystick joystick) { 42 | this.mJoy = joystick; 43 | } 44 | 45 | /** @param port The port that the gamepad is plugged into */ 46 | public WPIGamepad(int port) { 47 | this(new Joystick(port)); 48 | } 49 | 50 | /**********************/ 51 | /*** JOYSTICK STUFF ***/ 52 | /**********************/ 53 | 54 | @Override 55 | public String getGamepadName() { 56 | return "WPIGamepad"; 57 | } 58 | 59 | /** @return if Gamepad has a Joystick */ 60 | public final boolean hasJoystick() { 61 | return getJoystick() != null; 62 | } 63 | 64 | /** @return Underlying joystick */ 65 | public final Joystick getJoystick() { 66 | return this.mJoy; 67 | } 68 | 69 | /** 70 | * @param button Joystick button id 71 | * @return The value of the button 72 | */ 73 | public final boolean getRawButton(int button) { 74 | if (!hasJoystick()) { 75 | return false; 76 | } 77 | return getJoystick().getRawButton(button); 78 | } 79 | 80 | /** 81 | * @param button Joystick button id 82 | * @return Trigger that activates with {@link #getRawButton(int)} 83 | */ 84 | public final Trigger getButton(int button) { 85 | return new Trigger(() -> getRawButton(button)); 86 | } 87 | 88 | /** 89 | * @param axis Joystick axis id 90 | * @return The value of the axis 91 | */ 92 | public final double getRawAxis(int axis) { 93 | if (!hasJoystick()) { 94 | return 0.0; 95 | } 96 | return getJoystick().getRawAxis(axis); 97 | } 98 | 99 | /** @param intensity amount to make the gamepad rumble */ 100 | public final void setRumble(double intensity) { 101 | getJoystick().setRumble(Joystick.RumbleType.kLeftRumble, intensity); 102 | getJoystick().setRumble(Joystick.RumbleType.kRightRumble, intensity); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/input/gamepads/AutoGamepad.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.input.gamepads; 6 | 7 | import com.stuypulse.stuylib.input.Gamepad; 8 | import com.stuypulse.stuylib.math.SLMath; 9 | import com.stuypulse.stuylib.util.StopWatch; 10 | 11 | import edu.wpi.first.wpilibj.GenericHID.HIDType; 12 | import edu.wpi.first.wpilibj.Joystick; 13 | 14 | /** 15 | * This class was created as a saftey measure to prevent issues that arise when the wrong gamepad is 16 | * plugged in. After the robot smashed into a wall because we plugged in a PS4 controller when the 17 | * code expected a Logitech controller, it was deemed that we needed this class. This class will 18 | * ONLY RETURN SAFE VALUES, it will never return negatives or unusual values. This cannot be said 19 | * about other gamepad classes. 20 | * 21 | *

The type of controllers supported are {@link Logitech.DMode}, {@link Logitech.XMode}, {@link 22 | * PS4}, and {@link Xbox}. If none of these gamepads are detected, the code will default to a 23 | * gamepad that does not do anything. 24 | * 25 | * @author Sam (sam.belliveau@gmail.com) 26 | */ 27 | public final class AutoGamepad extends Gamepad { 28 | 29 | // The amount of time that the gamepad will go before checking the type 30 | private static final double MIN_CONTROLLER_CHECK = 4; 31 | 32 | // All of the Different Gamepad classes that AutoGamepad Supports 33 | // Logitech XMode is not here as it is the same as an Xbox Controller 34 | private final int port; 35 | private final Joystick mJoystick; 36 | private final Gamepad mLogitech; 37 | private final Gamepad mPS4; 38 | private final Gamepad mXbox; 39 | private final Gamepad mNull; 40 | 41 | // The gamepad that will be used, and Stopwatch to track updating it 42 | private StopWatch mTimer; 43 | private Gamepad mCurrent; 44 | 45 | /** @param port the port that the gamepad should read from */ 46 | public AutoGamepad(int port) { 47 | this.port = port; 48 | mJoystick = new Joystick(this.port); 49 | mLogitech = new Logitech.DMode(mJoystick); 50 | mPS4 = new PS4(mJoystick); 51 | mXbox = new Xbox(port); 52 | mNull = new Gamepad(); 53 | 54 | mTimer = new StopWatch(); 55 | mCurrent = mNull; 56 | } 57 | 58 | /** @return the correct type of gamepad coming from driverstation directly */ 59 | private Gamepad forceDetectGamepad() { 60 | // Check if joystick is connected 61 | if (!mJoystick.isConnected()) return mNull; 62 | 63 | // Get Type of Joystick 64 | HIDType type = mJoystick.getType(); 65 | 66 | // Return the right gamepad based on the gamepad type 67 | if (type == null) return mNull; 68 | else { 69 | switch (type) { 70 | case kXInputGamepad: 71 | return mXbox; 72 | case kHIDJoystick: 73 | return mLogitech; 74 | case kHIDGamepad: 75 | return mPS4; 76 | default: 77 | return mNull; 78 | } 79 | } 80 | } 81 | 82 | /** @return the internal gamepad class that this gamepad will be reading from */ 83 | public Gamepad getDetectedGamepad() { 84 | if (MIN_CONTROLLER_CHECK < mTimer.getTime()) { 85 | mTimer.reset(); 86 | mCurrent = forceDetectGamepad(); 87 | } 88 | 89 | return mCurrent; 90 | } 91 | 92 | /***************************/ 93 | /*** Gamepad Passthrough ***/ 94 | /***************************/ 95 | 96 | // Name // 97 | @Override 98 | public String getGamepadName() { 99 | return "AutoGamepad (" + getDetectedGamepad().getGamepadName() + ")"; 100 | } 101 | 102 | // Left Stick // 103 | public double getLeftX() { 104 | return SLMath.clamp(getDetectedGamepad().getLeftX(), -1.0, 1.0); 105 | } 106 | 107 | public double getLeftY() { 108 | return SLMath.clamp(getDetectedGamepad().getLeftY(), -1.0, 1.0); 109 | } 110 | 111 | // Right Stick // 112 | public double getRightX() { 113 | return SLMath.clamp(getDetectedGamepad().getRightX(), -1.0, 1.0); 114 | } 115 | 116 | public double getRightY() { 117 | return SLMath.clamp(getDetectedGamepad().getRightY(), -1.0, 1.0); 118 | } 119 | 120 | // D-Pad // 121 | public boolean getRawDPadUp() { 122 | return getDetectedGamepad().getRawDPadUp(); 123 | } 124 | 125 | public boolean getRawDPadDown() { 126 | return getDetectedGamepad().getRawDPadDown(); 127 | } 128 | 129 | public boolean getRawDPadLeft() { 130 | return getDetectedGamepad().getRawDPadLeft(); 131 | } 132 | 133 | public boolean getRawDPadRight() { 134 | return getDetectedGamepad().getRawDPadRight(); 135 | } 136 | 137 | // Bumpers // 138 | public boolean getRawLeftBumper() { 139 | return getDetectedGamepad().getRawLeftBumper(); 140 | } 141 | 142 | public boolean getRawRightBumper() { 143 | return getDetectedGamepad().getRawRightBumper(); 144 | } 145 | 146 | // Triggers // 147 | public double getLeftTrigger() { 148 | return SLMath.clamp(getDetectedGamepad().getLeftTrigger(), 0.0, 1.0); 149 | } 150 | 151 | public double getRightTrigger() { 152 | return SLMath.clamp(getDetectedGamepad().getRightTrigger(), 0.0, 1.0); 153 | } 154 | 155 | // Face Buttons // 156 | public boolean getRawLeftButton() { 157 | return getDetectedGamepad().getRawLeftButton(); 158 | } 159 | 160 | public boolean getRawBottomButton() { 161 | return getDetectedGamepad().getRawBottomButton(); 162 | } 163 | 164 | public boolean getRawRightButton() { 165 | return getDetectedGamepad().getRawRightButton(); 166 | } 167 | 168 | public boolean getRawTopButton() { 169 | return getDetectedGamepad().getRawTopButton(); 170 | } 171 | 172 | // Left Menu / Right Menu // 173 | public boolean getRawLeftMenuButton() { 174 | return getDetectedGamepad().getRawLeftMenuButton(); 175 | } 176 | 177 | public boolean getRawRightMenuButton() { 178 | return getDetectedGamepad().getRawRightMenuButton(); 179 | } 180 | 181 | // Analog Stick Buttons // 182 | public boolean getRawLeftStickButton() { 183 | return getDetectedGamepad().getRawLeftStickButton(); 184 | } 185 | 186 | public boolean getRawRightStickButton() { 187 | return getDetectedGamepad().getRawRightStickButton(); 188 | } 189 | 190 | // Rumble // 191 | public void setRumble(double intensity) { 192 | getDetectedGamepad().setRumble(intensity); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/input/gamepads/Logitech.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.input.gamepads; 6 | 7 | import com.stuypulse.stuylib.input.WPIGamepad; 8 | 9 | import edu.wpi.first.wpilibj.Joystick; 10 | import edu.wpi.first.wpilibj.XboxController; 11 | 12 | /** 13 | * Implementation of Logitech Controller and its 2 Modes for the Gamepad Class 14 | * 15 | * @author Sam (sam.belliveau@gmail.com) 16 | */ 17 | public class Logitech extends WPIGamepad { 18 | 19 | // Constructor // 20 | protected Logitech(int port) { 21 | super(port); 22 | } 23 | 24 | protected Logitech(Joystick joystick) { 25 | super(joystick); 26 | } 27 | 28 | // D-Pad // 29 | @Override 30 | public boolean getRawDPadUp() { 31 | return getJoystick().getPOV() == 0; 32 | } 33 | 34 | @Override 35 | public boolean getRawDPadDown() { 36 | return getJoystick().getPOV() == 180; 37 | } 38 | 39 | @Override 40 | public boolean getRawDPadLeft() { 41 | return getJoystick().getPOV() == 270; 42 | } 43 | 44 | @Override 45 | public boolean getRawDPadRight() { 46 | return getJoystick().getPOV() == 90; 47 | } 48 | 49 | /** 50 | * There is a switch on the back of the controller this is for when the switch is in the D 51 | * position 52 | */ 53 | public static class DMode extends Logitech { 54 | 55 | // Constructor // 56 | public DMode(int port) { 57 | super(port); 58 | } 59 | 60 | public DMode(Joystick joystick) { 61 | super(joystick); 62 | } 63 | 64 | // Name // 65 | @Override 66 | public String getGamepadName() { 67 | return "Logitech DMode"; 68 | } 69 | 70 | // Left Stick // 71 | @Override 72 | public double getLeftX() { 73 | return getRawAxis(0); 74 | } 75 | 76 | @Override 77 | public double getLeftY() { 78 | return -getRawAxis(1); 79 | } 80 | 81 | // Right Stick // 82 | @Override 83 | public double getRightX() { 84 | return getRawAxis(2); 85 | } 86 | 87 | @Override 88 | public double getRightY() { 89 | return -getRawAxis(3); 90 | } 91 | 92 | // Bumpers // 93 | @Override 94 | public boolean getRawLeftBumper() { 95 | return getRawButton(5); 96 | } 97 | 98 | @Override 99 | public boolean getRawRightBumper() { 100 | return getRawButton(6); 101 | } 102 | 103 | // Triggers // 104 | @Override 105 | public double getLeftTrigger() { 106 | return getRawButton(7) ? 1.0 : 0.0; 107 | } 108 | 109 | @Override 110 | public double getRightTrigger() { 111 | return getRawButton(8) ? 1.0 : 0.0; 112 | } 113 | 114 | // Face Buttons // 115 | @Override 116 | public boolean getRawLeftButton() { 117 | return getRawButton(1); 118 | } 119 | 120 | @Override 121 | public boolean getRawBottomButton() { 122 | return getRawButton(2); 123 | } 124 | 125 | @Override 126 | public boolean getRawRightButton() { 127 | return getRawButton(3); 128 | } 129 | 130 | @Override 131 | public boolean getRawTopButton() { 132 | return getRawButton(4); 133 | } 134 | 135 | // Left Menu / Right Menu // 136 | @Override 137 | public boolean getRawLeftMenuButton() { 138 | return getRawButton(9); 139 | } 140 | 141 | @Override 142 | public boolean getRawRightMenuButton() { 143 | return getRawButton(10); 144 | } 145 | 146 | // Analog Stick Buttons // 147 | @Override 148 | public boolean getRawLeftStickButton() { 149 | return getRawButton(11); 150 | } 151 | 152 | @Override 153 | public boolean getRawRightStickButton() { 154 | return getRawButton(12); 155 | } 156 | } 157 | 158 | /** 159 | * There is a switch on the back of the controller this is for when the switch is in the X 160 | * position. 161 | * 162 | *

Because of the use of XInput, this is equivilant to using {@link Xbox}. However if you are 163 | * using a logitech controller on XMode, use this class for readability. 164 | */ 165 | public static class XMode extends Xbox { 166 | 167 | // Constructor // 168 | public XMode(int port) { 169 | super(port); 170 | } 171 | 172 | public XMode(XboxController joystick) { 173 | super(joystick); 174 | } 175 | 176 | // Name // 177 | @Override 178 | public String getGamepadName() { 179 | return "Logitech XMode"; 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/input/gamepads/PS4.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.input.gamepads; 6 | 7 | import com.stuypulse.stuylib.input.WPIGamepad; 8 | 9 | import edu.wpi.first.wpilibj.Joystick; 10 | 11 | /** 12 | * Implementation of the PS4 for the {@link com.stuypulse.stuylib.input.Gamepad} Class 13 | * 14 | * @author Sam (sam.belliveau@gmail.com) 15 | */ 16 | public class PS4 extends WPIGamepad { 17 | 18 | // Constructor // 19 | public PS4(int port) { 20 | super(port); 21 | } 22 | 23 | public PS4(Joystick joystick) { 24 | super(joystick); 25 | } 26 | 27 | // Name // 28 | @Override 29 | public String getGamepadName() { 30 | return "Playstation Controller"; 31 | } 32 | 33 | // Left Stick // 34 | @Override 35 | public double getLeftX() { 36 | return getRawAxis(0); 37 | } 38 | 39 | @Override 40 | public double getLeftY() { 41 | return -getRawAxis(1); 42 | } 43 | 44 | // Right Stick // 45 | @Override 46 | public double getRightX() { 47 | return getRawAxis(2); 48 | } 49 | 50 | @Override 51 | public double getRightY() { 52 | return -getRawAxis(5); 53 | } 54 | 55 | // D-Pad // 56 | @Override 57 | public boolean getRawDPadUp() { 58 | return getJoystick().getPOV() == 0; 59 | } 60 | 61 | @Override 62 | public boolean getRawDPadDown() { 63 | return getJoystick().getPOV() == 180; 64 | } 65 | 66 | @Override 67 | public boolean getRawDPadLeft() { 68 | return getJoystick().getPOV() == 270; 69 | } 70 | 71 | @Override 72 | public boolean getRawDPadRight() { 73 | return getJoystick().getPOV() == 90; 74 | } 75 | 76 | // Bumpers // 77 | @Override 78 | public boolean getRawLeftBumper() { 79 | return getRawButton(5); 80 | } 81 | 82 | @Override 83 | public boolean getRawRightBumper() { 84 | return getRawButton(6); 85 | } 86 | 87 | // Triggers // 88 | @Override 89 | public double getLeftTrigger() { 90 | return (getRawAxis(3) + 1.0) / 2.0; 91 | } 92 | 93 | @Override 94 | public double getRightTrigger() { 95 | return (getRawAxis(4) + 1.0) / 2.0; 96 | } 97 | 98 | // Face Buttons // 99 | @Override 100 | public boolean getRawLeftButton() { 101 | return getRawButton(1); 102 | } 103 | 104 | @Override 105 | public boolean getRawBottomButton() { 106 | return getRawButton(2); 107 | } 108 | 109 | @Override 110 | public boolean getRawRightButton() { 111 | return getRawButton(3); 112 | } 113 | 114 | @Override 115 | public boolean getRawTopButton() { 116 | return getRawButton(4); 117 | } 118 | 119 | // Left Menu / Right Menu // 120 | @Override 121 | public boolean getRawLeftMenuButton() { 122 | return getRawButton(9); 123 | } 124 | 125 | @Override 126 | public boolean getRawRightMenuButton() { 127 | return getRawButton(10); 128 | } 129 | 130 | // Analog Stick Buttons // 131 | @Override 132 | public boolean getRawLeftStickButton() { 133 | return getRawButton(11); 134 | } 135 | 136 | @Override 137 | public boolean getRawRightStickButton() { 138 | return getRawButton(12); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/input/gamepads/Xbox.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.input.gamepads; 6 | 7 | import com.stuypulse.stuylib.input.Gamepad; 8 | 9 | import edu.wpi.first.wpilibj.GenericHID.RumbleType; 10 | import edu.wpi.first.wpilibj.XboxController; 11 | 12 | /** 13 | * A wrapper for the XboxController class to work with the {@link Gamepad} interface. 14 | * 15 | * @author Sam (sam.belliveau@gmail.com) 16 | */ 17 | public class Xbox extends Gamepad { 18 | 19 | private XboxController mJoystick; 20 | 21 | private final boolean flipped; 22 | 23 | // Constructor // 24 | public Xbox(XboxController joystick) { 25 | this(joystick, true); 26 | } 27 | 28 | protected Xbox(XboxController joystick, boolean flipped) { 29 | mJoystick = joystick; 30 | 31 | this.flipped = flipped; 32 | } 33 | 34 | public Xbox(int port) { 35 | this(new XboxController(port)); 36 | } 37 | 38 | // Get the underlying XboxClass 39 | public XboxController getJoystick() { 40 | return mJoystick; 41 | } 42 | 43 | // Name // 44 | @Override 45 | public String getGamepadName() { 46 | return "Xbox Controller"; 47 | } 48 | 49 | // Left Stick // 50 | @Override 51 | public double getLeftX() { 52 | return getJoystick().getLeftX(); 53 | } 54 | 55 | @Override 56 | public double getLeftY() { 57 | return (flipped ? -1 : +1) * getJoystick().getLeftY(); 58 | } 59 | 60 | // Right Stick // 61 | @Override 62 | public double getRightX() { 63 | return getJoystick().getRightX(); 64 | } 65 | 66 | @Override 67 | public double getRightY() { 68 | return (flipped ? -1 : +1) * getJoystick().getRightY(); 69 | } 70 | 71 | // D-Pad // 72 | @Override 73 | public boolean getRawDPadUp() { 74 | return getJoystick().getPOV() == 0; 75 | } 76 | 77 | @Override 78 | public boolean getRawDPadDown() { 79 | return getJoystick().getPOV() == 180; 80 | } 81 | 82 | @Override 83 | public boolean getRawDPadLeft() { 84 | return getJoystick().getPOV() == 270; 85 | } 86 | 87 | @Override 88 | public boolean getRawDPadRight() { 89 | return getJoystick().getPOV() == 90; 90 | } 91 | 92 | // Bumpers // 93 | @Override 94 | public boolean getRawLeftBumper() { 95 | return getJoystick().getLeftBumper(); 96 | } 97 | 98 | @Override 99 | public boolean getRawRightBumper() { 100 | return getJoystick().getRightBumper(); 101 | } 102 | 103 | // Triggers // 104 | @Override 105 | public double getLeftTrigger() { 106 | return getJoystick().getLeftTriggerAxis(); 107 | } 108 | 109 | @Override 110 | public double getRightTrigger() { 111 | return getJoystick().getRightTriggerAxis(); 112 | } 113 | 114 | // Face Buttons // 115 | @Override 116 | public boolean getRawLeftButton() { 117 | return getJoystick().getXButton(); 118 | } 119 | 120 | @Override 121 | public boolean getRawBottomButton() { 122 | return getJoystick().getAButton(); 123 | } 124 | 125 | @Override 126 | public boolean getRawRightButton() { 127 | return getJoystick().getBButton(); 128 | } 129 | 130 | @Override 131 | public boolean getRawTopButton() { 132 | return getJoystick().getYButton(); 133 | } 134 | 135 | // Left Menu / Right Menu // 136 | @Override 137 | public boolean getRawLeftMenuButton() { 138 | return getJoystick().getBackButton(); 139 | } 140 | 141 | @Override 142 | public boolean getRawRightMenuButton() { 143 | return getJoystick().getStartButton(); 144 | } 145 | 146 | // Analog Stick Buttons // 147 | @Override 148 | public boolean getRawLeftStickButton() { 149 | return getJoystick().getLeftStickButton(); 150 | } 151 | 152 | @Override 153 | public boolean getRawRightStickButton() { 154 | return getJoystick().getRightStickButton(); 155 | } 156 | 157 | // Rumble 158 | @Override 159 | public void setRumble(double intensity) { 160 | mJoystick.setRumble(RumbleType.kLeftRumble, intensity); 161 | mJoystick.setRumble(RumbleType.kRightRumble, intensity); 162 | } 163 | 164 | public Xbox flipped() { 165 | return new Xbox(mJoystick, !flipped); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/input/gamepads/readme.md: -------------------------------------------------------------------------------- 1 | # StuyLib Gamepad Implementations 2 | 3 | WIP... -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/input/readme.md: -------------------------------------------------------------------------------- 1 | # StuyLib Gamepad Library 2 | 3 | WIP... -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/math/SLMath.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.math; 6 | 7 | /** 8 | * SLMath (StuyLib Math) is a class containing many algorithms that are useful for developing robot 9 | * code. Algorithms include limit, deadband, raising to powers while keeping the sign, and some 10 | * other new algorithms we came up with. 11 | * 12 | * @author Sam (sam.belliveau@gmail.com) 13 | */ 14 | public final class SLMath { 15 | 16 | // Prevent the class from being extended at all 17 | private SLMath() {} 18 | 19 | /**************/ 20 | /*** LIMITS ***/ 21 | /**************/ 22 | 23 | /** 24 | * clamp input from max to min 25 | * 26 | * @param x input 27 | * @param min min value for x 28 | * @param max max value for x 29 | * @return clamp input 30 | */ 31 | public static double clamp(double x, double min, double max) { 32 | if (min < max) { 33 | if (x > max) return max; 34 | if (x < min) return min; 35 | } else { 36 | if (x > min) return min; 37 | if (x < max) return max; 38 | } 39 | return x; 40 | } 41 | 42 | /** 43 | * clamp input from max to -max 44 | * 45 | * @param x input 46 | * @param max max and min value 47 | * @return clamped input 48 | */ 49 | public static double clamp(double x, double max) { 50 | return clamp(x, -max, max); 51 | } 52 | 53 | /** 54 | * clamp input from -1 to 1 55 | * 56 | * @param x input 57 | * @return clamped input 58 | */ 59 | public static double clamp(double x) { 60 | return clamp(x, 1.0); 61 | } 62 | 63 | /**************************/ 64 | /*** DEADBAND ALGORITHM ***/ 65 | /**************************/ 66 | 67 | /** 68 | * Dead bands x value with window being the dead band. all values for this are [-1.0...1.0] 69 | * 70 | * @param x value 71 | * @param window deadband window 72 | * @return deadbanded value 73 | */ 74 | public static double deadband(double x, double window) { 75 | window = Math.abs(window); 76 | 77 | if (Math.abs(x) < window) { 78 | return 0.0; 79 | } else { 80 | return (x - Math.copySign(window, x)) / (1.0 - window); 81 | } 82 | } 83 | 84 | /*****************************************/ 85 | /*** RAISE TO POWER WHILE KEEPING SIGN ***/ 86 | /*****************************************/ 87 | 88 | /** 89 | * spow (signless pow), raises a number to a power without affecting the sign of the number 90 | * 91 | * @param x input 92 | * @param power power to raise x to 93 | * @return input ^ power 94 | */ 95 | public static double spow(double x, double power) { 96 | return Math.pow(Math.abs(x), power) * Math.signum(x); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/math/Vector2D.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.math; 6 | 7 | import edu.wpi.first.math.geometry.Translation2d; 8 | 9 | /** 10 | * A Vector2D class that stores x and y position data. It is made to work with the StuyLib Angle 11 | * class and be easy to use. It is a standard Vector2D class with all of the functions that you 12 | * would expect. 13 | * 14 | * @author Sam (sam.belliveau@gmail.com) 15 | */ 16 | public final class Vector2D { 17 | 18 | // Vector Constnants 19 | public static final Vector2D kOrigin = new Vector2D(0, 0); 20 | public static final Vector2D kI = new Vector2D(1, 0); 21 | public static final Vector2D kJ = new Vector2D(0, 1); 22 | 23 | /****************************/ 24 | /*** Class Implementation ***/ 25 | /****************************/ 26 | 27 | /** The x position of the Vector2D */ 28 | public final double x; 29 | 30 | /** The y position of the Vector2D */ 31 | public final double y; 32 | 33 | /** 34 | * @param x the x axis of the vector 35 | * @param y the y axis of the vector 36 | */ 37 | public Vector2D(double x, double y) { 38 | this.x = x; 39 | this.y = y; 40 | } 41 | 42 | /** 43 | * @param axis Array of size 2 where the first element will be defined as x, and the second will 44 | * be defined as y. 45 | */ 46 | public Vector2D(double[] axis) { 47 | if (axis.length != 2) { 48 | throw new IllegalArgumentException("axis must be of size 2"); 49 | } 50 | 51 | this.x = axis[0]; 52 | this.y = axis[1]; 53 | } 54 | 55 | /** @param translation Translation2d to copy this vector into */ 56 | public Vector2D(Translation2d translation) { 57 | this.x = translation.getX(); 58 | this.y = translation.getY(); 59 | } 60 | 61 | /** @return double array of size 2 defined as {x, y} */ 62 | public double[] getArray() { 63 | return new double[] {x, y}; 64 | } 65 | 66 | /** 67 | * Return the StuyLib Vector2D class in the form as WPILib's Translation2d. 68 | * 69 | *

This function is here in order to make interoperability with WPILib easier so that manual 70 | * conversion isn't needed as much. 71 | * 72 | * @return Translation2d class with the same value as the Vector2d 73 | */ 74 | public Translation2d getTranslation2d() { 75 | return new Translation2d(x, y); 76 | } 77 | 78 | /** 79 | * @param other other Vector2D 80 | * @return the distance between the two Vector2Ds 81 | */ 82 | public double distance(Vector2D other) { 83 | return Math.hypot(other.x - this.x, other.y - this.y); 84 | } 85 | 86 | /** @return distance from 0, 0 */ 87 | public double distance() { 88 | return Math.hypot(this.x, this.y); 89 | } 90 | 91 | /** @return magnitude of the vector (same as distance from 0, 0) */ 92 | public double magnitude() { 93 | return this.distance(); 94 | } 95 | 96 | /** @return the angle of the Vector2D around 0, 0 */ 97 | public Angle getAngle() { 98 | return Angle.fromVector(this); 99 | } 100 | 101 | /** 102 | * @param angle angle to rotate by 103 | * @param origin point to rotate around 104 | * @return result of rotation 105 | */ 106 | public Vector2D rotate(Angle angle, Vector2D origin) { 107 | return new Vector2D( 108 | origin.x + (this.x - origin.x) * angle.cos() - (this.y - origin.y) * angle.sin(), 109 | origin.y + (this.y - origin.y) * angle.cos() + (this.x - origin.x) * angle.sin()); 110 | } 111 | 112 | /** 113 | * @param angle angle to rotate by 114 | * @return result of rotation 115 | */ 116 | public Vector2D rotate(Angle angle) { 117 | return new Vector2D( 118 | this.x * angle.cos() - this.y * angle.sin(), 119 | this.y * angle.cos() + this.x * angle.sin()); 120 | } 121 | 122 | /** 123 | * @param other Vector2D to be added by 124 | * @return sum of the two Vector2Ds 125 | */ 126 | public Vector2D add(Vector2D other) { 127 | return new Vector2D(this.x + other.x, this.y + other.y); 128 | } 129 | 130 | /** 131 | * @param other Vector2D to be subtracted from 132 | * @return difference between the two Vector2Ds 133 | */ 134 | public Vector2D sub(Vector2D other) { 135 | return new Vector2D(this.x - other.x, this.y - other.y); 136 | } 137 | 138 | /** 139 | * @param other Vector2D to be multiplied by 140 | * @return product of the two Vector2Ds 141 | */ 142 | public Vector2D mul(Vector2D other) { 143 | return new Vector2D(this.x * other.x, this.y * other.y); 144 | } 145 | 146 | /** 147 | * @param other Vector2D to be divided by 148 | * @return division of the two Vector2Ds 149 | */ 150 | public Vector2D div(Vector2D other) { 151 | return new Vector2D(this.x / other.x, this.y / other.y); 152 | } 153 | 154 | /** 155 | * @param multiplier amount to multiply the x and y components by 156 | * @return result of multiplying the x and y components by the multiplier 157 | */ 158 | public Vector2D mul(double multiplier) { 159 | return new Vector2D(this.x * multiplier, this.y * multiplier); 160 | } 161 | 162 | /** 163 | * @param divisor amount to divide the x and y components by 164 | * @return result of dividing the x and y components by the divisor 165 | */ 166 | public Vector2D div(double divisor) { 167 | return new Vector2D(this.x / divisor, this.y / divisor); 168 | } 169 | 170 | /** 171 | * @param power power to raise magnitude of vector to 172 | * @return result of raising the magnitude of the vector to the power 173 | */ 174 | public Vector2D pow(double power) { 175 | return this.mul(Math.pow(magnitude(), power - 1)); 176 | } 177 | 178 | /** 179 | * @param other Vector2D to perform dot product with 180 | * @return result of performing the dot product with the other Vector2D 181 | */ 182 | public double dot(Vector2D other) { 183 | return this.x * other.x + this.y * other.y; 184 | } 185 | 186 | /** 187 | * @param other Vector3D to perform cross product with 188 | * @return result of performing the cross product with the other Vector2D 189 | */ 190 | public double cross(Vector2D other) { 191 | return this.x * other.y - this.y * other.x; 192 | } 193 | 194 | /** @return result of normalizing the Vector2D so that the magnitude is 1.0 */ 195 | public Vector2D normalize() { 196 | final double magnitude = this.distance(); 197 | if (magnitude <= 1e-9) { 198 | return Vector2D.kI; 199 | } else { 200 | return this.div(magnitude); 201 | } 202 | } 203 | 204 | /** 205 | * limit the magnitude of a vector to a maximum 206 | * 207 | * @param maxMagnitude max magitude of resulting vector 208 | * @return vector with limited magnitude 209 | */ 210 | public Vector2D clamp(double maxMagnitude) { 211 | if (maxMagnitude <= 0.0) return Vector2D.kOrigin; 212 | 213 | final double magnitude = this.distance(); 214 | 215 | if (maxMagnitude <= magnitude) { 216 | return mul(maxMagnitude / magnitude); 217 | } 218 | 219 | return this; 220 | } 221 | 222 | /** @return result of negating the x and y components */ 223 | public Vector2D negative() { 224 | return new Vector2D(-this.x, -this.y); 225 | } 226 | 227 | /** 228 | * @param other object to compare to 229 | * @return both objects are Vector2Ds and they equal eachother 230 | */ 231 | @Override 232 | public boolean equals(Object other) { 233 | if (this == other) { 234 | return true; 235 | } 236 | 237 | if (other instanceof Vector2D) { 238 | Vector2D o = (Vector2D) other; 239 | return this.x == o.x && this.y == o.y; 240 | } 241 | 242 | return false; 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/math/readme.md: -------------------------------------------------------------------------------- 1 | # StuyLib Math Library 2 | 3 | ## What is the point of these classes 4 | 5 | ### [SLMath](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/math/SLMath.java) 6 | 7 | SLMath is just a class with some static math functions that are useful in robotics. 8 | 9 | For example, when squaring a number, `SLMath.square(x)` will keep the sign of the number. *this is used when filtering controller inputs* `SLMath.pow(x, p)` will also keep the sign of x no matter what the value of x or p. This might not seem useful normally, but is nice to have in FRC. 10 | 11 | Other functions like `SLMath.limit` or `SLMath.deadband` are super helpful in FRC. 12 | 13 | ### [Vector2D](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/math/Vector2D.java) 14 | 15 | This is just a standard Vector2D class that has every feature you would need, and also works with our other classes in Stuylib. It also works nicely with our [Angle](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/math/Angle.java) class, which is a nice bonus. 16 | 17 | ### [Angle](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/math/Vector2D.java) 18 | 19 | This is an angle class that removes the confusion about degrees / radians, and works nicely with our [Vector2D](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/math/Vector2D.java). When doing math with the angle class, you do not need to worry about the value of the angle being outside the range of `-pi < x < pi`. 20 | 21 | ## Summary 22 | 23 | This Math folder in stuylib contains some helpful utilities that make programming FRC code a little bit easier. -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/network/SmartBoolean.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.network; 6 | 7 | import com.stuypulse.stuylib.streams.booleans.BStream; 8 | 9 | import edu.wpi.first.networktables.NetworkTablesJNI; 10 | import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; 11 | 12 | /** 13 | * {@link SmartBoolean} works as a wrapper for values on {@link SmartDashboard}. The idea for this 14 | * class was to make getting values on {@link SmartDashboard} easier by making them variables that 15 | * you know were initialized. 16 | * 17 | * @author Sam (sam.belliveau@gmail.com) 18 | */ 19 | public final class SmartBoolean implements BStream { 20 | 21 | /** The ID / Name for the value on {@link SmartDashboard}. */ 22 | private final int mHandle; 23 | 24 | /** The default value that the {@link SmartDashboard} value was set too. */ 25 | private final boolean mDefaultValue; 26 | 27 | /** 28 | * Creates a {@link SmartBoolean} with the element name and a default value. The value on {@link 29 | * SmartDashboard} will be reset to the default value on initialization. 30 | * 31 | * @param id the name of the boolean on {@link SmartDashboard} 32 | * @param value the default / initialization value for the value 33 | */ 34 | public SmartBoolean(String id, boolean value) { 35 | mHandle = 36 | NetworkTablesJNI.getEntry( 37 | NetworkTablesJNI.getDefaultInstance(), "SmartDashboard/" + id); 38 | mDefaultValue = value; 39 | reset(); 40 | } 41 | 42 | /** @return the value of the boolean from {@link SmartDashboard} */ 43 | public boolean get() { 44 | return NetworkTablesJNI.getBoolean(mHandle, mDefaultValue); 45 | } 46 | 47 | /** @return the default value of the boolean */ 48 | public boolean getDefault() { 49 | return mDefaultValue; 50 | } 51 | 52 | /** @param value what the value on {@link SmartDashboard} will be set to */ 53 | public void set(boolean value) { 54 | NetworkTablesJNI.setBoolean(mHandle, 0, value); 55 | } 56 | 57 | /** Resets the value on {@link SmartDashboard} to the default value */ 58 | public void reset() { 59 | set(getDefault()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/network/SmartNumber.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.network; 6 | 7 | import com.stuypulse.stuylib.streams.numbers.IStream; 8 | 9 | import edu.wpi.first.networktables.NetworkTablesJNI; 10 | import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; 11 | 12 | /** 13 | * SmartNumber works as a wrapper for values on {@link SmartDashboard}. The idea for this class was 14 | * to make getting values on {@link SmartDashboard} easier by making them variables that you know 15 | * were initialized. 16 | * 17 | * @author Sam (sam.belliveau@gmail.com) 18 | */ 19 | public final class SmartNumber extends Number implements IStream { 20 | 21 | private static final long serialVersionUID = 1L; 22 | 23 | /** The ID / Name for the value on {@link SmartDashboard}. */ 24 | private final int mHandle; 25 | 26 | /** The default value that the {@link SmartDashboard} value was set too. */ 27 | private final double mDefaultValue; 28 | 29 | /** 30 | * Creates a SmartNumber with the element name and a default value. The value on {@link 31 | * SmartDashboard} will be reset to the default value on initialization. 32 | * 33 | * @param id the name of the number on SmartDashboard 34 | * @param value the default / initialization value for the value 35 | */ 36 | public SmartNumber(String id, double value) { 37 | mHandle = 38 | NetworkTablesJNI.getEntry( 39 | NetworkTablesJNI.getDefaultInstance(), "SmartDashboard/" + id); 40 | mDefaultValue = value; 41 | reset(); 42 | } 43 | 44 | /** @return the value of the number from SmartDashboard */ 45 | public double get() { 46 | return NetworkTablesJNI.getDouble(mHandle, mDefaultValue); 47 | } 48 | 49 | /** @return the default value of the number */ 50 | public double getDefault() { 51 | return mDefaultValue; 52 | } 53 | 54 | /** @param value what the value on {@link SmartDashboard} will be set to */ 55 | public void set(Number value) { 56 | NetworkTablesJNI.setDouble(mHandle, 0, value.doubleValue()); 57 | } 58 | 59 | /** Resets the value on {@link SmartDashboard} to the default value */ 60 | public void reset() { 61 | set(getDefault()); 62 | } 63 | 64 | /** @return the value of the number from {@link SmartDashboard} (casted to a double) */ 65 | public double doubleValue() { 66 | return (double) this.get(); 67 | } 68 | 69 | /** @return the value of the number from {@link SmartDashboard} (casted to a float) */ 70 | public float floatValue() { 71 | return (float) this.get(); 72 | } 73 | 74 | /** @return the value of the number from {@link SmartDashboard} (casted to a int) */ 75 | public int intValue() { 76 | return (int) this.get(); 77 | } 78 | 79 | /** @return the value of the number from {@link SmartDashboard} (casted to a long) */ 80 | public long longValue() { 81 | return (long) this.get(); 82 | } 83 | 84 | /** 85 | * This function checks to see if {@code oldNumber} is a {@link SmartNumber}, if it is, then it 86 | * sets the {@code oldNumber} to {@code newNumber} and returns the {@code oldNumber}. Otherwise, 87 | * it just returns {@code newNumber}. 88 | * 89 | * @param oldNumber number that is being replaced 90 | * @param newNumber number representing the value of the new number 91 | * @return if {@code oldNumber} is not a {@link SmartNumber}, then this function will return 92 | * {@code newNumber}. If {@code oldNumber} is a {@link SmartNumber}, then this function will 93 | * return {@code oldNumber} with its value set to {@code newNumber} 94 | */ 95 | public static Number setNumber(Number oldNumber, Number newNumber) { 96 | if (oldNumber instanceof SmartNumber) { 97 | SmartNumber oldSmartNumber = (SmartNumber) oldNumber; 98 | oldSmartNumber.set(newNumber.doubleValue()); 99 | return oldSmartNumber; 100 | } else { 101 | return newNumber; 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/network/SmartString.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.network; 6 | 7 | import edu.wpi.first.networktables.NetworkTablesJNI; 8 | import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; 9 | 10 | import java.util.function.Supplier; 11 | 12 | /** 13 | * SmartString works as a wrapper for values on {@link SmartDashboard}. The idea for this class was 14 | * to make getting values on {@link SmartDashboard} easier by making them variables that you know 15 | * were initialized. 16 | * 17 | * @author Sam (sam.belliveau@gmail.com) 18 | */ 19 | public final class SmartString implements Supplier { 20 | 21 | /** The ID / Name for the value on {@link SmartDashboard}. */ 22 | private final int mHandle; 23 | 24 | /** The default value that the {@link SmartDashboard} value was set too. */ 25 | private final String mDefaultValue; 26 | 27 | /** 28 | * Creates a SmartString with the element name and a default value. The value on {@link 29 | * SmartDashboard} will be reset to the default value on initialization. 30 | * 31 | * @param id the name of the String on {@link SmartDashboard} 32 | * @param value the default / initialization value for the value 33 | */ 34 | public SmartString(String id, String value) { 35 | mHandle = 36 | NetworkTablesJNI.getEntry( 37 | NetworkTablesJNI.getDefaultInstance(), "SmartDashboard/" + value); 38 | mDefaultValue = value; 39 | reset(); 40 | } 41 | 42 | /** @return the value of the String from SmartDashboard */ 43 | public String get() { 44 | return NetworkTablesJNI.getString(mHandle, mDefaultValue); 45 | } 46 | 47 | /** @return the default value of the String */ 48 | public String getDefault() { 49 | return mDefaultValue; 50 | } 51 | 52 | /** @param value what the value on {@link SmartDashboard} will be set to */ 53 | public void set(String value) { 54 | NetworkTablesJNI.setString(mHandle, 0, value); 55 | } 56 | 57 | /** Resets the value on {@link SmartDashboard} to the default value */ 58 | public void reset() { 59 | set(getDefault()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/network/readme.md: -------------------------------------------------------------------------------- 1 | # StuyLib Robot Networking Library 2 | 3 | WIP... -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/readme.md: -------------------------------------------------------------------------------- 1 | # StuyLib Source Files 2 | 3 | There will be a readme like this in every folder. 4 | 5 | Currently none of the readme's are filled out, but we totally should. -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/angles/AStick.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.angles; 6 | 7 | import com.stuypulse.stuylib.math.Angle; 8 | import com.stuypulse.stuylib.math.Vector2D; 9 | import com.stuypulse.stuylib.streams.vectors.VStream; 10 | 11 | /** 12 | * When controlling angle with a joystick that has a deadzone filter, the angle will snap to 0 13 | * degrees while in the deadzone. This class prevents the angle snapping and will return the most 14 | * recent angle from outside of the deadzone. 15 | * 16 | * @author Benjamin Goldfisher 17 | */ 18 | public class AStick implements AStream { 19 | 20 | private final VStream mStick; 21 | private final Number mDeadzone; 22 | 23 | private Angle mPrevious; 24 | 25 | public AStick(VStream stick, Number deadzone) { 26 | mStick = stick; 27 | mDeadzone = deadzone; 28 | 29 | mPrevious = Angle.kNull; 30 | } 31 | 32 | @Override 33 | public Angle get() { 34 | final Vector2D stick = mStick.get(); 35 | 36 | if (mDeadzone.doubleValue() < stick.magnitude()) { 37 | mPrevious = stick.getAngle(); 38 | } 39 | 40 | return mPrevious; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/angles/AStream.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.angles; 6 | 7 | import com.stuypulse.stuylib.math.Angle; 8 | import com.stuypulse.stuylib.streams.angles.filters.AFilter; 9 | import com.stuypulse.stuylib.streams.numbers.IStream; 10 | import com.stuypulse.stuylib.streams.vectors.VStream; 11 | 12 | import java.util.function.Supplier; 13 | 14 | public interface AStream extends Supplier { 15 | 16 | public static AStream create(AStream stream) { 17 | return stream; 18 | } 19 | 20 | public static AStream create(IStream stream) { 21 | return () -> Angle.fromRadians(stream.get()); 22 | } 23 | 24 | public static AStream create(VStream stream) { 25 | return () -> stream.get().getAngle(); 26 | } 27 | 28 | /** @return next value in the stream */ 29 | public Angle get(); 30 | 31 | /** 32 | * Create a new FilteredAStream from the current stream 33 | * 34 | * @param filters the filters you want to apply to the AStream 35 | * @return The FilteredAStream 36 | */ 37 | public default FilteredAStream filtered(AFilter... filters) { 38 | return new FilteredAStream(this, filters); 39 | } 40 | 41 | /** 42 | * Create a new PollingAStream from the current stream 43 | * 44 | * @param dt the time inbetween each poll of the AStream 45 | * @return The PollingAStream 46 | */ 47 | public default PollingAStream polling(double dt) { 48 | return new PollingAStream(this, dt); 49 | } 50 | 51 | /** 52 | * Combine two AStreams by adding their results together 53 | * 54 | * @param other other AStream to add to this one 55 | * @return the resulting AStream after the sum 56 | */ 57 | public default AStream add(AStream other) { 58 | return () -> get().add(other.get()); 59 | } 60 | 61 | /** 62 | * Combine two AStreams by subtracting their results together 63 | * 64 | * @param other other AStream to subtract from this one 65 | * @return the resulting AStream after the subtraction 66 | */ 67 | public default AStream sub(AStream other) { 68 | return () -> get().sub(other.get()); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/angles/FilteredAStream.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.angles; 6 | 7 | import com.stuypulse.stuylib.math.Angle; 8 | import com.stuypulse.stuylib.streams.angles.filters.AFilter; 9 | 10 | /** 11 | * Takes an {@link AStream} and a {@link AFilter} and makes a {@link FilteredAStream} 12 | * 13 | *

This could be used to automatically filter controller inputs 14 | * 15 | * @author Sam (sam.belliveau@gmail.com) 16 | */ 17 | public class FilteredAStream implements AStream { 18 | 19 | private AStream mStream; // Stream used 20 | private AFilter mStreamFilter; // StreamFilter used 21 | 22 | /** 23 | * Makes filtered stream from stream and stream filter 24 | * 25 | * @param stream input stream 26 | * @param filter stream filter 27 | */ 28 | public FilteredAStream(AStream stream, AFilter... filter) { 29 | mStream = stream; 30 | mStreamFilter = AFilter.create(filter); 31 | } 32 | 33 | /** 34 | * Get next value from filtered stream 35 | * 36 | * @return next value 37 | */ 38 | public Angle get() { 39 | return mStreamFilter.get(mStream.get()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/angles/PollingAStream.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.angles; 6 | 7 | import com.stuypulse.stuylib.math.Angle; 8 | 9 | import edu.wpi.first.wpilibj.Notifier; 10 | 11 | /** 12 | * A PollingAStream calls {@link AStream#get()} every x milliseconds instead of when the user calls 13 | * get 14 | * 15 | * @author Sam (sam.belliveau@gmail.com) 16 | */ 17 | public class PollingAStream implements AStream, AutoCloseable { 18 | 19 | private Notifier mPoller; 20 | private volatile Angle mResult; 21 | 22 | /** 23 | * Creates a PollingAStream from an AStream and a time value 24 | * 25 | * @param stream astream to poll from 26 | * @param dt Number of calls per second 27 | */ 28 | public PollingAStream(AStream stream, double dt) { 29 | if (dt <= 0) { 30 | throw new IllegalArgumentException("dt must be greater than 0"); 31 | } 32 | 33 | mResult = Angle.kNull; 34 | mPoller = new Notifier(() -> mResult = stream.get()); 35 | mPoller.startPeriodic(dt); 36 | } 37 | 38 | public Angle get() { 39 | return mResult; 40 | } 41 | 42 | protected void finalize() { 43 | close(); 44 | } 45 | 46 | public void close() { 47 | mPoller.close(); 48 | mResult = Angle.kNull; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/angles/filters/AFilter.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.angles.filters; 6 | 7 | import com.stuypulse.stuylib.math.Angle; 8 | 9 | /** 10 | * This is the Filter interface class that gives a definition for how to implement a filter. 11 | * 12 | *

All that a filter does is take in the next double in a series and gives you the filtered 13 | * value. 14 | * 15 | * @author Sam (sam.belliveau@gmail.com) 16 | */ 17 | public interface AFilter { 18 | 19 | /** 20 | * Create an AFilter from a list of AFilters. This will create an AFilter with the least 21 | * possible overhead for the number of filters provided. 22 | * 23 | * @param filters list of AFilters to apply 24 | * @return an AFilter that uses every filter given 25 | */ 26 | public static AFilter create(AFilter... filters) { 27 | switch (filters.length) { 28 | case 0: 29 | return x -> x; 30 | case 1: 31 | return filters[0]; 32 | case 2: 33 | return x -> filters[1].get(filters[0].get(x)); 34 | case 3: 35 | return x -> filters[2].get(filters[1].get(filters[0].get(x))); 36 | case 4: 37 | return x -> filters[3].get(filters[2].get(filters[1].get(filters[0].get(x)))); 38 | default: 39 | return new AFilterGroup(filters); 40 | } 41 | } 42 | 43 | /** 44 | * Get next value in Filter based on the next value given 45 | * 46 | * @param next next input value in the stream 47 | * @return the output value of the filter 48 | */ 49 | public Angle get(Angle next); 50 | 51 | /** 52 | * Combine an AFilter with another AFilter 53 | * 54 | * @param next filter to be evaluated after this one 55 | * @return the combined filter 56 | */ 57 | public default AFilter then(AFilter next) { 58 | return x -> next.get(get(x)); 59 | } 60 | 61 | /** 62 | * Combine two AFilters by adding their results together 63 | * 64 | * @param other other AFilter to add to this one 65 | * @return the resulting AFilter after the sum 66 | */ 67 | public default AFilter add(AFilter other) { 68 | return x -> get(x).add(other.get(x)); 69 | } 70 | 71 | /** 72 | * Combine two AFilters by subtracting their results together 73 | * 74 | * @param other other AFilter to subtract from this one 75 | * @return the resulting AFilter after the subtraction 76 | */ 77 | public default AFilter sub(AFilter other) { 78 | return x -> get(x).sub(other.get(x)); 79 | } 80 | 81 | /** 82 | * Invert an AFilter by subtracting the input from the result of the AFilter. 83 | * 84 | *

Inverting a LowPassFilter gives you a HighPassFilter and vise versa. 85 | * 86 | *

Inverting something twice gives you the original value. 87 | * 88 | * @return the inverted filter 89 | */ 90 | public default AFilter invert() { 91 | return x -> x.sub(get(x)); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/angles/filters/AFilterGroup.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.angles.filters; 6 | 7 | import com.stuypulse.stuylib.math.Angle; 8 | 9 | /** 10 | * A class that lets you combine multiple stream filters into one stream filter 11 | * 12 | * @author Sam (sam.belliveau@gmail.com) 13 | */ 14 | public class AFilterGroup implements AFilter { 15 | 16 | // Array of all the filters 17 | private final AFilter[] mFilters; 18 | 19 | /** 20 | * Make AFilterGroup out of an array of Filters 21 | * 22 | * @param filters list of filters to combine 23 | */ 24 | public AFilterGroup(AFilter... filters) { 25 | mFilters = filters; 26 | } 27 | 28 | public Angle get(Angle next) { 29 | // Put next through each of the filters 30 | for (AFilter filter : mFilters) { 31 | next = filter.get(next); 32 | } 33 | 34 | // Return filtered value 35 | return next; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/angles/filters/AHighPassFilter.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.angles.filters; 6 | 7 | import com.stuypulse.stuylib.math.Angle; 8 | 9 | /** 10 | * Implementation for of a real time IIR HighPassFilter 11 | * 12 | * @author Sam (sam.belliveau@gmail.com) 13 | */ 14 | public class AHighPassFilter implements AFilter { 15 | 16 | private ALowPassFilter mInverse; 17 | 18 | /** @param rc time constant for high pass filter */ 19 | public AHighPassFilter(Number rc) { 20 | mInverse = new ALowPassFilter(rc); 21 | } 22 | 23 | public Angle get(Angle next) { 24 | return next.sub(mInverse.get(next)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/angles/filters/ALowPassFilter.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.angles.filters; 6 | 7 | import com.stuypulse.stuylib.math.Angle; 8 | import com.stuypulse.stuylib.util.StopWatch; 9 | 10 | /** 11 | * Implementation of a real time IIR LowPassFilter 12 | * 13 | * @author Sam (sam.belliveau@gmail.com) 14 | */ 15 | public class ALowPassFilter implements AFilter { 16 | 17 | // Used to calculate time in between calls 18 | private final StopWatch mTimer; 19 | 20 | // Used to calculate next value based on previous value and time 21 | private final Number mRC; 22 | private Angle mLastValue; 23 | 24 | /** 25 | * @param rc Time Constant. The time constant is the amount of time in seconds that it takes to 26 | * get 63.2% of the way to the target value. 63.2% is (1 - (1 / e)). 27 | */ 28 | public ALowPassFilter(Number rc) { 29 | mTimer = new StopWatch(); 30 | mLastValue = Angle.kZero; 31 | mRC = rc; 32 | } 33 | 34 | public Angle get(Angle next) { 35 | // If RC will cause errors, disable filter 36 | if (mRC.doubleValue() <= 0.0) { 37 | return mLastValue = next; 38 | } 39 | 40 | // Get a constant, which is determined based on dt and the mRC constant 41 | double a = Math.exp(-mTimer.reset() / mRC.doubleValue()); 42 | 43 | // Based on the value of a (which is determined by dt), the next value 44 | // could either change a lot, or not by much. (smaller dt = smaller change) 45 | return mLastValue = mLastValue.add(next.sub(mLastValue).mul(1.0 - a)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/angles/filters/AMotionProfile.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.angles.filters; 6 | 7 | import com.stuypulse.stuylib.math.Angle; 8 | import com.stuypulse.stuylib.math.SLMath; 9 | import com.stuypulse.stuylib.util.StopWatch; 10 | 11 | /** 12 | * A filter, that when applied to the input of a motor, will profile it. Similar to the way in which 13 | * motion profiling can limit the amount of acceleration and jerk in an S-Curve, this can do that to 14 | * real time input. Because this will add a delay, it is recommended that the accelLimit is as high 15 | * as possible. Aside from the accelLimit, this is identical to a SlewRateLimiter or TimedRateLimit. 16 | * 17 | * @author Sam (sam.belliveau@gmail.com) 18 | */ 19 | public class AMotionProfile implements AFilter { 20 | 21 | // Default number of times to apply filter (helps accuracy) 22 | private static final int kDefaultSteps = 64; 23 | 24 | // Stopwatch to Track dt 25 | private StopWatch mTimer; 26 | 27 | // Limits for each of the derivatives 28 | private Number mVelLimit; 29 | private Number mAccelLimit; 30 | 31 | // The last output / acceleration 32 | private Angle mOutput; 33 | private double mAccel; 34 | 35 | // Number of times to apply filter (helps accuracy) 36 | private final int mSteps; 37 | 38 | /** 39 | * @param velLimit maximum change in velocity per second (u/s) 40 | * @param accelLimit maximum change in acceleration per second (u/s/s) 41 | * @param steps number of times to apply filter (improves accuracy) 42 | */ 43 | public AMotionProfile(Number velLimit, Number accelLimit, int steps) { 44 | mTimer = new StopWatch(); 45 | 46 | mVelLimit = velLimit; 47 | mAccelLimit = accelLimit; 48 | 49 | mOutput = Angle.kZero; 50 | mAccel = 0; 51 | 52 | mSteps = steps; 53 | } 54 | 55 | /** 56 | * @param velLimit maximum change in velocity per second (u/s) 57 | * @param accelLimit maximum change in acceleration per second (u/s/s) 58 | */ 59 | public AMotionProfile(Number velLimit, Number accelLimit) { 60 | this(velLimit, accelLimit, kDefaultSteps); 61 | } 62 | 63 | public Angle get(Angle target) { 64 | double dt = mTimer.reset() / mSteps; 65 | 66 | for (int i = 0; i < mSteps; ++i) { 67 | // if there is a accel limit, limit the amount the acceleration can change 68 | if (0 < mAccelLimit.doubleValue()) { 69 | // amount of windup in system (how long it would take to slow down) 70 | double windup = Math.abs(mAccel) / mAccelLimit.doubleValue(); 71 | 72 | // If the windup is too small, just use normal algorithm to limit acceleration 73 | if (windup < dt) { 74 | // Calculate acceleration needed to reach target 75 | double accel = target.velocityRadians(mOutput, dt) - mAccel; 76 | 77 | // Try to reach it while abiding by accellimit 78 | mAccel += SLMath.clamp(accel, dt * mAccelLimit.doubleValue()); 79 | } else { 80 | // the position it would end up if it attempted to come to a full stop 81 | Angle future = 82 | mOutput.addRadians( 83 | 0.5 * mAccel * (dt + windup)); // where the robot will end up 84 | 85 | // Calculate acceleration needed to come to stop at target throughout windup 86 | double accel = target.velocityRadians(future, windup); 87 | 88 | // Try to reach it while abiding by accelLimit 89 | mAccel += SLMath.clamp(accel, dt * mAccelLimit.doubleValue()); 90 | } 91 | 92 | } else { 93 | // make the acceleration the difference between target and current 94 | mAccel = target.velocityRadians(mOutput, dt); 95 | } 96 | 97 | // if there is an acceleration limit, limit the acceleration 98 | if (0 < mVelLimit.doubleValue()) { 99 | mAccel = SLMath.clamp(mAccel, mVelLimit.doubleValue()); 100 | } 101 | 102 | // adjust output by calculated acceleration 103 | mOutput = mOutput.addRadians(dt * mAccel); 104 | } 105 | 106 | return mOutput; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/angles/filters/ARateLimit.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.angles.filters; 6 | 7 | import com.stuypulse.stuylib.math.Angle; 8 | import com.stuypulse.stuylib.math.SLMath; 9 | import com.stuypulse.stuylib.util.StopWatch; 10 | 11 | /** 12 | * This class lets you rate limit a stream of inputs 13 | * 14 | *

Instead of being based on the rate that update is called, the value you give it is based on 15 | * how much it should be able to change in one second. 16 | * 17 | * @author Sam (sam.belliveau@gmail.com) 18 | */ 19 | public class ARateLimit implements AFilter { 20 | 21 | // Used to get the time since the last get call 22 | private StopWatch mTimer; 23 | 24 | // Used to limit the change from the last value 25 | private Angle mLastValue; 26 | private Number mRateLimit; 27 | 28 | /** @param rateLimit The max speed in rad/s. */ 29 | public ARateLimit(Number rateLimit) { 30 | if (rateLimit.doubleValue() <= 0) { 31 | throw new IllegalArgumentException("rateLimit must be a positive number"); 32 | } 33 | 34 | mTimer = new StopWatch(); 35 | mRateLimit = rateLimit; 36 | mLastValue = Angle.kZero; 37 | } 38 | 39 | public Angle get(Angle next) { 40 | final double limit = mRateLimit.doubleValue() * mTimer.reset(); 41 | return mLastValue = 42 | mLastValue.addRadians(SLMath.clamp(next.sub(mLastValue).toRadians(), limit)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/booleans/BStream.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.booleans; 6 | 7 | import com.stuypulse.stuylib.streams.booleans.filters.BFilter; 8 | 9 | import edu.wpi.first.wpilibj.DigitalInput; 10 | import edu.wpi.first.wpilibj2.command.button.Trigger; 11 | 12 | import java.util.function.BooleanSupplier; 13 | 14 | /** 15 | * A BStream is similar to an IStream, but instead of a stream of doubles, it represents a stream of 16 | * booleans. 17 | * 18 | * @author Sam (sam.belliveau@gmail.com) 19 | */ 20 | public interface BStream extends BooleanSupplier { 21 | 22 | /** 23 | * Create a BStream from another BStream. This is helpful if you want to use some of the 24 | * decorator functions with a lambda. 25 | * 26 | * @param stream stream to create BStream from 27 | * @return the resulting BStream 28 | */ 29 | public static BStream create(BStream stream) { 30 | return stream; 31 | } 32 | 33 | /** 34 | * Create a BStream from a digital source. This can helpful for processing a digital stream, 35 | * like negating the value when used for switches. 36 | * 37 | * @param input digital input object 38 | * @return the resulting BStream 39 | */ 40 | public static BStream create(DigitalInput input) { 41 | return input::get; 42 | } 43 | 44 | /** @return next value in the stream */ 45 | public boolean get(); 46 | 47 | /** @return get BStream as a Boolean */ 48 | public default boolean getAsBoolean() { 49 | return get(); 50 | } 51 | 52 | /** 53 | * Create a new FilteredBStream from the current stream 54 | * 55 | * @param filters the filters you want to apply to the BStream 56 | * @return The FilteredBStream 57 | */ 58 | public default FilteredBStream filtered(BFilter... filters) { 59 | return new FilteredBStream(this, filters); 60 | } 61 | 62 | /** 63 | * Create a new PollingBStream from the current stream 64 | * 65 | * @param dt the time inbetween each poll of the BStream 66 | * @return The PollingBStream 67 | */ 68 | public default PollingBStream polling(double dt) { 69 | return new PollingBStream(this, dt); 70 | } 71 | 72 | /** 73 | * Combine two BStreams by and'ing their results together 74 | * 75 | * @param other other BStream to and with this one 76 | * @return the resulting BStream after the and 77 | */ 78 | public default BStream and(BStream other) { 79 | return () -> get() & other.get(); 80 | } 81 | 82 | /** 83 | * Combine two BStreams by or'ing their results together 84 | * 85 | * @param other other BStream to or with this one 86 | * @return the resulting BStream after the or 87 | */ 88 | public default BStream or(BStream other) { 89 | return () -> get() | other.get(); 90 | } 91 | 92 | /** 93 | * Combine two BStreams by xor'ing their results together 94 | * 95 | * @param other other BStream to xor with this one 96 | * @return the resulting BStream after the xor 97 | */ 98 | public default BStream xor(BStream other) { 99 | return () -> get() ^ other.get(); 100 | } 101 | 102 | /** 103 | * Create a BStream that returns the opposite result as the original 104 | * 105 | * @return the resulting BStream after the not operation 106 | */ 107 | public default BStream not() { 108 | return () -> !get(); 109 | } 110 | 111 | /** @return a WPILib Trigger that is pressed when this class is true */ 112 | public default Trigger toButton() { 113 | return new Trigger(this); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/booleans/FilteredBStream.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.booleans; 6 | 7 | import com.stuypulse.stuylib.streams.booleans.filters.BFilter; 8 | 9 | /** 10 | * A FilteredBStream is similar to a FilteredIStream. 11 | * 12 | *

It works like a BStream, but every time you call .get(), it runs the value through the filters 13 | * you provided it. 14 | * 15 | * @author Sam (sam.belliveau@gmail.com) 16 | */ 17 | public class FilteredBStream implements BStream { 18 | 19 | private BStream mStream; // Stream used 20 | private BFilter mStreamFilter; // StreamFilter used 21 | 22 | /** 23 | * Makes filtered stream from stream and stream filter 24 | * 25 | * @param stream input stream 26 | * @param filter stream filter 27 | */ 28 | public FilteredBStream(BStream stream, BFilter... filter) { 29 | mStream = stream; 30 | mStreamFilter = BFilter.create(filter); 31 | } 32 | 33 | /** 34 | * Get next value from filtered stream 35 | * 36 | * @return next value 37 | */ 38 | public boolean get() { 39 | return mStreamFilter.get(mStream.get()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/booleans/PollingBStream.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.booleans; 6 | 7 | import edu.wpi.first.wpilibj.Notifier; 8 | 9 | /** 10 | * A PollingBStream is a BStream but its .get() method is called for you at a certain rate. This is 11 | * really helpful when you want to read from a sensor using a debouncer, but you dont always call 12 | * .get() in your periodic loop. This will do that for you, and give you the most recent result. It 13 | * is not recommended to filter a PollingBStream as you should ideally filter before you poll. 14 | * 15 | * @author Sam (sam.belliveau@gmail.com) 16 | */ 17 | public class PollingBStream implements BStream { 18 | 19 | private Notifier mPoller; 20 | private volatile boolean mResult; 21 | 22 | /** 23 | * Creates a PollingBStream from an BStream and a time value 24 | * 25 | * @param stream BStream to poll from 26 | * @param dt time inbetween each poll 27 | */ 28 | public PollingBStream(BStream stream, double dt) { 29 | if (dt <= 0) { 30 | throw new IllegalArgumentException("dt must be greater than 0"); 31 | } 32 | 33 | mResult = false; 34 | mPoller = new Notifier(() -> mResult = stream.get()); 35 | mPoller.startPeriodic(dt); 36 | } 37 | 38 | public boolean get() { 39 | return mResult; 40 | } 41 | 42 | protected void finalize() { 43 | close(); 44 | } 45 | 46 | public void close() { 47 | mPoller.close(); 48 | mResult = false; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/booleans/filters/BButton.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.booleans.filters; 6 | 7 | /** 8 | * A simple boolean filter that returns true when a boolean stream changes depending on the type. 9 | * 10 | *

1. Both - will return true if the BStream changes at all 11 | * 12 | *

2. Pressed - will return true if the BStream changes from false to true 13 | * 14 | *

3. Released - will return true if the BStream changes from true to false 15 | * 16 | * @author Sam (sam.belliveau@gmail.com) 17 | */ 18 | public interface BButton extends BFilter { 19 | 20 | public static class Both implements BButton { 21 | private boolean mLastValue; 22 | 23 | public Both() { 24 | mLastValue = false; 25 | } 26 | 27 | public boolean get(boolean next) { 28 | if (next == mLastValue) { 29 | return false; 30 | } else { 31 | mLastValue = next; 32 | return true; 33 | } 34 | } 35 | } 36 | 37 | public static class Pressed implements BButton { 38 | private boolean mLastValue; 39 | 40 | public Pressed() { 41 | mLastValue = false; 42 | } 43 | 44 | public boolean get(boolean next) { 45 | if (next == mLastValue) { 46 | return false; 47 | } else { 48 | return (mLastValue = next) == true; 49 | } 50 | } 51 | } 52 | 53 | public static class Released implements BButton { 54 | private boolean mLastValue; 55 | 56 | public Released() { 57 | mLastValue = false; 58 | } 59 | 60 | public boolean get(boolean next) { 61 | if (next == mLastValue) { 62 | return false; 63 | } else { 64 | return (mLastValue = next) == false; 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/booleans/filters/BButtonRC.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.booleans.filters; 6 | 7 | import com.stuypulse.stuylib.streams.numbers.filters.HighPassFilter; 8 | 9 | /** 10 | * A simple boolean filter that returns true when a boolean stream changes depending on the type. 11 | * This type of button filter will hold true for a certain decay length before returning to false. 12 | * 13 | *

1. Both - will return true if the BStream changes at all 14 | * 15 | *

2. Pressed - will return true if the BStream changes from false to true 16 | * 17 | *

3. Released - will return true if the BStream changes from true to false 18 | * 19 | * @author Sam (sam.belliveau@gmail.com) 20 | */ 21 | public interface BButtonRC extends BFilter { 22 | 23 | public static final double kTrue = 1.0; 24 | public static final double kFalse = 0.0; 25 | 26 | public static final double kPressedLimit = 1.0 / Math.E; 27 | public static final double kReleasedLimit = -kPressedLimit; 28 | 29 | public static class Both implements BButtonRC { 30 | private HighPassFilter mFilter; 31 | 32 | /** @param decay time in seconds to remain true after the BStream is pressed or released */ 33 | public Both(Number decay) { 34 | mFilter = new HighPassFilter(decay); 35 | } 36 | 37 | public boolean get(boolean next) { 38 | double value = mFilter.get(next ? kTrue : kFalse); 39 | return value > kPressedLimit || value < kReleasedLimit; 40 | } 41 | } 42 | 43 | public static class Pressed implements BButtonRC { 44 | private HighPassFilter mFilter; 45 | 46 | /** @param decay time in seconds to remain true after the BStream has been pressed */ 47 | public Pressed(Number decay) { 48 | mFilter = new HighPassFilter(decay); 49 | } 50 | 51 | public boolean get(boolean next) { 52 | return mFilter.get(next ? kTrue : kFalse) > kPressedLimit; 53 | } 54 | } 55 | 56 | public static class Released implements BButtonRC { 57 | private HighPassFilter mFilter; 58 | 59 | /** @param decay time in seconds to remain true after the BStream has been released */ 60 | public Released(Number decay) { 61 | mFilter = new HighPassFilter(decay); 62 | } 63 | 64 | public boolean get(boolean next) { 65 | return mFilter.get(next ? kTrue : kFalse) < kReleasedLimit; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/booleans/filters/BDebounce.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.booleans.filters; 6 | 7 | import com.stuypulse.stuylib.util.StopWatch; 8 | 9 | /** 10 | * A collection of Debounce Classes in 3 flavors. 11 | * 12 | *

1. Rising - BStream must be true for x seconds 13 | * 14 | *

2. Falling - BStream must be false for x seconds 15 | * 16 | *

3. Both - BStream must remain constant for x seconds to change 17 | * 18 | * @author Sam (sam.belliveau@gmail.com) 19 | */ 20 | public interface BDebounce extends BFilter { 21 | 22 | /** 23 | * A Rising Debounce Filter. 24 | * 25 | *

The input BStream must remain true for debounceTime before this returns true 26 | */ 27 | public static class Rising implements BDebounce { 28 | 29 | private final StopWatch mTimer; 30 | private final Number mDebounceTime; 31 | 32 | /** @param debounceTime amount of time the BStream must remain true before returning true */ 33 | public Rising(Number debounceTime) { 34 | mTimer = new StopWatch(); 35 | mDebounceTime = debounceTime; 36 | } 37 | 38 | public boolean get(boolean next) { 39 | if (next == false) { 40 | mTimer.reset(); 41 | return false; 42 | } 43 | 44 | return (mDebounceTime.doubleValue() < mTimer.getTime()) == true; 45 | } 46 | } 47 | 48 | /** 49 | * A Falling Debounce Filter. 50 | * 51 | *

The input BStream must remain false for debounceTime before this returns false 52 | */ 53 | public static class Falling implements BDebounce { 54 | 55 | private final StopWatch mTimer; 56 | private final Number mDebounceTime; 57 | 58 | /** 59 | * @param debounceTime amount of time the BStream must remain false before returning false 60 | */ 61 | public Falling(Number debounceTime) { 62 | mTimer = new StopWatch(); 63 | mDebounceTime = debounceTime; 64 | } 65 | 66 | public boolean get(boolean next) { 67 | if (next == true) { 68 | mTimer.reset(); 69 | return true; 70 | } 71 | 72 | return (mDebounceTime.doubleValue() < mTimer.getTime()) == false; 73 | } 74 | } 75 | 76 | /** 77 | * A General Debounce Filter. 78 | * 79 | *

The input BStream must remain the constant for debounceTime before it changes the value it 80 | * returns 81 | */ 82 | public static class Both implements BDebounce { 83 | 84 | private final StopWatch mTimer; 85 | private final Number mDebounceTime; 86 | private boolean mLastValue; 87 | 88 | /** @param debounceTime amount of time the BStream must remain constant before changing */ 89 | public Both(Number debounceTime) { 90 | mTimer = new StopWatch(); 91 | mDebounceTime = debounceTime; 92 | mLastValue = false; 93 | } 94 | 95 | public boolean get(boolean next) { 96 | if (next == mLastValue) { 97 | mTimer.reset(); 98 | return mLastValue; 99 | } else if (mDebounceTime.doubleValue() < mTimer.getTime()) { 100 | mTimer.reset(); 101 | return mLastValue = next; 102 | } else { 103 | return mLastValue; 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/booleans/filters/BDebounceRC.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.booleans.filters; 6 | 7 | import com.stuypulse.stuylib.streams.numbers.filters.LowPassFilter; 8 | 9 | /** 10 | * An RC Debounce class takes the average of the past few boolean values to remove noise. 11 | * Recommended to be called very often using a PollingBStream. 12 | * 13 | * @author Sam (sam.belliveau@gmail.com) 14 | */ 15 | public interface BDebounceRC extends BFilter { 16 | 17 | public static final double kTrue = 1.0; 18 | public static final double kFalse = 0.0; 19 | 20 | public static final double kLowerLimit = 1.0 / Math.E; 21 | public static final double kUpperLimit = 1.0 - kLowerLimit; 22 | 23 | /** An RC Debouncer that has a true bias */ 24 | public static class Rising implements BDebounceRC { 25 | private final LowPassFilter mFilter; 26 | 27 | /** @param debounceTime amount of time on average to go from false to true */ 28 | public Rising(Number debounceTime) { 29 | mFilter = new LowPassFilter(debounceTime); 30 | } 31 | 32 | public boolean get(boolean next) { 33 | return mFilter.get(next ? kTrue : kFalse) > kUpperLimit; 34 | } 35 | } 36 | 37 | /** An RC Debouncer that has a false bias */ 38 | public static class Falling implements BDebounceRC { 39 | private final LowPassFilter mFilter; 40 | 41 | /** @param debounceTime amount of time on average to go from true to false */ 42 | public Falling(Number debounceTime) { 43 | mFilter = new LowPassFilter(debounceTime); 44 | } 45 | 46 | public boolean get(boolean next) { 47 | return mFilter.get(next ? kTrue : kFalse) > kLowerLimit; 48 | } 49 | } 50 | 51 | /** An RC Debouncer that has a bias towards the previous value */ 52 | public static class Both implements BDebounceRC { 53 | 54 | private final LowPassFilter mFilter; 55 | private boolean mPrev; 56 | 57 | /** @param debounceTime amount of time on average to change value */ 58 | public Both(Number debounceTime) { 59 | mFilter = new LowPassFilter(debounceTime); 60 | mPrev = false; 61 | } 62 | 63 | public boolean get(boolean next) { 64 | return mPrev = mFilter.get(next ? kTrue : kFalse) > (mPrev ? kLowerLimit : kUpperLimit); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/booleans/filters/BFilter.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.booleans.filters; 6 | 7 | /** 8 | * This is the BFilter interface class that gives a definition for how to implement a filter. 9 | * 10 | *

All that a BFilter does is take in the next boolean in a stream and gives you the filtered 11 | * value. 12 | * 13 | * @author Sam (sam.belliveau@gmail.com) 14 | */ 15 | public interface BFilter { 16 | 17 | /** 18 | * Create a BFilter from a list of BFilters. This will create a BFilter with the least possible 19 | * overhead for the number of filters provided. 20 | * 21 | * @param filters list of BFilters to apply 22 | * @return an BFilter that uses every filter given 23 | */ 24 | public static BFilter create(BFilter... filters) { 25 | switch (filters.length) { 26 | case 0: 27 | return x -> x; 28 | case 1: 29 | return filters[0]; 30 | case 2: 31 | return x -> filters[1].get(filters[0].get(x)); 32 | case 3: 33 | return x -> filters[2].get(filters[1].get(filters[0].get(x))); 34 | case 4: 35 | return x -> filters[3].get(filters[2].get(filters[1].get(filters[0].get(x)))); 36 | default: 37 | return new BFilterGroup(filters); 38 | } 39 | } 40 | 41 | /** 42 | * Get next value in Filter based on the next value given 43 | * 44 | * @param next next input value in the stream 45 | * @return the output value of the filter 46 | */ 47 | public boolean get(boolean next); 48 | 49 | /** 50 | * Combine an BFilter with another BFilter 51 | * 52 | * @param next filter to be evaluated after this one 53 | * @return the combined filter 54 | */ 55 | public default BFilter then(BFilter next) { 56 | return x -> next.get(get(x)); 57 | } 58 | 59 | /** 60 | * Combine two BFilters by and'ing their results together 61 | * 62 | * @param other other BFilter to and with this one 63 | * @return the resulting BFilter after the and 64 | */ 65 | public default BFilter and(BFilter other) { 66 | return x -> get(x) & other.get(x); 67 | } 68 | 69 | /** 70 | * Combine two BFilters by or'ing their results together 71 | * 72 | * @param other other BFilter to or with this one 73 | * @return the resulting BFilter after the or 74 | */ 75 | public default BFilter or(BFilter other) { 76 | return x -> get(x) | other.get(x); 77 | } 78 | 79 | /** 80 | * Combine two BFilters by xor'ing their results together 81 | * 82 | * @param other other BFilter to xor with this one 83 | * @return the resulting BFilter after the xor 84 | */ 85 | public default BFilter xor(BFilter other) { 86 | return x -> get(x) ^ other.get(x); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/booleans/filters/BFilterGroup.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.booleans.filters; 6 | 7 | /** 8 | * A class that lets you combine multiple boolean filters into one boolean filter. 9 | * 10 | * @author Sam (sam.belliveau@gmail.com) 11 | */ 12 | public class BFilterGroup implements BFilter { 13 | 14 | // Array of all the filters 15 | private final BFilter[] mFilters; 16 | 17 | /** 18 | * Make BFilterGroup out of an array of Filters 19 | * 20 | * @param filters list of filters to combine 21 | */ 22 | public BFilterGroup(BFilter... filters) { 23 | mFilters = filters; 24 | } 25 | 26 | public boolean get(boolean next) { 27 | // Put next through each of the filters 28 | for (BFilter filter : mFilters) { 29 | next = filter.get(next); 30 | } 31 | 32 | // Return filtered value 33 | return next; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/numbers/FilteredIStream.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.numbers; 6 | 7 | import com.stuypulse.stuylib.streams.numbers.filters.IFilter; 8 | 9 | /** 10 | * Takes an {@link IStream} and a {@link IFilter} and makes a {@link FilteredIStream} 11 | * 12 | *

This could be used to automatically filter controller inputs 13 | * 14 | * @author Sam (sam.belliveau@gmail.com) 15 | */ 16 | public class FilteredIStream implements IStream { 17 | 18 | private IStream mStream; // Stream used 19 | private IFilter mStreamFilter; // StreamFilter used 20 | 21 | /** 22 | * Makes filtered stream from stream and stream filter 23 | * 24 | * @param stream input stream 25 | * @param filter stream filter 26 | */ 27 | public FilteredIStream(IStream stream, IFilter... filter) { 28 | mStream = stream; 29 | mStreamFilter = IFilter.create(filter); 30 | } 31 | 32 | /** 33 | * Get next value from filtered stream 34 | * 35 | * @return next value 36 | */ 37 | public double get() { 38 | return mStreamFilter.get(mStream.get()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/numbers/IStream.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.numbers; 6 | 7 | import com.stuypulse.stuylib.streams.angles.AStream; 8 | import com.stuypulse.stuylib.streams.booleans.BStream; 9 | import com.stuypulse.stuylib.streams.numbers.filters.IFilter; 10 | 11 | import java.util.function.DoubleSupplier; 12 | 13 | /** 14 | * A stream of doubles that is accessed with the {@link IStream#get()} function 15 | * 16 | *

Can be created with lambdas to represent a stream of inputs. 17 | * 18 | *

It can be filitered with the available filtering options. 19 | * 20 | * @author Sam (sam.belliveau@gmail.com) 21 | */ 22 | public interface IStream extends DoubleSupplier { 23 | 24 | /** 25 | * Create an IStream from another IStream. This is helpful if you want to use some of the 26 | * decorator functions with a lambda. 27 | * 28 | * @param stream stream to create IStream from 29 | * @return the resulting IStream 30 | */ 31 | public static IStream create(IStream stream) { 32 | return stream; 33 | } 34 | 35 | /** 36 | * Create an IStream from another AStream. This will convert the angle to a double in radians. 37 | * 38 | * @param stream stream to create IStream from 39 | * @return the resulting IStream 40 | */ 41 | public static IStream create(AStream stream) { 42 | return () -> stream.get().toRadians(); 43 | } 44 | 45 | /** 46 | * Create a IStream from another BStream. This will check if the amplitude is above a certain 47 | * threshold. 48 | * 49 | * @param stream stream to create IStream from 50 | * @return the resulting IStream 51 | */ 52 | public static IStream create(BStream stream) { 53 | return () -> stream.get() ? 1.0 : 0.0; 54 | } 55 | 56 | /** @return next value in the stream */ 57 | public double get(); 58 | 59 | /** @return get IStream as a double */ 60 | public default double getAsDouble() { 61 | return get(); 62 | } 63 | 64 | /** 65 | * Create a new FilteredIStream from the current stream 66 | * 67 | * @param filters the filters you want to apply to the IStream 68 | * @return The FilteredIStream 69 | */ 70 | public default FilteredIStream filtered(IFilter... filters) { 71 | return new FilteredIStream(this, filters); 72 | } 73 | 74 | /** 75 | * Create a new PollingIStream from the current stream 76 | * 77 | * @param dt the time inbetween each poll of the IStream 78 | * @return The PollingIStream 79 | */ 80 | public default PollingIStream polling(double dt) { 81 | return new PollingIStream(this, dt); 82 | } 83 | 84 | /** 85 | * Combine two IStreams by adding their results together 86 | * 87 | * @param other other IStream to add to this one 88 | * @return the resulting IStream after the sum 89 | */ 90 | public default IStream add(IStream other) { 91 | return () -> get() + other.get(); 92 | } 93 | 94 | /** 95 | * Combine two IStreams by subtracting their results together 96 | * 97 | * @param other other IStream to subtract from this one 98 | * @return the resulting IStream after the subtraction 99 | */ 100 | public default IStream sub(IStream other) { 101 | return () -> get() - other.get(); 102 | } 103 | 104 | /** 105 | * Casts an IStream to a Number 106 | * 107 | * @return a Number that reads from this stream 108 | */ 109 | public default NumberStream number() { 110 | return new NumberStream(this); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/numbers/NumberStream.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.numbers; 6 | 7 | /** 8 | * A number which reads from a streams. 9 | * 10 | *

Useful for filtering streams (including SmartNumbers) and converting them back into Numbers. 11 | * 12 | * @author Myles Pasetsky (myles.pasetsky@gmail.com) 13 | */ 14 | public class NumberStream extends Number implements IStream { 15 | 16 | /** the underlying stream */ 17 | private final IStream mStream; 18 | 19 | /** 20 | * Create a number stream given a stream 21 | * 22 | * @param stream IStream to return through number classes 23 | */ 24 | public NumberStream(IStream stream) { 25 | mStream = stream; 26 | } 27 | 28 | /** @return value from stream */ 29 | @Override 30 | public double get() { 31 | return mStream.get(); 32 | } 33 | 34 | /** @return value from stream */ 35 | @Override 36 | public double doubleValue() { 37 | return get(); 38 | } 39 | 40 | /** @return value from stream as float */ 41 | @Override 42 | public float floatValue() { 43 | return (float) get(); 44 | } 45 | 46 | /** @return value from stream as integer */ 47 | @Override 48 | public int intValue() { 49 | return (int) get(); 50 | } 51 | 52 | /** @return value from stream as long */ 53 | @Override 54 | public long longValue() { 55 | return (long) get(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/numbers/PollingIStream.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.numbers; 6 | 7 | import edu.wpi.first.wpilibj.Notifier; 8 | 9 | /** 10 | * A PollingIStream calls {@link IStream#get()} every x milliseconds instead of when the user calls 11 | * get 12 | * 13 | * @author Sam (sam.belliveau@gmail.com) 14 | */ 15 | public class PollingIStream implements IStream, AutoCloseable { 16 | 17 | private Notifier mPoller; 18 | private volatile double mResult; 19 | 20 | /** 21 | * Creates a PollingIStream from an IStream and a time value 22 | * 23 | * @param stream istream to poll from 24 | * @param dt Number of calls per second 25 | */ 26 | public PollingIStream(IStream stream, double dt) { 27 | if (dt <= 0) { 28 | throw new IllegalArgumentException("dt must be greater than 0"); 29 | } 30 | 31 | mResult = 0.0; 32 | mPoller = new Notifier(() -> mResult = stream.get()); 33 | mPoller.startPeriodic(dt); 34 | } 35 | 36 | public double get() { 37 | return mResult; 38 | } 39 | 40 | protected void finalize() { 41 | close(); 42 | } 43 | 44 | public void close() { 45 | mPoller.close(); 46 | mResult = 0.0; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/numbers/filters/Derivative.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.numbers.filters; 6 | 7 | import com.stuypulse.stuylib.util.StopWatch; 8 | 9 | /** 10 | * This class takes an IStream and gives you the derivative with respect to time. 11 | * 12 | * @author Sam (sam.belliveau@gmail.com) 13 | */ 14 | public class Derivative implements IFilter { 15 | 16 | private final StopWatch mTimer; 17 | private double mLastValue; 18 | 19 | public Derivative() { 20 | mTimer = new StopWatch(); 21 | mLastValue = 0.0; 22 | } 23 | 24 | public double get(double next) { 25 | double diff = next - mLastValue; 26 | mLastValue = next; 27 | return diff / mTimer.reset(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/numbers/filters/HighPassFilter.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.numbers.filters; 6 | 7 | /** 8 | * Implementation for of a real time IIR HighPassFilter 9 | * 10 | * @author Sam (sam.belliveau@gmail.com) 11 | */ 12 | public class HighPassFilter implements IFilter { 13 | 14 | private LowPassFilter mInverse; 15 | 16 | /** @param rc time constant for high pass filter */ 17 | public HighPassFilter(Number rc) { 18 | mInverse = new LowPassFilter(rc); 19 | } 20 | 21 | public double get(double next) { 22 | return next - mInverse.get(next); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/numbers/filters/IFilter.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.numbers.filters; 6 | 7 | /** 8 | * This is the Filter interface class that gives a definition for how to implement a filter. 9 | * 10 | *

All that a filter does is take in the next double in a series and gives you the filtered 11 | * value. 12 | * 13 | * @author Sam (sam.belliveau@gmail.com) 14 | */ 15 | public interface IFilter { 16 | 17 | /** 18 | * Create an IFilter from a list of IFilters. This will create an IFilter with the least 19 | * possible overhead for the number of filters provided. 20 | * 21 | * @param filters list of IFilters to apply 22 | * @return an IFilter that uses every filter given 23 | */ 24 | public static IFilter create(IFilter... filters) { 25 | switch (filters.length) { 26 | case 0: 27 | return x -> x; 28 | case 1: 29 | return filters[0]; 30 | case 2: 31 | return x -> filters[1].get(filters[0].get(x)); 32 | case 3: 33 | return x -> filters[2].get(filters[1].get(filters[0].get(x))); 34 | case 4: 35 | return x -> filters[3].get(filters[2].get(filters[1].get(filters[0].get(x)))); 36 | default: 37 | return new IFilterGroup(filters); 38 | } 39 | } 40 | 41 | /** 42 | * Get next value in Filter based on the next value given 43 | * 44 | * @param next next input value in the stream 45 | * @return the output value of the filter 46 | */ 47 | public double get(double next); 48 | 49 | /** 50 | * Combine an IFilter with another IFilter 51 | * 52 | * @param next filter to be evaluated after this one 53 | * @return the combined filter 54 | */ 55 | public default IFilter then(IFilter next) { 56 | return x -> next.get(get(x)); 57 | } 58 | 59 | /** 60 | * Combine two IFilters by adding their results together 61 | * 62 | * @param other other IFilter to add to this one 63 | * @return the resulting IFilter after the sum 64 | */ 65 | public default IFilter add(IFilter other) { 66 | return x -> get(x) + other.get(x); 67 | } 68 | 69 | /** 70 | * Combine two IFilters by subtracting their results together 71 | * 72 | * @param other other IFilter to subtract from this one 73 | * @return the resulting IFilter after the subtraction 74 | */ 75 | public default IFilter sub(IFilter other) { 76 | return x -> get(x) - other.get(x); 77 | } 78 | 79 | /** 80 | * Invert an IFilter by subtracting the input from the result of the IFilter. 81 | * 82 | *

Inverting a LowPassFilter gives you a HighPassFilter and vise versa. 83 | * 84 | *

Inverting something twice gives you the original value. 85 | * 86 | * @return the inverted filter 87 | */ 88 | public default IFilter invert() { 89 | return x -> x - get(x); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/numbers/filters/IFilterGroup.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.numbers.filters; 6 | 7 | /** 8 | * A class that lets you combine multiple stream filters into one stream filter 9 | * 10 | * @author Sam (sam.belliveau@gmail.com) 11 | */ 12 | public class IFilterGroup implements IFilter { 13 | 14 | // Array of all the filters 15 | private final IFilter[] mFilters; 16 | 17 | /** 18 | * Make IFilterGroup out of an array of Filters 19 | * 20 | * @param filters list of filters to combine 21 | */ 22 | public IFilterGroup(IFilter... filters) { 23 | mFilters = filters; 24 | } 25 | 26 | public double get(double next) { 27 | // Put next through each of the filters 28 | for (IFilter filter : mFilters) { 29 | next = filter.get(next); 30 | } 31 | 32 | // Return filtered value 33 | return next; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/numbers/filters/LowPassFilter.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.numbers.filters; 6 | 7 | import com.stuypulse.stuylib.util.StopWatch; 8 | 9 | /** 10 | * Implementation of a real time IIR LowPassFilter 11 | * 12 | * @author Sam (sam.belliveau@gmail.com) 13 | */ 14 | public class LowPassFilter implements IFilter { 15 | 16 | // Used to calculate time in between calls 17 | private final StopWatch mTimer; 18 | 19 | // Used to calculate next value based on previous value and time 20 | private final Number mRC; 21 | private double mLastValue; 22 | 23 | /** 24 | * @param rc Time Constant. The time constant is the amount of time in seconds that it takes to 25 | * get 63.2% of the way to the target value. 63.2% is (1 - (1 / e)). 26 | */ 27 | public LowPassFilter(Number rc) { 28 | mTimer = new StopWatch(); 29 | mLastValue = 0; 30 | mRC = rc; 31 | } 32 | 33 | public double get(double next) { 34 | // If RC will cause errors, disable filter 35 | if (mRC.doubleValue() <= 0.0) { 36 | return mLastValue = next; 37 | } 38 | 39 | // Get a constant, which is determined based on dt and the mRC constant 40 | double a = 1.0 - Math.exp(-mTimer.reset() / mRC.doubleValue()); 41 | 42 | // Based on the value of a (which is determined by dt), the next value 43 | // could either change a lot, or not by much. (smaller dt = smaller change) 44 | return mLastValue += a * (next - mLastValue); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/numbers/filters/MotionProfile.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.numbers.filters; 6 | 7 | import com.stuypulse.stuylib.math.SLMath; 8 | import com.stuypulse.stuylib.util.StopWatch; 9 | 10 | /** 11 | * A filter, that when applied to the input of a motor, will profile it. Similar to the way in which 12 | * motion profiling can limit the amount of acceleration and jerk in an S-Curve, this can do that to 13 | * real time input. Because this will add a delay, it is recommended that the accelLimit is as high 14 | * as possible. Aside from the accelLimit, this is identical to a SlewRateLimiter or TimedRateLimit. 15 | * 16 | * @author Sam (sam.belliveau@gmail.com) 17 | */ 18 | public class MotionProfile implements IFilter { 19 | 20 | // Default number of times to apply filter (helps accuracy) 21 | private static final int kDefaultSteps = 64; 22 | 23 | // Stopwatch to Track dt 24 | private StopWatch mTimer; 25 | 26 | // Limits for each of the derivatives 27 | private Number mVelLimit; 28 | private Number mAccelLimit; 29 | 30 | // The last output / acceleration 31 | private double mOutput; 32 | private double mAccel; 33 | 34 | // Number of times to apply filter (helps accuracy) 35 | private final int mSteps; 36 | 37 | /** 38 | * @param velLimit maximum change in displacement per second (u/s) 39 | * @param accelLimit maximum change in velocity per second (u/s/s) 40 | * @param steps number of times to apply filter (improves accuracy) 41 | */ 42 | public MotionProfile(Number velLimit, Number accelLimit, int steps) { 43 | mTimer = new StopWatch(); 44 | 45 | mVelLimit = velLimit; 46 | mAccelLimit = accelLimit; 47 | 48 | mOutput = 0; 49 | mAccel = 0; 50 | 51 | mSteps = steps; 52 | } 53 | 54 | /** 55 | * @param velLimit maximum change in velocity per second (u/s) 56 | * @param accelLimit maximum change in acceleration per second (u/s/s) 57 | */ 58 | public MotionProfile(Number velLimit, Number accelLimit) { 59 | this(velLimit, accelLimit, kDefaultSteps); 60 | } 61 | 62 | public double get(double target) { 63 | double dt = mTimer.reset() / mSteps; 64 | 65 | for (int i = 0; i < mSteps; ++i) { 66 | // if there is a jerk limit, limit the amount the acceleration can change 67 | if (0 < mAccelLimit.doubleValue()) { 68 | // amount of windup in system (how long it would take to slow down) 69 | double windup = Math.abs(mAccel) / mAccelLimit.doubleValue(); 70 | 71 | // If the windup is too small, just use normal algorithm to limit acceleration 72 | if (windup < dt) { 73 | // Calculate acceleration needed to reach target 74 | double accel = (target - mOutput) / dt - mAccel; 75 | 76 | // Try to reach it while abiding by jerklimit 77 | mAccel += SLMath.clamp(accel, dt * mAccelLimit.doubleValue()); 78 | } else { 79 | // the position it would end up if it attempted to come to a full stop 80 | double windA = 0.5 * mAccel * (dt + windup); // windup caused by acceleration 81 | double future = mOutput + windA; // where the robot will end up 82 | 83 | // Calculate acceleration needed to come to stop at target throughout windup 84 | double accel = (target - future) / windup; 85 | 86 | // Try to reach it while abiding by jerklimit 87 | mAccel += SLMath.clamp(accel, dt * mAccelLimit.doubleValue()); 88 | } 89 | 90 | } else { 91 | // make the acceleration the difference between target and current 92 | mAccel = (target - mOutput) / dt; 93 | } 94 | 95 | // if there is an acceleration limit, limit the acceleration 96 | if (0 < mVelLimit.doubleValue()) { 97 | mAccel = SLMath.clamp(mAccel, mVelLimit.doubleValue()); 98 | } 99 | 100 | // adjust output by calculated acceleration 101 | mOutput += dt * mAccel; 102 | } 103 | 104 | return mOutput; 105 | } 106 | 107 | public void reset(double position) { 108 | mOutput = position; 109 | mAccel = 0; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/numbers/filters/RateLimit.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.numbers.filters; 6 | 7 | import com.stuypulse.stuylib.math.SLMath; 8 | import com.stuypulse.stuylib.util.StopWatch; 9 | 10 | /** 11 | * This class lets you rate limit a stream of inputs 12 | * 13 | *

Instead of being based on the rate that update is called, the value you give it is based on 14 | * how much it should be able to change in one second. 15 | * 16 | * @author Sam (sam.belliveau@gmail.com) 17 | */ 18 | public class RateLimit implements IFilter { 19 | 20 | // Used to get the time since the last get call 21 | private StopWatch mTimer; 22 | 23 | // Used to limit the change from the last value 24 | private double mLastValue; 25 | private Number mRateLimit; 26 | 27 | /** @param rateLimit The amount that the value should be able to change in one second. */ 28 | public RateLimit(Number rateLimit) { 29 | if (rateLimit.doubleValue() <= 0) { 30 | throw new IllegalArgumentException("rateLimit must be a positive number"); 31 | } 32 | 33 | mTimer = new StopWatch(); 34 | mRateLimit = rateLimit; 35 | mLastValue = 0; 36 | } 37 | 38 | public double get(double next) { 39 | return mLastValue += 40 | SLMath.clamp(next - mLastValue, mRateLimit.doubleValue() * mTimer.reset()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/numbers/filters/TimedMovingAverage.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.numbers.filters; 6 | 7 | import com.stuypulse.stuylib.util.StopWatch; 8 | 9 | import java.util.LinkedList; 10 | 11 | /** 12 | * A Simple Moving Average where instead of averaging the past x values, you average all the values 13 | * given in the last x seconds. 14 | * 15 | * @author Sam (sam.belliveau@gmail.com) 16 | */ 17 | public class TimedMovingAverage implements IFilter { 18 | 19 | /** 20 | * How many removals the moving average can do before it does a complete recount for better 21 | * accuracy. 22 | * 23 | *

This is a trade off between speed and accuracy. Having this at 512 means a complete 24 | * recount ~ every 10 seconds on robot code. 25 | */ 26 | private static final int kMaxRemovalsBeforeReset = 512; 27 | 28 | /** Class used to store value and time */ 29 | private static class Value { 30 | 31 | public double value; 32 | public double time; 33 | 34 | public Value(double value, double time) { 35 | this.value = value; 36 | this.time = time; 37 | } 38 | 39 | public double truncate(double t) { 40 | time -= t; 41 | return value * t; 42 | } 43 | 44 | public double sum() { 45 | return value * time; 46 | } 47 | } 48 | 49 | // All information needed to calculate value 50 | private Number mMaxTime; 51 | 52 | private StopWatch mTimer; 53 | 54 | private LinkedList mValues; 55 | private double mCurrentTime; 56 | private double mTotal; 57 | 58 | private int mRemovals; 59 | 60 | /** 61 | * Make Timed Moving Average with time span 62 | * 63 | * @param time time span for which to average 64 | */ 65 | public TimedMovingAverage(Number time) { 66 | if (time.doubleValue() <= 0) { 67 | throw new IllegalArgumentException("time must be > 0"); 68 | } 69 | 70 | mMaxTime = time; 71 | 72 | mTimer = new StopWatch(); 73 | 74 | mValues = new LinkedList<>(); 75 | mCurrentTime = 0.0; 76 | mTotal = 0.0; 77 | 78 | mRemovals = 0; 79 | } 80 | 81 | /** 82 | * Does a recount of the entire moving average to make sure that floating point errors don't 83 | * build up over long periods of time. 84 | */ 85 | private void reset() { 86 | mCurrentTime = 0.0; 87 | mTotal = 0.0; 88 | mRemovals = 0; 89 | 90 | for (final Value val : mValues) { 91 | mCurrentTime += val.time; 92 | mTotal += val.sum(); 93 | } 94 | } 95 | 96 | /** 97 | * Remove entries from the list 98 | * 99 | * @param t the amount of time that needs to be removed 100 | * @return if this should be the last removal 101 | */ 102 | private boolean remove(double t) { 103 | // Redo the count if there have been too many removals 104 | if (kMaxRemovalsBeforeReset < ++mRemovals) { 105 | reset(); 106 | return false; 107 | } 108 | 109 | // If the list is empty or we have negative t we should reset 110 | if (mValues.isEmpty() || t <= 0.0) { 111 | reset(); 112 | return true; 113 | } 114 | 115 | // get the value that we should be removing 116 | final Value val = mValues.getFirst(); 117 | 118 | // if we can remove the entire entry, do that, 119 | // but that means we are not done 120 | if (val.time <= t) { 121 | mValues.removeFirst(); 122 | mTotal -= val.sum(); 123 | mCurrentTime -= val.time; 124 | return false; 125 | } 126 | 127 | // This partially removes something from the list 128 | // this allows us to sample over an exact amount of time 129 | else { 130 | mTotal -= val.truncate(t); 131 | mCurrentTime -= t; 132 | return true; 133 | } 134 | } 135 | 136 | public double get(double next) { 137 | // Add new value to running average 138 | final Value val = new Value(next, mTimer.reset()); 139 | mValues.addLast(val); 140 | mCurrentTime += val.time; 141 | mTotal += val.sum(); 142 | 143 | // Remove values until our time is consistent 144 | while (!remove(mCurrentTime - mMaxTime.doubleValue())) {} 145 | 146 | // Return running average value 147 | if (mCurrentTime <= 0) { 148 | return next; 149 | } else { 150 | return mTotal / mCurrentTime; 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/numbers/filters/readme.md: -------------------------------------------------------------------------------- 1 | # StuyLib Filters Library 2 | 3 | ## What is a filter? 4 | 5 | A filter is basically a class / function that takes in a value, and returns the "filtered" value. 6 | 7 | For example say we made a Moving Average Filter. 8 | 9 | ```java 10 | // Returns the average of the last 3 values 11 | IFilter avg = new MovingAverage(3); 12 | 13 | System.out.println(avg.get(1)); // Prints: 1.0 14 | System.out.println(avg.get(2)); // Prints: 1.5 15 | System.out.println(avg.get(3)); // Prints: 2.0 16 | System.out.println(avg.get(4)); // Prints: 3.0 17 | System.out.println(avg.get(5)); // Prints: 4.0 18 | ``` 19 | 20 | Everytime we called `.get()` on the filter, it returned the averagee of the last 3 values, because thats what the filter was made to do. 21 | 22 | ## What should we use them for? 23 | 24 | ### Ramping Motors 25 | 26 | When sending speed values to motors, you might not always want them to update immediately, as that could cause jerk and current spikes. 27 | 28 | In WPILib, there is the option to ramp motors when configuring them. This ramping algorithm is analygous to the filtering done by the filter called [Rate Limit](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/filters/RateLimit.java). RateLimit works good for smoothing out the motion of the robot, but through testing we have found that the filter called [LowPass Filter](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/filters/LowPassFilter.java), does a much nicer job of providing smooth motion without introducing a large delay. 29 | 30 | We did experiment with [Speed Profile](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/filters/SpeedProfile.java), which algebraicly removes jerk from the inputs of a conroller, however the delay it introduced killed its usefulness compared to [LowPass Filter](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/filters/LowPassFilter.java). 31 | 32 | The reduced jerk when controlling the motors is a huge advantage as it uses less power, and prevents the robot from skipping / tilting, which can mess with sensor data. This allows the driver to drive faster without worrying about damaging the robot. 33 | 34 | Delay is the biggest issue with filters. Because they can not predict the future, it's impossible for it to smooth out values without introducing some amount of delay. We've found that filters that prioritize recent values more than past ones, are able to do the best. 35 | 36 | ### Filtering Data 37 | 38 | When reading data from something like the Limelight or an encoder, sometimes noise can be an issue. Encoders sometimes include a form of [Moving Average](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/filters/MovingAverage.java) to smooth out noise in data. However, filters can often add a delay, which if fed into a PID loop, can often cause decreased performance. We've found that using a small [LowPass Filter](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/filters/LowPassFilter.java) on data that has a slight bit of noise in it can help a lot, however it will decrease performance. 39 | 40 | A recently added filter named [Median Filter](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/filters/MedianFilter.java) is able to reduce noise while not having a huge impact on delay. It is good to combine this filter with other filters to get an optimal result. 41 | 42 | ## Which Filter should I use? 43 | 44 | Some filters have internal timers in them which keep track of real world time. This is helpful if the data your collecting is real time, and in a time series. When it comes to FRC Robotics, almost all data that we have used filters on is real time. Be sure to read the documentation of certain filters to see which ones use timers in them. 45 | 46 | Depending on which type of data you are working with, some filters might be better than others. However, we've found the [LowPass Filter](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/filters/LowPassFilter.java) to be an amazing all around filter, and should be used as a Rule of Thumb when testing out filters. *(meaning that LowPass Filters will usually be the best)* 47 | 48 | Also, combine filters using [IFilterGroup](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/filters/IFilterGroup.java), as filtering filtered values can often have interesting and desirable results. 49 | 50 | ## Why do filters take in `Numbers` instead of `double`? 51 | 52 | In order to help make filters more configurable, the configuation is done using the Number class. Why? Because [SmartNumber](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/network/SmartNumber.java) extends `Number`, which makes it extremely easy to change values like the RC of a [LowPass Filter](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/filters/LowPassFilter.java) on the fly. 53 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/readme.md: -------------------------------------------------------------------------------- 1 | # StuyLib Streams Library 2 | 3 | ## What is a Stream 4 | 5 | A stream is any class / lambda that when you call `.get()` it returns a double. 6 | 7 | For example, say we wanted a stream of controller inputs, we would write. 8 | 9 | ```java 10 | Gamepad driver = new PS4(0); 11 | 12 | // Basic Right - Left calculation to get speed 13 | IStream speed = () -> driver.getRightTrigger() - driver.getLeftTrigger(); 14 | 15 | System.out.println(speed.get()); // Prints the current desired speed 16 | ``` 17 | 18 | By using a stream here, we now have an object we can call / pass around to get the desired speed of the robot. All we would need to do is call `.get()` to recieve the data. 19 | 20 | ## Why is this useful 21 | 22 | An [IStream](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/IStream.java) is a useful object to pass around as it represents a constantly updating stream of information. There are things we can do with this stream to get some extra functionality. 23 | 24 | ### [Filtered IStream](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/FilteredIStream.java) 25 | 26 | A [Filtered ISteam](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/FilteredIStream.java) applies a [Filter](https://github.com/StuyPulse/StuyLib/tree/main/src/com/stuypulse/stuylib/streams/filters) to a stream, and returns that value instead. 27 | 28 | Continuing from the example above, we could do: 29 | 30 | ```java 31 | IStream filtered_speed = new FilteredIStream(speed, new LowPassFilter(0.5)); 32 | 33 | System.out.println(filtered_speed.get()); // Prints the filtered value of the stream 34 | ``` 35 | 36 | ### [CSVIStream](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/CSVIStream.java) 37 | 38 | CSVIStream has two child classes in it, `CSVIStream.Writer` and `CSVIStream.Reader`. 39 | 40 | `CSVIStream.Writer` is used to save the values of a stream as you read it, and can be used as so. 41 | 42 | ```java 43 | IStream recorded_speed = new CSVIStream.Writer("./speeds.csv", speed); 44 | 45 | System.out.println(recorded_speed.get()); // Prints the value of the speed stream and writes the value to the CSV. 46 | ``` 47 | 48 | `CSVIStream.Reader` is used to read back the values of a CSV file in the form of a stream. Imagine that this is playing back the file we just created from before: 49 | 50 | ```java 51 | IStream file_speed = new CSVIStream.Reader("./speeds.csv"); 52 | 53 | System.out.println(file_speed.get()); // The first value of the CSV file. 54 | System.out.println(file_speed.get()); // The second value of the CSV file. 55 | System.out.println(file_speed.get()); // The third value of the CSV file. 56 | ``` 57 | 58 | This is useful in general for things like logging or reading CSV files in a streamlined way. 59 | 60 | ### [PollingIStream](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/PollingIStream.java) 61 | 62 | [PollingIStream](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/PollingIStream.java) is a very powerful wrapper around an [IStream](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/IStream.java) that polls the underlying `.get()` function a fixed number of times per second. When you call `.get()` on a [PollingIStream](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/PollingIStream.java), it will return you the last polled value from the underlying stream. 63 | 64 | ```java 65 | // Creates an IStream that is updated 50 times a second. 66 | IStream polled_speed = new PollingIStream(speed, 50.0); 67 | ``` 68 | 69 | This is useful for streams where the state changes when you call `.get()`. Say you were using reading from [CSVIStream.Reader](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/CSVIStream.java), but you wanted a new value every 50th of a second. By using a PollingIStream, no matter how fast you call `.get()`, you will only ever recieve a new value every 50th of a second. When using a [Filtered IStream](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/FilteredIStream.java) or a [CSVIStream.Reader](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/CSVIStream.java), the value can change when you call `.get()`, so its important to be aware when you poll a stream. 70 | 71 | ### [BufferedIStream](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/BufferedIStream.java) 72 | 73 | A [BufferedIStream](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/BufferedIStream.java) is just a wrapper around a normal [IStream](https://github.com/StuyPulse/StuyLib/blob/main/src/com/stuypulse/stuylib/streams/IStream.java) that lets you access past values with `.get(delta)`. When stored inside an IStream it acts identically to the original stream, making its usage a bit difficult. 74 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/vectors/FilteredVStream.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.vectors; 6 | 7 | import com.stuypulse.stuylib.math.Vector2D; 8 | import com.stuypulse.stuylib.streams.vectors.filters.VFilter; 9 | 10 | /** 11 | * A FilteredVStream is similar to a FilteredIStream. 12 | * 13 | *

It works like a VStream, but every time you call .get(), it runs the value through the filters 14 | * you provided it. 15 | * 16 | * @author Sam (sam.belliveau@gmail.com) 17 | */ 18 | public class FilteredVStream implements VStream { 19 | 20 | private VStream mStream; // Stream used 21 | private VFilter mStreamFilter; // StreamFilter used 22 | 23 | /** 24 | * Makes filtered stream from stream and stream filter 25 | * 26 | * @param stream input stream 27 | * @param filter stream filter 28 | */ 29 | public FilteredVStream(VStream stream, VFilter... filter) { 30 | mStream = stream; 31 | mStreamFilter = VFilter.create(filter); 32 | } 33 | 34 | /** 35 | * Get next value from filtered stream 36 | * 37 | * @return next value 38 | */ 39 | public Vector2D get() { 40 | return mStreamFilter.get(mStream.get()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/vectors/PollingVStream.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.vectors; 6 | 7 | import com.stuypulse.stuylib.math.Vector2D; 8 | 9 | import edu.wpi.first.wpilibj.Notifier; 10 | 11 | /** 12 | * A PollingVStream is a VStream but its .get() method is called for you at a certain rate. This 13 | * contains race conditions. 14 | * 15 | * @author Sam (sam.belliveau@gmail.com) 16 | */ 17 | public class PollingVStream implements VStream { 18 | 19 | private Notifier mPoller; 20 | private volatile Vector2D mResult; 21 | 22 | /** 23 | * Creates a PollingVStream from an VStream and a time value 24 | * 25 | * @param stream VStream to poll from 26 | * @param dt time inbetween each poll 27 | */ 28 | public PollingVStream(VStream stream, double dt) { 29 | if (dt <= 0) { 30 | throw new IllegalArgumentException("dt must be greater than 0"); 31 | } 32 | 33 | mResult = Vector2D.kOrigin; 34 | mPoller = new Notifier(() -> mResult = stream.get()); 35 | mPoller.startPeriodic(dt); 36 | } 37 | 38 | public Vector2D get() { 39 | return mResult; 40 | } 41 | 42 | protected void finalize() { 43 | close(); 44 | } 45 | 46 | public void close() { 47 | mPoller.close(); 48 | mResult = Vector2D.kOrigin; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/vectors/VStream.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.vectors; 6 | 7 | import com.stuypulse.stuylib.math.Vector2D; 8 | import com.stuypulse.stuylib.streams.angles.AStream; 9 | import com.stuypulse.stuylib.streams.numbers.IStream; 10 | import com.stuypulse.stuylib.streams.vectors.filters.VFilter; 11 | 12 | import java.util.function.Supplier; 13 | 14 | /** 15 | * A VStream is similar to an IStream, but instead of a stream of doubles, it represents a stream of 16 | * Vector2Ds. 17 | * 18 | * @author Sam (sam.belliveau@gmail.com) 19 | */ 20 | public interface VStream extends Supplier { 21 | 22 | public static VStream create(VStream stream) { 23 | return stream; 24 | } 25 | 26 | public static VStream create(AStream stream) { 27 | return () -> stream.get().getVector(); 28 | } 29 | 30 | public static VStream create(IStream x, IStream y) { 31 | return () -> new Vector2D(x.get(), y.get()); 32 | } 33 | 34 | /** @return next value in the stream */ 35 | public Vector2D get(); 36 | 37 | /** 38 | * Create a new FilteredVStream from the current stream 39 | * 40 | * @param filters the filters you want to apply to the VStream 41 | * @return The FilteredVStream 42 | */ 43 | public default FilteredVStream filtered(VFilter... filters) { 44 | return new FilteredVStream(this, filters); 45 | } 46 | 47 | /** 48 | * Create a new PollingVStream from the current stream 49 | * 50 | * @param dt the time inbetween each poll of the VStream 51 | * @return The PollingVStream 52 | */ 53 | public default PollingVStream polling(double dt) { 54 | return new PollingVStream(this, dt); 55 | } 56 | 57 | /** 58 | * Combine two VStreams by adding their results together 59 | * 60 | * @param other other VStream to and with this one 61 | * @return the resulting VStream after the and 62 | */ 63 | public default VStream add(VStream other) { 64 | return () -> get().add(other.get()); 65 | } 66 | 67 | /** 68 | * Combine two VStreams by subtracting their results together 69 | * 70 | * @param other other VStream to or with this one 71 | * @return the resulting VStream after the or 72 | */ 73 | public default VStream sub(VStream other) { 74 | return () -> get().sub(other.get()); 75 | } 76 | 77 | /** 78 | * Combine two VStreams into an IStream by dot producting them together 79 | * 80 | * @param other other VStream to dot with this one 81 | * @return the IStream with the dot product 82 | */ 83 | public default IStream dot(VStream other) { 84 | return () -> get().dot(other.get()); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/vectors/filters/VClamp.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.vectors.filters; 6 | 7 | import com.stuypulse.stuylib.math.Vector2D; 8 | 9 | /** 10 | * A filter that clamps the magnitude of a vector to a certain size 11 | * 12 | * @author Sam (sam.belliveau@gmail.com) 13 | */ 14 | public class VClamp implements VFilter { 15 | 16 | private Number mMaxMagnitude; 17 | 18 | public VClamp(Number maxMagnitude) { 19 | mMaxMagnitude = maxMagnitude; 20 | } 21 | 22 | public Vector2D get(Vector2D target) { 23 | return target.clamp(mMaxMagnitude.doubleValue()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/vectors/filters/VDeadZone.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.vectors.filters; 6 | 7 | import com.stuypulse.stuylib.math.Vector2D; 8 | 9 | /** 10 | * A filter that will zero out a vector if its magnitude is too small 11 | * 12 | * @author Sam (sam.belliveau@gmail.com) 13 | */ 14 | public class VDeadZone implements VFilter { 15 | 16 | private Number mDeadZone; 17 | 18 | public VDeadZone(Number deadzone) { 19 | mDeadZone = deadzone; 20 | } 21 | 22 | public Vector2D get(Vector2D target) { 23 | if (target.magnitude() < mDeadZone.doubleValue()) { 24 | return Vector2D.kOrigin; 25 | } 26 | 27 | return target; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/vectors/filters/VDerivative.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.vectors.filters; 6 | 7 | import com.stuypulse.stuylib.streams.numbers.filters.Derivative; 8 | 9 | /** 10 | * Filter that takes the derivative of a VStream with respect to time. 11 | * 12 | * @author Sam (sam.belliveau@gmail.com) 13 | */ 14 | public class VDerivative extends XYFilter { 15 | 16 | public VDerivative() { 17 | super(new Derivative(), new Derivative()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/vectors/filters/VFilter.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.vectors.filters; 6 | 7 | import com.stuypulse.stuylib.math.Vector2D; 8 | import com.stuypulse.stuylib.streams.numbers.filters.IFilter; 9 | 10 | /** 11 | * This is the VFilter interface class that gives a definition for how to implement a filter. 12 | * 13 | *

All that a VFilter does is take in the next vector in a stream and gives you the filtered 14 | * value. 15 | * 16 | * @author Sam (sam.belliveau@gmail.com) 17 | */ 18 | public interface VFilter { 19 | 20 | /** 21 | * Create a VFilter from a list of VFilters. This will create a VFilter with the least possible 22 | * overhead for the number of filters provided. 23 | * 24 | * @param filters list of VFilters to apply 25 | * @return an VFilter that uses every filter given 26 | */ 27 | public static VFilter create(VFilter... filters) { 28 | switch (filters.length) { 29 | case 0: 30 | return x -> x; 31 | case 1: 32 | return filters[0]; 33 | case 2: 34 | return x -> filters[1].get(filters[0].get(x)); 35 | case 3: 36 | return x -> filters[2].get(filters[1].get(filters[0].get(x))); 37 | case 4: 38 | return x -> filters[3].get(filters[2].get(filters[1].get(filters[0].get(x)))); 39 | default: 40 | return new VFilterGroup(filters); 41 | } 42 | } 43 | 44 | /** 45 | * Create a VFilter by using an IFilter on the x and y components. 46 | * 47 | * @param x IFilter used on x component 48 | * @param y IFilter used on y component 49 | * @return resulting VFilters 50 | */ 51 | public static VFilter create(IFilter x, IFilter y) { 52 | return new XYFilter(x, y); 53 | } 54 | 55 | /** 56 | * Get next value in Filter based on the next value given 57 | * 58 | * @param next next input value in the stream 59 | * @return the output value of the filter 60 | */ 61 | public Vector2D get(Vector2D next); 62 | 63 | /** 64 | * Combine an VFilter with another VFilter 65 | * 66 | * @param next filter to be evaluated after this one 67 | * @return the combined filter 68 | */ 69 | public default VFilter then(VFilter next) { 70 | return x -> next.get(get(x)); 71 | } 72 | 73 | /** 74 | * Combine two VFilters by adding their results together 75 | * 76 | * @param other other VFilter to and with this one 77 | * @return the resulting VFilter after the and 78 | */ 79 | public default VFilter add(VFilter other) { 80 | return x -> get(x).add(other.get(x)); 81 | } 82 | 83 | /** 84 | * Combine two VFilters by subtracting their results together 85 | * 86 | * @param other other VFilter to or with this one 87 | * @return the resulting VFilter after the or 88 | */ 89 | public default VFilter sub(VFilter other) { 90 | return x -> get(x).sub(other.get(x)); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/vectors/filters/VFilterGroup.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.vectors.filters; 6 | 7 | import com.stuypulse.stuylib.math.Vector2D; 8 | 9 | /** 10 | * A class that lets you combine multiple vector filters into one vector filter. 11 | * 12 | * @author Sam (sam.belliveau@gmail.com) 13 | */ 14 | public class VFilterGroup implements VFilter { 15 | 16 | // Array of all the filters 17 | private final VFilter[] mFilters; 18 | 19 | /** 20 | * Make VFilterGroup out of an array of Filters 21 | * 22 | * @param filters list of filters to combine 23 | */ 24 | public VFilterGroup(VFilter... filters) { 25 | mFilters = filters; 26 | } 27 | 28 | public Vector2D get(Vector2D next) { 29 | // Put next through each of the filters 30 | for (VFilter filter : mFilters) { 31 | next = filter.get(next); 32 | } 33 | 34 | // Return filtered value 35 | return next; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/vectors/filters/VHighPassFilter.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.vectors.filters; 6 | 7 | import com.stuypulse.stuylib.streams.numbers.filters.HighPassFilter; 8 | 9 | /** 10 | * A filter that applies a HighPassFilter to a VStream 11 | * 12 | * @author Sam (sam.belliveau@gmail.com) 13 | */ 14 | public class VHighPassFilter extends XYFilter { 15 | 16 | public VHighPassFilter(Number rc) { 17 | super(new HighPassFilter(rc), new HighPassFilter(rc)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/vectors/filters/VLowPassFilter.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.vectors.filters; 6 | 7 | import com.stuypulse.stuylib.streams.numbers.filters.LowPassFilter; 8 | 9 | /** 10 | * A filter that applies a LowPassFilter to a VStream 11 | * 12 | * @author Sam (sam.belliveau@gmail.com) 13 | */ 14 | public class VLowPassFilter extends XYFilter { 15 | 16 | public VLowPassFilter(Number rc) { 17 | super(new LowPassFilter(rc), new LowPassFilter(rc)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/vectors/filters/VMotionProfile.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.vectors.filters; 6 | 7 | import com.stuypulse.stuylib.math.Vector2D; 8 | import com.stuypulse.stuylib.util.StopWatch; 9 | 10 | /** 11 | * A filter, that when applied to the input of a motor, will profile it. Similar to the way in which 12 | * motion profiling can limit the amount of acceleration and jerk in an S-Curve, this can do that to 13 | * real time input. Because this will add a delay, it is recommended that the accelLimit is as high 14 | * as possible. Aside from the accelLimit, this is identical to a SlewRateLimiter or TimedRateLimit. 15 | * 16 | * @author Sam (sam.belliveau@gmail.com) 17 | */ 18 | public class VMotionProfile implements VFilter { 19 | 20 | // Default number of times to apply filter (helps accuracy) 21 | private static final int kDefaultSteps = 64; 22 | 23 | // Stopwatch to Track dt 24 | private StopWatch mTimer; 25 | 26 | // Limits for each of the derivatives 27 | private Number mVelLimit; 28 | private Number mAccelLimit; 29 | 30 | // The last output / acceleration 31 | private Vector2D mOutput; 32 | private Vector2D mAccel; 33 | 34 | // Number of times to apply filter (helps accuracy) 35 | private final int mSteps; 36 | 37 | /** 38 | * @param velLimit maximum change in displacement per second (u/s) 39 | * @param accelLimit maximum change in velocity per second (u/s/s) 40 | * @param steps number of times to apply filter (improves accuracy) 41 | */ 42 | public VMotionProfile(Number velLimit, Number accelLimit, int steps) { 43 | mTimer = new StopWatch(); 44 | 45 | mVelLimit = velLimit; 46 | mAccelLimit = accelLimit; 47 | 48 | mOutput = Vector2D.kOrigin; 49 | mAccel = Vector2D.kOrigin; 50 | 51 | mSteps = steps; 52 | } 53 | 54 | /** 55 | * @param velLimit maximum change in velocity per second (u/s) 56 | * @param accelLimit maximum change in acceleration per second (u/s/s) 57 | */ 58 | public VMotionProfile(Number velLimit, Number accelLimit) { 59 | this(velLimit, accelLimit, kDefaultSteps); 60 | } 61 | 62 | public Vector2D get(Vector2D target) { 63 | double dt = mTimer.reset() / mSteps; 64 | 65 | for (int i = 0; i < mSteps; ++i) { 66 | // if there is a jerk limit, limit the amount the acceleration can change 67 | if (0 < mAccelLimit.doubleValue()) { 68 | // amount of windup in system (how long it would take to slow down) 69 | double windup = mAccel.magnitude() / mAccelLimit.doubleValue(); 70 | 71 | // If the windup is too small, just use normal algorithm to limit acceleration 72 | if (windup < dt) { 73 | // Calculate acceleration needed to reach target 74 | Vector2D accel = target.sub(mOutput).div(dt).sub(mAccel); 75 | 76 | // Try to reach it while abiding by jerklimit 77 | mAccel = mAccel.add(accel.clamp(dt * mAccelLimit.doubleValue())); 78 | } else { 79 | // the position it would end up if it attempted to come to a full stop 80 | Vector2D windA = 81 | mAccel.mul(0.5 * (dt + windup)); // windup caused by acceleration 82 | Vector2D future = mOutput.add(windA); // where the robot will end up 83 | 84 | // Calculate acceleration needed to come to stop at target throughout windup 85 | Vector2D accel = target.sub(future).div(windup); 86 | 87 | // Try to reach it while abiding by jerklimit 88 | mAccel = mAccel.add(accel.clamp(dt * mAccelLimit.doubleValue())); 89 | } 90 | 91 | } else { 92 | // make the acceleration the difference between target and current 93 | mAccel = target.sub(mOutput).div(dt); 94 | } 95 | 96 | // if there is an acceleration limit, limit the acceleration 97 | if (0 < mVelLimit.doubleValue()) { 98 | mAccel = mAccel.clamp(mVelLimit.doubleValue()); 99 | } 100 | 101 | // adjust output by calculated acceleration 102 | mOutput = mOutput.add(mAccel.mul(dt)); 103 | } 104 | 105 | return mOutput; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/vectors/filters/VRateLimit.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.vectors.filters; 6 | 7 | import com.stuypulse.stuylib.math.Vector2D; 8 | import com.stuypulse.stuylib.util.StopWatch; 9 | 10 | /** 11 | * A filter that limits the amount that a Vector2D can change by per second. 12 | * 13 | * @author Sam (sam.belliveau@gmail.com) 14 | */ 15 | public class VRateLimit implements VFilter { 16 | 17 | private final StopWatch mTimer; 18 | private final Number mRateLimit; 19 | 20 | private Vector2D mLastValue; 21 | 22 | public VRateLimit(Number rateLimit) { 23 | mTimer = new StopWatch(); 24 | mRateLimit = rateLimit; 25 | 26 | mLastValue = Vector2D.kOrigin; 27 | } 28 | 29 | public Vector2D get(Vector2D next) { 30 | final double dt = mTimer.reset(); 31 | return mLastValue = 32 | mLastValue.add(next.sub(mLastValue).clamp(dt * mRateLimit.doubleValue())); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/vectors/filters/VTimedMovingAverage.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.vectors.filters; 6 | 7 | import com.stuypulse.stuylib.streams.numbers.filters.TimedMovingAverage; 8 | 9 | /** 10 | * A filter that takes a timed moving average of a VStream 11 | * 12 | * @author Sam (sam.belliveau@gmail.com) 13 | */ 14 | public class VTimedMovingAverage extends XYFilter { 15 | 16 | public VTimedMovingAverage(Number time) { 17 | super(new TimedMovingAverage(time), new TimedMovingAverage(time)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/streams/vectors/filters/XYFilter.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.streams.vectors.filters; 6 | 7 | import com.stuypulse.stuylib.math.Vector2D; 8 | import com.stuypulse.stuylib.streams.numbers.filters.IFilter; 9 | 10 | /** 11 | * A filter that applies separate IFilters to the x and y component of a VFilter 12 | * 13 | * @author Sam (sam.belliveau@gmail.com) 14 | */ 15 | public class XYFilter implements VFilter { 16 | 17 | private final IFilter mXFilter; 18 | private final IFilter mYFilter; 19 | 20 | public XYFilter(IFilter xFilter, IFilter yFilter) { 21 | mXFilter = xFilter; 22 | mYFilter = yFilter; 23 | } 24 | 25 | public Vector2D get(Vector2D next) { 26 | return new Vector2D(mXFilter.get(next.x), mYFilter.get(next.y)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/util/AngleVelocity.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.util; 6 | 7 | import com.stuypulse.stuylib.math.Angle; 8 | 9 | public class AngleVelocity { 10 | 11 | private final StopWatch mTimer; 12 | private Angle mPreviousAngle; 13 | 14 | public AngleVelocity() { 15 | mTimer = new StopWatch(); 16 | mPreviousAngle = Angle.kZero; 17 | } 18 | 19 | public double get(Angle angle) { 20 | double velocity = angle.velocityRadians(mPreviousAngle, mTimer.reset()); 21 | mPreviousAngle = angle; 22 | return velocity; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/util/Conversion.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.util; 6 | 7 | import java.util.function.Function; 8 | 9 | /** 10 | * Represents a conversion from a type to another type. 11 | * 12 | *

Useful when wanting to package two related type/unit conversions functions together. 13 | * 14 | * @author Myles Pasetsky 15 | */ 16 | public interface Conversion { 17 | static Conversion make(Function to, Function from) { 18 | return new Conversion() { 19 | public Out to(In value) { 20 | return to.apply(value); 21 | } 22 | 23 | public In from(Out value) { 24 | return from.apply(value); 25 | } 26 | }; 27 | } 28 | 29 | default Conversion invert() { 30 | return make(this::from, this::to); 31 | } 32 | 33 | To to(From value); 34 | 35 | From from(To value); 36 | } 37 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/util/StopWatch.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 StuyPulse Robotics. All rights reserved. */ 2 | /* This work is licensed under the terms of the MIT license */ 3 | /* found in the root directory of this project. */ 4 | 5 | package com.stuypulse.stuylib.util; 6 | 7 | import edu.wpi.first.wpilibj.RobotController; 8 | 9 | /** 10 | * This StopWatch class helps classes who want their functions to be time independent do that by 11 | * giving them an easy way to get intervals of time. 12 | * 13 | *

This is better than just doing it in the class because it stores the time as a long to keep 14 | * accuracy, but converts it into a double for convenience. 15 | * 16 | * @author Sam (sam.belliveau@gmail.com) 17 | */ 18 | public class StopWatch { 19 | 20 | private TimeEngine mEngine; 21 | private long mLastTime; 22 | 23 | /** 24 | * Creates timer and reset it to now. 25 | * 26 | * @param engine the method that is used to get the current time 27 | */ 28 | public StopWatch(TimeEngine engine) { 29 | mEngine = engine; 30 | mLastTime = mEngine.getRawTime(); 31 | } 32 | 33 | /** Creates timer and reset it to now. */ 34 | public StopWatch() { 35 | this(kDefaultEngine); 36 | } 37 | 38 | /** 39 | * Resets the stop watch to the current time and returns time since last reset. 40 | * 41 | * @return the time since the last reset was called in seconds. The result is always a non 0 42 | * positive number. 43 | */ 44 | public double reset() { 45 | long time = mEngine.getRawTime(); 46 | long delta = Math.max(1, time - mLastTime); 47 | mLastTime = time; 48 | return mEngine.toSeconds(delta); 49 | } 50 | 51 | /** 52 | * Gets the time since the stop watch was reset 53 | * 54 | * @return the time since the last reset was called in seconds. The result is always a non 0 55 | * positive number. 56 | */ 57 | public double getTime() { 58 | long delta = Math.max(1, mEngine.getRawTime() - mLastTime); 59 | return mEngine.toSeconds(delta); 60 | } 61 | 62 | // Engine interface used to get the current time 63 | private interface TimeEngine { 64 | 65 | // Get the raw time as a long 66 | public long getRawTime(); 67 | 68 | // Convert the integer time into a double 69 | public double toSeconds(long raw); 70 | } 71 | 72 | /** This engine is used to get the current time with the system function System.nanoTime() */ 73 | public static final TimeEngine kNanoEngine = 74 | new TimeEngine() { 75 | 76 | public long getRawTime() { 77 | return System.nanoTime(); 78 | } 79 | 80 | public double toSeconds(long raw) { 81 | return raw / 1_000_000_000.0; 82 | } 83 | }; 84 | 85 | /** 86 | * This engine is used to get the current time with the system function 87 | * System.currentTimeMillis() 88 | * 89 | *

This may have a lower resolution, but it is stable. 90 | */ 91 | public static final TimeEngine kMillisEngine = 92 | new TimeEngine() { 93 | 94 | public long getRawTime() { 95 | return System.currentTimeMillis(); 96 | } 97 | 98 | public double toSeconds(long raw) { 99 | return raw / 1_000.0; 100 | } 101 | }; 102 | 103 | /** 104 | * This engine is used to get the current time with the WPI function Timer.getFPGATimestamp() 105 | * 106 | *

This is more accurate when used on a real robot. 107 | */ 108 | public static final TimeEngine kFPGAEngine = 109 | new TimeEngine() { 110 | 111 | public long getRawTime() { 112 | return RobotController.getFPGATime(); 113 | } 114 | 115 | public double toSeconds(long raw) { 116 | return raw / 1_000_000.0; 117 | } 118 | }; 119 | 120 | /** Variable that stores the time engine we want to use by default */ 121 | private static TimeEngine kSelectedDefaultEngine = kNanoEngine; 122 | 123 | /** This engine will return the same values as whatever is selected as the default engine */ 124 | public static final TimeEngine kDefaultEngine = 125 | new TimeEngine() { 126 | public long getRawTime() { 127 | return kSelectedDefaultEngine.getRawTime(); 128 | } 129 | 130 | public double toSeconds(long raw) { 131 | return kSelectedDefaultEngine.toSeconds(raw); 132 | } 133 | }; 134 | 135 | /** 136 | * Set the default engine for the StopWatch 137 | * 138 | *

This will be the engine used by every PID Loop and IFilter 139 | * 140 | * @param defaultEngine the engine that you want to be used by default 141 | */ 142 | public static void setDefaultEngine(TimeEngine defaultEngine) { 143 | kSelectedDefaultEngine = defaultEngine; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/com/stuypulse/stuylib/util/readme.md: -------------------------------------------------------------------------------- 1 | # StuyLib Misc Utils 2 | 3 | WIP... 4 | -------------------------------------------------------------------------------- /vendordeps/WPILibNewCommands.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileName": "WPILibNewCommands.json", 3 | "name": "WPILib-New-Commands", 4 | "version": "1.0.0", 5 | "uuid": "111e20f7-815e-48f8-9dd6-e675ce75b266", 6 | "frcYear": "2025", 7 | "mavenUrls": [], 8 | "jsonUrl": "", 9 | "javaDependencies": [ 10 | { 11 | "groupId": "edu.wpi.first.wpilibNewCommands", 12 | "artifactId": "wpilibNewCommands-java", 13 | "version": "wpilib" 14 | } 15 | ], 16 | "jniDependencies": [], 17 | "cppDependencies": [ 18 | { 19 | "groupId": "edu.wpi.first.wpilibNewCommands", 20 | "artifactId": "wpilibNewCommands-cpp", 21 | "version": "wpilib", 22 | "libName": "wpilibNewCommands", 23 | "headerClassifier": "headers", 24 | "sourcesClassifier": "sources", 25 | "sharedLibrary": true, 26 | "skipInvalidPlatforms": true, 27 | "binaryPlatforms": [ 28 | "linuxathena", 29 | "linuxarm32", 30 | "linuxarm64", 31 | "windowsx86-64", 32 | "windowsx86", 33 | "linuxx86-64", 34 | "osxuniversal" 35 | ] 36 | } 37 | ] 38 | } 39 | --------------------------------------------------------------------------------