diff --git a/.vscode/settings.json b/.vscode/settings.json index a61d0487..d0825d49 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -58,5 +58,6 @@ "edu.wpi.first.math.**.struct.*", ], "java.compile.nullAnalysis.mode": "automatic", - "java.debug.settings.onBuildFailureProceed": true + "java.debug.settings.onBuildFailureProceed": true, + "java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx8G -Xms100m -Xlog:disable" } diff --git a/advantagescope-custom-assets/README.txt b/advantagescope-custom-assets/README.txt new file mode 100644 index 00000000..b7a6d125 --- /dev/null +++ b/advantagescope-custom-assets/README.txt @@ -0,0 +1,3 @@ +This folder contains extra assets for the odometry, 3D field, and joystick views. For more details, see the "Custom Fields/Robots/Joysticks" page in the AdvantageScope documentation (available through the documentation tab in the app or the URL below). + +https://docs.advantagescope.org/more-features/custom-assets \ No newline at end of file diff --git a/advantagescope-custom-assets/Robot_FM/config.json b/advantagescope-custom-assets/Robot_FM/config.json new file mode 100644 index 00000000..ebbe19d6 --- /dev/null +++ b/advantagescope-custom-assets/Robot_FM/config.json @@ -0,0 +1,57 @@ +{ + "name": "3847 - FM", + "isFTC": false, + "disableSimplification": false, + "rotations": [ + { + "axis": "x", + "degrees": 90 + }, + { + "axis": "z", + "degrees": 90 + } + ], + "position": [ + 0, + 0, + 0 + ], + "cameras": [], + "components": [ + { + "zeroedRotations": [ + { + "axis": "x", + "degrees": 90 + }, + { + "axis": "z", + "degrees": 90 + } + ], + "zeroedPosition": [ + 0, + 0, + 0 + ] + }, + { + "zeroedRotations": [ + { + "axis": "x", + "degrees": 90 + }, + { + "axis": "z", + "degrees": 90 + } + ], + "zeroedPosition": [ + 0, + 0, + 0 + ] + } + ] +} \ No newline at end of file diff --git a/advantagescope-custom-assets/Robot_FM/model_0.glb b/advantagescope-custom-assets/Robot_FM/model_0.glb new file mode 100644 index 00000000..fce57928 Binary files /dev/null and b/advantagescope-custom-assets/Robot_FM/model_0.glb differ diff --git a/advantagescope-custom-assets/Robot_FM/model_1.glb b/advantagescope-custom-assets/Robot_FM/model_1.glb new file mode 100644 index 00000000..144bf450 Binary files /dev/null and b/advantagescope-custom-assets/Robot_FM/model_1.glb differ diff --git a/advantagescope-custom-assets/Robot_PM/config.json b/advantagescope-custom-assets/Robot_PM/config.json new file mode 100644 index 00000000..d170d216 --- /dev/null +++ b/advantagescope-custom-assets/Robot_PM/config.json @@ -0,0 +1,54 @@ +{ + "name": "3847 - PM", + "isFTC": false, + "disableSimplification": false, + "rotations": [{ + "axis": "x", + "degrees": 90 + }, { + "axis": "z", + "degrees": 90 + }], + "position": [ + 0, + 0, + 0 + ], + "cameras": [], + "components": [ + { + "zeroedRotations": [ + { + "axis": "x", + "degrees": 90 + }, + { + "axis": "z", + "degrees": 90 + } + ], + "zeroedPosition": [ + 0, + 0, + 0 + ] + }, + { + "zeroedRotations": [ + { + "axis": "x", + "degrees": 90 + }, + { + "axis": "z", + "degrees": 90 + } + ], + "zeroedPosition": [ + 0, + 0, + 0 + ] + } + ] +} \ No newline at end of file diff --git a/advantagescope-custom-assets/Robot_PM/model_0.glb b/advantagescope-custom-assets/Robot_PM/model_0.glb new file mode 100644 index 00000000..2850e34e Binary files /dev/null and b/advantagescope-custom-assets/Robot_PM/model_0.glb differ diff --git a/advantagescope-custom-assets/Robot_PM/model_1.glb b/advantagescope-custom-assets/Robot_PM/model_1.glb new file mode 100644 index 00000000..a4c54061 Binary files /dev/null and b/advantagescope-custom-assets/Robot_PM/model_1.glb differ diff --git a/simgui-ds.json b/simgui-ds.json index aca93797..4d17e032 100644 --- a/simgui-ds.json +++ b/simgui-ds.json @@ -1,9 +1,4 @@ { - "System Joysticks": { - "window": { - "enabled": false - } - }, "keyboardJoysticks": [ { "axisConfig": [ @@ -98,11 +93,11 @@ ], "robotJoysticks": [ { - "guid": "78696e70757401000000000000000000", - "useGamepad": true + "guid": "Keyboard0" }, { "useGamepad": true } - ] + ], + "useEnableDisableHotkeys": true } diff --git a/simgui-window.json b/simgui-window.json index dd8ae2d5..6c9eca77 100644 --- a/simgui-window.json +++ b/simgui-window.json @@ -1,11 +1,11 @@ { "Docking": { "Data": [ - "DockNode ID=0x00000001 Pos=6,1072 Size=979,323 Selected=0x48913727", - "DockNode ID=0x00000002 Pos=995,825 Size=1237,566 Split=X", - "DockNode ID=0x00000003 Parent=0x00000002 SizeRef=625,413 Selected=0xFA2A8FFA", - "DockNode ID=0x00000004 Parent=0x00000002 SizeRef=610,413 Selected=0xA6A62DF3", - "DockNode ID=0x00000005 Pos=2,21 Size=2242,790 Split=X", + "DockNode ID=0x00000001 Pos=4,942 Size=983,285 Selected=0x48913727", + "DockNode ID=0x00000003 Pos=983,443 Size=877,514 Split=X", + "DockNode ID=0x00000004 Parent=0x00000003 SizeRef=238,497 Selected=0xFA2A8FFA", + "DockNode ID=0x00000006 Parent=0x00000003 SizeRef=235,497 Selected=0xA6A62DF3", + "DockNode ID=0x00000005 Pos=9,29 Size=2242,790 Split=X", "DockNode ID=0x00000011 Parent=0x00000005 SizeRef=1872,784 Split=X", "DockNode ID=0x00000008 Parent=0x00000011 SizeRef=322,445 Split=Y Selected=0x39AD1970", "DockNode ID=0x0000000A Parent=0x00000008 SizeRef=187,370 Split=Y Selected=0x39AD1970", @@ -24,13 +24,13 @@ "GLOBAL": { "font": "Proggy Dotted", "fps": "120", - "height": "1415", + "height": "974", "maximized": "1", "style": "0", "userScale": "2", - "width": "2256", + "width": "1920", "xpos": "0", - "ypos": "29" + "ypos": "34" } }, "Table": { @@ -55,63 +55,63 @@ "###/SmartDashboard/Alerts": { "Collapsed": "0", "DockId": "0x00000013,0", - "Pos": "1876,21", + "Pos": "1883,29", "Size": "368,183" }, "###/SmartDashboard/Auto Chooser": { "Collapsed": "0", "DockId": "0x0000000E,0", - "Pos": "2,414", + "Pos": "9,422", "Size": "322,78" }, "###/SmartDashboard/Field2d": { "Collapsed": "0", "DockId": "0x00000009,0", - "Pos": "326,21", + "Pos": "333,29", "Size": "1548,790" }, "###/SmartDashboard/Scheduler": { "Collapsed": "0", "DockId": "0x00000014,0", - "Pos": "1876,206", + "Pos": "1883,214", "Size": "368,605" }, "###/SmartDashboard/Sim/LeftView": { "Collapsed": "0", - "DockId": "0x00000004,0", - "Pos": "1622,825", - "Size": "610,566" + "DockId": "0x00000006,0", + "Pos": "1425,443", + "Size": "435,514" }, "###/SmartDashboard/Sim/TopView": { "Collapsed": "0", - "DockId": "0x00000003,0", - "Pos": "995,825", - "Size": "625,566" + "DockId": "0x00000004,0", + "Pos": "983,443", + "Size": "440,514" }, "###/SmartDashboard/VisionSystemSim-main/Sim Field": { "Collapsed": "0", - "Pos": "1080,167", - "Size": "500,250" + "Pos": "937,167", + "Size": "513,250" }, "###Addressable LEDs": { "Collapsed": "0", "Pos": "290,100", - "Size": "197,54" + "Size": "229,60" }, "###FMS": { "Collapsed": "0", "DockId": "0x0000000C,0", - "Pos": "2,21", - "Size": "322,391" + "Pos": "9,29", + "Size": "322,230" }, "###Joysticks": { "Collapsed": "0", - "Pos": "6,824", - "Size": "976,102" + "Pos": "26,752", + "Size": "1156,202" }, "###Keyboard 0 Settings": { "Collapsed": "0", - "Pos": "10,50", + "Pos": "502,141", "Size": "300,560" }, "###Keyboard 1 Settings": { @@ -122,25 +122,25 @@ "###NetworkTables": { "Collapsed": "0", "DockId": "0x00000001,0", - "Pos": "6,1072", - "Size": "979,323" + "Pos": "4,942", + "Size": "983,285" }, "###NetworkTables Info": { "Collapsed": "0", "DockId": "0x00000001,1", - "Pos": "6,1072", - "Size": "979,323" + "Pos": "4,942", + "Size": "983,285" }, "###System Joysticks": { "Collapsed": "0", "DockId": "0x00000001,2", - "Pos": "27,1035", - "Size": "935,312" + "Pos": "4,942", + "Size": "983,285" }, "###Timing": { "Collapsed": "0", "DockId": "0x0000000F,0", - "Pos": "2,494", + "Pos": "9,502", "Size": "322,317" }, "Debug##Default": { @@ -151,8 +151,8 @@ "Robot State": { "Collapsed": "0", "DockId": "0x0000000D,0", - "Pos": "2,253", - "Size": "32,38" + "Pos": "9,261", + "Size": "322,159" } } } diff --git a/src/main/deploy/pathplanner/autos/TBTB Full.auto b/src/main/deploy/pathplanner/autos/TBTB Full.auto index 97bb18c1..e7a74121 100644 --- a/src/main/deploy/pathplanner/autos/TBTB Full.auto +++ b/src/main/deploy/pathplanner/autos/TBTB Full.auto @@ -15,6 +15,12 @@ "data": { "pathName": "1st-TBTB 2" } + }, + { + "type": "path", + "data": { + "pathName": "1st-TBTB 3" + } } ] } diff --git a/src/main/java/edu/wpi/first/wpilibj2/command/button/README.md b/src/main/java/edu/wpi/first/wpilibj2/command/button/README.md deleted file mode 100644 index 08512bb9..00000000 --- a/src/main/java/edu/wpi/first/wpilibj2/command/button/README.md +++ /dev/null @@ -1,5 +0,0 @@ -This file is changed to allow us to modify the WPILib Trigger Class - -* We add overloaded and() and or() methods that can take any number of arguments -* We changed the initial state of the triggers to be true or false based on the binding, so they can immediately run if the condition is met. This is important for having triggers for each robot state (disabled, teleop, etc.) -* To deploy this to a robot you have to edit the build.gradle file -> The Jar duplicateStrategy should be set to "duplicatesStrategy = DuplicatesStrategy.EXCLUDE" diff --git a/src/main/java/edu/wpi/first/wpilibj2/command/button/Trigger.java b/src/main/java/edu/wpi/first/wpilibj2/command/button/Trigger.java deleted file mode 100644 index ac19bf9c..00000000 --- a/src/main/java/edu/wpi/first/wpilibj2/command/button/Trigger.java +++ /dev/null @@ -1,539 +0,0 @@ -// Copyright (c) FIRST and other WPILib contributors. -// Open Source Software; you can modify and/or share it under the terms of -// the WPILib BSD license file in the root directory of this project. - -package edu.wpi.first.wpilibj2.command.button; - -import static edu.wpi.first.util.ErrorMessages.requireNonNullParam; - -import edu.wpi.first.math.filter.Debouncer; -import edu.wpi.first.wpilibj.event.EventLoop; -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.CommandScheduler; -import java.util.function.BooleanSupplier; - -/** - * This class provides an easy way to link commands to conditions. - * - *

It is very easy to link a button to a command. For instance, you could link the trigger button - * of a joystick to a "score" command. - * - *

Triggers can easily be composed for advanced functionality using the {@link - * #and(BooleanSupplier)}, {@link #or(BooleanSupplier)}, {@link #negate()} operators. - * - *

This class is provided by the NewCommands VendorDep - * - *

Spectrum modified in Fall 2024 to allow triggers to default start condition of false, so if - * something is already true when bound it will activate the trigger. We needed this for a trigger - * to activate only if Teleop was enabled. - */ -public class Trigger implements BooleanSupplier { - private final BooleanSupplier m_condition; - private final EventLoop m_loop; - public static final Trigger kFalse = new Trigger(() -> false); - public static final Trigger kTrue = new Trigger(() -> true); - - /** - * Creates a new trigger based on the given condition. - * - * @param loop The loop instance that polls this trigger. - * @param condition the condition represented by this trigger - */ - public Trigger(EventLoop loop, BooleanSupplier condition) { - m_loop = requireNonNullParam(loop, "loop", "Trigger"); - m_condition = requireNonNullParam(condition, "condition", "Trigger"); - } - - /** - * Creates a new trigger based on the given condition. - * - *

Polled by the default scheduler button loop. - * - * @param condition the condition represented by this trigger - */ - public Trigger(BooleanSupplier condition) { - this(CommandScheduler.getInstance().getDefaultButtonLoop(), condition); - } - - /** - * Starts the command when the condition changes. - * - * @param command the command to start - * @return this trigger, so calls can be chained - */ - public Trigger onChange(Command command) { - requireNonNullParam(command, "command", "onChange"); - m_loop.bind( - new Runnable() { - private boolean m_pressedLast = m_condition.getAsBoolean(); - - @Override - public void run() { - boolean pressed = m_condition.getAsBoolean(); - - if (m_pressedLast != pressed) { - CommandScheduler.getInstance().schedule(command); - } - - m_pressedLast = pressed; - } - }); - return this; - } - - /** - * Starts the given command whenever the condition changes from `false` to `true`. - * - * @param command the command to start - * @return this trigger, so calls can be chained - */ - public Trigger onTrue(Command command) { - requireNonNullParam(command, "command", "onTrue"); - m_loop.bind( - new Runnable() { - private boolean m_pressedLast = false; - - @Override - public void run() { - boolean pressed = m_condition.getAsBoolean(); - - if (!m_pressedLast && pressed) { - CommandScheduler.getInstance().schedule(command); - } - - m_pressedLast = pressed; - } - }); - return this; - } - - /** - * Starts the given commands whenever the condition changes from `false` to `true`. - * - * @param commands the commands to start - * @return this trigger, so calls can be chained - */ - public Trigger onTrue(Command... commands) { - requireNonNullParam(commands, "command", "onTrue"); - m_loop.bind( - new Runnable() { - private boolean m_pressedLast = false; - - @Override - public void run() { - boolean pressed = m_condition.getAsBoolean(); - - if (!m_pressedLast && pressed) { - for (Command command : commands) { - CommandScheduler.getInstance().schedule(command); - } - } - - m_pressedLast = pressed; - } - }); - return this; - } - - /** - * Starts the given command whenever the condition changes from `true` to `false`. - * - * @param command the command to start - * @return this trigger, so calls can be chained - */ - public Trigger onFalse(Command command) { - requireNonNullParam(command, "command", "onFalse"); - m_loop.bind( - new Runnable() { - private boolean m_pressedLast = true; - - @Override - public void run() { - boolean pressed = m_condition.getAsBoolean(); - - if (m_pressedLast && !pressed) { - CommandScheduler.getInstance().schedule(command); - } - - m_pressedLast = pressed; - } - }); - return this; - } - - public Trigger onFalse(Command... commands) { - requireNonNullParam(commands, "command", "onFalse"); - m_loop.bind( - new Runnable() { - private boolean m_pressedLast = true; - - @Override - public void run() { - boolean pressed = m_condition.getAsBoolean(); - - if (m_pressedLast && !pressed) { - for (Command command : commands) { - CommandScheduler.getInstance().schedule(command); - } - } - - m_pressedLast = pressed; - } - }); - return this; - } - - /** - * Starts the given command whenever the condition changes from `true` to `false`, but has to - * have run once prior - * - * @param command the command to start - * @return this trigger, so calls can be chained - */ - public Trigger onChangeToFalse(Command command) { - requireNonNullParam(command, "command", "onFalse"); - m_loop.bind( - new Runnable() { - private boolean m_pressedLast = false; - - @Override - public void run() { - boolean pressed = m_condition.getAsBoolean(); - - if (m_pressedLast && !pressed) { - CommandScheduler.getInstance().schedule(command); - } - - m_pressedLast = pressed; - } - }); - return this; - } - - /** - * Starts the given command whenever the condition changes from `false` to `true`, but has to - * have run once prior - * - * @param command the command to start - * @return this trigger, so calls can be chained - */ - public Trigger onChangeToTrue(Command command) { - requireNonNullParam(command, "command", "onTrue"); - m_loop.bind( - new Runnable() { - private boolean m_pressedLast = true; - - @Override - public void run() { - boolean pressed = m_condition.getAsBoolean(); - - if (!m_pressedLast && pressed) { - CommandScheduler.getInstance().schedule(command); - } - - m_pressedLast = pressed; - } - }); - return this; - } - - /** - * Starts the given command when the condition changes to `true` and cancels it when the - * condition changes to `false`. - * - *

Doesn't re-start the command if it ends while the condition is still `true`. If the - * command should restart, see {@link edu.wpi.first.wpilibj2.command.RepeatCommand}. - * - * @param command the command to start - * @return this trigger, so calls can be chained - */ - public Trigger whileTrue(Command command) { - requireNonNullParam(command, "command", "whileTrue"); - m_loop.bind( - new Runnable() { - private boolean m_pressedLast = false; - - @Override - public void run() { - boolean pressed = m_condition.getAsBoolean(); - - if (!m_pressedLast && pressed) { - CommandScheduler.getInstance().schedule(command); - } else if (m_pressedLast && !pressed) { - command.cancel(); - } - - m_pressedLast = pressed; - } - }); - return this; - } - - /** - * Starts the given command when the condition changes to `true` and cancels it when the - * condition changes to `false`. - * - *

Doesn't re-start the command if it ends while the condition is still `true`. If the - * command should restart, see {@link edu.wpi.first.wpilibj2.command.RepeatCommand}. - * - * @param commands the commands to start - * @return this trigger, so calls can be chained - */ - public Trigger whileTrue(Command... commands) { - requireNonNullParam(commands, "command", "whileTrue"); - m_loop.bind( - new Runnable() { - private boolean m_pressedLast = false; - - @Override - public void run() { - boolean pressed = m_condition.getAsBoolean(); - - for (Command command : commands) { - if (!m_pressedLast && pressed) { - CommandScheduler.getInstance().schedule(command); - } else if (m_pressedLast && !pressed) { - command.cancel(); - } - } - - m_pressedLast = pressed; - } - }); - return this; - } - - /** - * Starts the given command when the condition changes to `false` and cancels it when the - * condition changes to `true`. - * - *

Doesn't re-start the command if it ends while the condition is still `false`. If the - * command should restart, see {@link edu.wpi.first.wpilibj2.command.RepeatCommand}. - * - * @param command the command to start - * @return this trigger, so calls can be chained - */ - public Trigger whileFalse(Command command) { - requireNonNullParam(command, "command", "whileFalse"); - m_loop.bind( - new Runnable() { - private boolean m_pressedLast = true; - - @Override - public void run() { - boolean pressed = m_condition.getAsBoolean(); - - if (m_pressedLast && !pressed) { - CommandScheduler.getInstance().schedule(command); - } else if (!m_pressedLast && pressed) { - command.cancel(); - } - - m_pressedLast = pressed; - } - }); - return this; - } - - public Trigger whileFalse(Command... commands) { - requireNonNullParam(commands, "command", "whileFalse"); - m_loop.bind( - new Runnable() { - private boolean m_pressedLast = true; - - @Override - public void run() { - boolean pressed = m_condition.getAsBoolean(); - - for (Command command : commands) { - if (m_pressedLast && !pressed) { - CommandScheduler.getInstance().schedule(command); - } else if (!m_pressedLast && pressed) { - command.cancel(); - } - } - - m_pressedLast = pressed; - } - }); - return this; - } - - /** - * Toggles a command when the condition changes from `false` to `true`. - * - * @param command the command to toggle - * @return this trigger, so calls can be chained - */ - public Trigger toggleOnTrue(Command command) { - requireNonNullParam(command, "command", "toggleOnTrue"); - m_loop.bind( - new Runnable() { - private boolean m_pressedLast = false; - - @Override - public void run() { - boolean pressed = m_condition.getAsBoolean(); - - if (!m_pressedLast && pressed) { - if (command.isScheduled()) { - command.cancel(); - } else { - CommandScheduler.getInstance().schedule(command); - } - } - - m_pressedLast = pressed; - } - }); - return this; - } - - /** - * Toggles a command when the condition changes from `true` to `false`. - * - * @param command the command to toggle - * @return this trigger, so calls can be chained - */ - public Trigger toggleOnFalse(Command command) { - requireNonNullParam(command, "command", "toggleOnFalse"); - m_loop.bind( - new Runnable() { - private boolean m_pressedLast = true; - - @Override - public void run() { - boolean pressed = m_condition.getAsBoolean(); - - if (m_pressedLast && !pressed) { - if (command.isScheduled()) { - command.cancel(); - } else { - CommandScheduler.getInstance().schedule(command); - } - } - - m_pressedLast = pressed; - } - }); - return this; - } - - /** - * Run a command while true. Also runs a command for a certain timeout when released. - * - * @param runCommand the command to run while true - * @param endCommand the command to run when released - * @param endTimeout the time to run the end command - * @return this trigger, so calls can be chained - */ - public Trigger runWithEndSequence(Command runCommand, Command endCommand, double endTimeout) { - this.whileTrue(runCommand); - this.onFalse(endCommand.withTimeout(endTimeout).withName(endCommand.getName())); - return this; - } - - @Override - public boolean getAsBoolean() { - return m_condition.getAsBoolean(); - } - - /** - * Composes two triggers with logical AND. - * - * @param trigger the condition to compose with - * @return A trigger which is active when both component triggers are active. - */ - public Trigger and(BooleanSupplier trigger) { - return new Trigger(m_loop, () -> m_condition.getAsBoolean() && trigger.getAsBoolean()); - } - - /** - * Combines multiple BooleanSupplier triggers using a logical AND operation. - * - * @param triggers an array of BooleanSupplier triggers to be combined. - * @return a new Trigger that represents the logical AND of all provided triggers. - */ - public Trigger and(BooleanSupplier... triggers) { - Trigger trig = this; - for (BooleanSupplier t : triggers) { - trig = trig.and(t); - } - return trig; - } - - /** - * Combines multiple BooleanSupplier triggers using a logical OR operation. - * - * @param triggers an array of BooleanSupplier triggers to be combined. - * @return a new NewTrigger instance that represents the logical OR of the provided triggers. - */ - public Trigger or(BooleanSupplier... triggers) { - Trigger trig = this; - for (BooleanSupplier t : triggers) { - trig = trig.or(t); - } - return trig; - } - - /** - * Composes two triggers with logical OR. - * - * @param trigger the condition to compose with - * @return A trigger which is active when either component trigger is active. - */ - public Trigger or(BooleanSupplier trigger) { - return new Trigger(m_loop, () -> m_condition.getAsBoolean() || trigger.getAsBoolean()); - } - - /** - * Creates a new trigger that is active when this trigger is inactive, i.e. that acts as the - * negation of this trigger. - * - * @return the negated trigger - */ - public Trigger negate() { - return new Trigger(m_loop, () -> !m_condition.getAsBoolean()); - } - - /** - * renamed negate - * - * @return the negated trigger - */ - public Trigger not() { - return negate(); - } - - /** - * Creates a new debounced trigger from this trigger - it will become active when this trigger - * has been active for longer than the specified period. - * - * @param seconds The debounce period. - * @return The debounced trigger (rising edges debounced only) - */ - public Trigger debounce(double seconds) { - return debounce(seconds, Debouncer.DebounceType.kRising); - } - - /** - * Creates a new debounced trigger from this trigger - it will become active when this trigger - * has been active for longer than the specified period. - * - * @param seconds The debounce period. - * @param type The debounce type. - * @return The debounced trigger. - */ - public Trigger debounce(double seconds, Debouncer.DebounceType type) { - return new Trigger( - m_loop, - new BooleanSupplier() { - final Debouncer m_debouncer = new Debouncer(seconds, type); - - @Override - public boolean getAsBoolean() { - return m_debouncer.calculate(m_condition.getAsBoolean()); - } - }); - } -} diff --git a/src/main/java/frc/rebuilt/Field.java b/src/main/java/frc/rebuilt/Field.java index 6a578163..b1f7fbec 100644 --- a/src/main/java/frc/rebuilt/Field.java +++ b/src/main/java/frc/rebuilt/Field.java @@ -1,9 +1,3 @@ -// Copyright (c) 2025 FRC 6328 -// http://github.com/Mechanical-Advantage -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file at -// the root directory of this project. package frc.rebuilt; import edu.wpi.first.math.geometry.*; @@ -39,7 +33,7 @@ public class Field { new Translation2d(fieldLength - 1, fieldWidth - 1); public static final Translation2d deepFeedBlueLeft = new Translation2d(1, fieldWidth - 2.5); - public static final Translation2d deepFeedBlueRight = new Translation2d(21, 2.5); + public static final Translation2d deepFeedBlueRight = new Translation2d(1, 2.5); public static final Translation2d deepFeedRedLeft = new Translation2d(fieldLength - 1, 2.5); public static final Translation2d deepFeedRedRight = new Translation2d(fieldLength - 1, fieldWidth - 2.5); @@ -166,7 +160,7 @@ public static class BlueTower { public static final double midRungZ = Units.inchesToMeters(45.0); public static final double highRungZ = Units.inchesToMeters(63.0); - public static final double tag31Y = Units.inchesToMeters(fieldWidth / 2); + public static final double tag31Y = fieldWidth / 2.0; // Fixed X location public static final double frontFaceX = Units.inchesToMeters(43.51); @@ -225,19 +219,8 @@ public static double BlueToRed(double translation) { return Field.fieldLength - translation; } - @Getter - public static final Translation3d blueHubCenter = BlueHub.topCenter; // new Translation3d( - // Units.inchesToMeters(182.11), - // Units.inchesToMeters(158.84), - // Units.inchesToMeters(72)); - @Getter - public static final Translation3d redHubCenter = - BlueToRed(BlueHub.topCenter); // new Translation3d( - // Units.inchesToMeters(469.11), - // Units.inchesToMeters(158.84), - // Units.inchesToMeters(72)); - - @Getter private static final double aprilTagWidth = Units.inchesToMeters(6.50); + @Getter public static final Translation3d blueHubCenter = BlueHub.topCenter; + @Getter public static final Translation3d redHubCenter = BlueToRed(BlueHub.topCenter); /** Returns {@code true} if the robot is on the blue alliance. */ public static boolean isBlue() { diff --git a/src/main/java/frc/rebuilt/FieldHelpers.java b/src/main/java/frc/rebuilt/FieldHelpers.java index b9de0245..961eab1d 100644 --- a/src/main/java/frc/rebuilt/FieldHelpers.java +++ b/src/main/java/frc/rebuilt/FieldHelpers.java @@ -47,22 +47,6 @@ public static Pose2d flipIfRed(Pose2d red) { return new Pose2d(flipIfRed(red.getTranslation()), flipAngleIfRed(red.getRotation())); } - public static Translation2d flipIfRedSide(Translation2d red) { - if (Zones.blueFieldSide.getAsBoolean()) { - return red; - } - return new Translation2d(flipX(red.getX()), flipY(red.getY())); - } - - public static Pose2d flipIfRedSide(Pose2d red) { - if (Zones.blueFieldSide.getAsBoolean()) { - return red; - } - return new Pose2d( - flipIfRedSide(new Translation2d(red.getX(), red.getY())), - flipAngle(red.getRotation())); - } - public static double flipX(double xCoordinate) { return Field.fieldLength - xCoordinate; } diff --git a/src/main/java/frc/rebuilt/FuelPhysicsSim.java b/src/main/java/frc/rebuilt/FuelPhysicsSim.java new file mode 100644 index 00000000..91171aec --- /dev/null +++ b/src/main/java/frc/rebuilt/FuelPhysicsSim.java @@ -0,0 +1,2494 @@ +/* + * FuelPhysicsSim.java - Full-field ball physics simulation for FRC 2026 REBUILT + * + * MIT License + * + * Copyright (c) 2026 FRC Team 5962 perSEVERE + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ + +package frc.rebuilt; + +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.math.kinematics.ChassisSpeeds; +import edu.wpi.first.networktables.DoublePublisher; +import edu.wpi.first.networktables.IntegerPublisher; +import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.networktables.StructArrayPublisher; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; + +/** + * Full-field ball physics simulation for FRC 2026 REBUILT. Handles drag, Magnus lift, friction, + * ball-ball collisions, wall bounces, hub scoring, sleeping, CCD, and robot interaction. Single + * file, only depends on WPILib (wpimath + ntcore). Drop it into your sim and watch balls fly. + * + *

Physics: symplectic Euler integration, 3D angular velocity for Magnus (omega x v cross + * product), Coulomb friction with spin transfer, sequential impulse collision solver with warm + * starting and Baumgarte stabilization. Spatial hashing for ball-ball broadphase. Ball sleeping + * keeps 350+ resting balls under 2ms/tick. + * + *

Usage: + * + *

+ *   FuelPhysicsSim ballSim = new FuelPhysicsSim("Sim/Fuel");
+ *   ballSim.enable();
+ *   ballSim.placeFieldBalls();   // spawns all the game pieces
+ *   // in simulationPeriodic():
+ *   ballSim.configureRobot(width, length, bumperH, poseSupplier, speedsSupplier);
+ *   ballSim.tick();              // runs physics, publishes to NT
+ * 
+ */ +public class FuelPhysicsSim { + + // Physics constants + + private static final double GRAVITY = 9.81; // m/s^2 + private static final double AIR_DENSITY = 1.225; // kg/m^3, standard atmosphere + private static final double BALL_MASS = 0.215; // kg, game manual 5.10.1 midpoint + private static final double BALL_DIAMETER = 0.1501; // m, game manual 5.10.1 + private static final double BALL_RADIUS = BALL_DIAMETER / 2.0; + private static final double BALL_CROSS_AREA = Math.PI * BALL_RADIUS * BALL_RADIUS; + private static final double BALL_MOMENT_OF_INERTIA = + 0.4 * BALL_MASS * BALL_RADIUS * BALL_RADIUS; // 2/5 * m * r^2, solid sphere + + // Aerodynamic coefficients + private static final double DEFAULT_CD = 0.47; // drag coefficient, smooth sphere + private static final double DEFAULT_CM = 0.2; // Magnus coefficient, conservative estimate + + // Precomputed force factors (divided by mass to get acceleration factors) + private static final double DRAG_ACCEL_FACTOR = + 0.5 * AIR_DENSITY * DEFAULT_CD * BALL_CROSS_AREA / BALL_MASS; + // Extra BALL_RADIUS factor converts the omega x v cross product to acceleration + private static final double MAGNUS_ACCEL_FACTOR = + 0.5 * AIR_DENSITY * DEFAULT_CM * BALL_CROSS_AREA * BALL_RADIUS / BALL_MASS; + + // Coefficients of restitution (per-material, from field element build instructions) + private static final double COR_CARPET = 0.65; // foam on low-pile carpet + private static final double COR_WALL = + 0.70; // foam on polycarbonate (alliance walls, guardrails) + private static final double COR_STEEL = 0.72; // foam on powder-coated steel (tower, rungs) + private static final double COR_HUB = 0.70; // foam on polycarbonate (hub body panels) + private static final double COR_HDPE = 0.60; // foam on textured HDPE (bump ramps, 15deg) + private static final double COR_NET = 0.15; // mesh fabric absorbs most of the energy + private static final double COR_BUMPER = 0.08; // polycarb-backed foam, nearly inelastic + private static final double COR_BALL_BALL = 0.45; // foam-on-foam, high deformation loss + + // Friction coefficients + private static final double MU_GROUND_KINETIC = 0.3; // kinetic friction on carpet + private static final double MU_GROUND_ROLLING = 0.05; // rolling friction + private static final double MU_WALL = 0.4; // foam on polycarbonate/fabric + private static final double MU_BALL_BALL = 0.3; // foam on foam + + // Velocity-dependent COR reference speed + private static final double COR_VREF = 3.0; // m/s + private static final double COR_EXPONENT = 0.15; + + // Reusable axis-aligned unit normals (avoids allocating these in hot loops) + private static final Translation3d AXIS_X_POS = new Translation3d(1, 0, 0); + private static final Translation3d AXIS_X_NEG = new Translation3d(-1, 0, 0); + private static final Translation3d AXIS_Y_POS = new Translation3d(0, 1, 0); + private static final Translation3d AXIS_Y_NEG = new Translation3d(0, -1, 0); + private static final Translation3d AXIS_Z_POS = new Translation3d(0, 0, 1); + private static final Translation3d AXIS_Z_NEG = new Translation3d(0, 0, -1); + + // Period + private static final double PERIOD = 0.02; // 20ms + + // Field geometry + + private static final double FIELD_LENGTH = 16.541; // m + private static final double FIELD_WIDTH = 8.052; // m + + // Alliance wall and guardrail heights + private static final double ALLIANCE_WALL_HEIGHT = 0.935; // 36.8 in + private static final double GUARDRAIL_HEIGHT = 0.508; // 20 in + + // Bump geometry (tent-shaped, 15-degree ramps) + private static final double BUMP_HEIGHT = 0.165; // m, 6.513 in + + // Hub positions (from game manual field layout) + private static final Translation2d BLUE_HUB_CENTER = new Translation2d(4.5974, 4.035); + private static final Translation2d RED_HUB_CENTER = new Translation2d(11.938, 4.035); + + // Hub dimensions + private static final double HUB_ENTRY_HEIGHT = 1.829; // m, 72 in + private static final double HUB_ENTRY_RADIUS = 0.5295; // m, 41.7 in / 2 across flats + private static final double HUB_SIDE = 1.194; // m, 47 in square base + // Hub base structure height for ball collision (not a solid wall to scoring height, + // just the base frame that deflects ground-level balls) + private static final double HUB_BASE_HEIGHT = 0.5; // m, ~20 in base frame + + // Net dimensions + private static final double NET_HEIGHT_MIN = 1.5; // m + private static final double NET_HEIGHT_MAX = 3.057; // m + private static final double NET_WIDTH = 1.484; // m + private static final double NET_OFFSET = HUB_SIDE / 2.0 + 0.261; + + // Trench geometry + private static final double TRENCH_WIDTH = 1.265; // m + private static final double TRENCH_BLOCK_WIDTH = 0.305; // m, 12 in + private static final double TRENCH_HEIGHT = 0.565; // m, 22.25 in underpass + private static final double TRENCH_PILLAR_HEIGHT = 1.346; // m, 53 in + + // Tower geometry + private static final double TOWER_POLE_WIDTH = 0.051; // m, 2 in + private static final double TOWER_POLE_HEIGHT = 1.194; // m, 47 in + private static final double TOWER_UPRIGHT_THICK = 0.038; // m, 1.5 in + private static final double TOWER_UPRIGHT_DEPTH = 0.089; // m, 3.5 in + private static final double TOWER_UPRIGHT_HEIGHT = 1.831; // m, 72.1 in + private static final double TOWER_UPRIGHT_SPACING = 0.819; // m, 32.25 in center-to-center + + // Tower rung geometry (Schedule 40 pipe) + private static final double RUNG_OUTER_DIAMETER = 0.042; // m, 1.66 in OD + private static final double RUNG_RADIUS = RUNG_OUTER_DIAMETER / 2.0; + private static final double RUNG_OVERHANG = 0.149; // m, 5.875 in past upright + private static final double RUNG_LOW_HEIGHT = 0.686; // m, 27 in + private static final double RUNG_MID_HEIGHT = 1.143; // m, 45 in + private static final double RUNG_HIGH_HEIGHT = 1.600; // m, 63 in + + // Tower bracing + private static final double BRACING_BOTTOM = 0.721; // m, 28.4 in + private static final double BRACING_TOP = 1.102; // m, 43.4 in + + // Hub ramp area + private static final double HUB_RAMP_WIDTH = 1.194; // m, 47 in + private static final double HUB_RAMP_LENGTH = 5.512; // m, 217 in + private static final double HUB_RAMP_HEIGHT = 0.165; // m, 6.5 in + + // Spatial hash + private static final double CELL_SIZE = 0.25; + private static final int GRID_COLS = (int) Math.ceil(FIELD_LENGTH / CELL_SIZE); + private static final int GRID_ROWS = (int) Math.ceil(FIELD_WIDTH / CELL_SIZE); + + // Max balls + private static final int MAX_BALLS = 2000; + + // Bump ramp segments (XZ line segments extruded along Y) + + private static final BumpSegment[] BUMP_SEGMENTS = { + // Blue lower bump ramp up/down + new BumpSegment(3.96, 0, 4.524, BUMP_HEIGHT, 1.88, 3.73), + new BumpSegment(4.524, BUMP_HEIGHT, 5.088, 0, 1.88, 3.73), + // Blue upper bump ramp up/down + new BumpSegment(3.96, 0, 4.524, BUMP_HEIGHT, 4.32, 6.17), + new BumpSegment(4.524, BUMP_HEIGHT, 5.088, 0, 4.32, 6.17), + // Red lower bump ramp up/down + new BumpSegment(FIELD_LENGTH - 5.088, 0, FIELD_LENGTH - 4.524, BUMP_HEIGHT, 1.88, 3.73), + new BumpSegment(FIELD_LENGTH - 4.524, BUMP_HEIGHT, FIELD_LENGTH - 3.96, 0, 1.88, 3.73), + // Red upper bump ramp up/down + new BumpSegment(FIELD_LENGTH - 5.088, 0, FIELD_LENGTH - 4.524, BUMP_HEIGHT, 4.32, 6.17), + new BumpSegment(FIELD_LENGTH - 4.524, BUMP_HEIGHT, FIELD_LENGTH - 3.96, 0, 4.32, 6.17), + }; + + // AABB obstacles + + private static final AABB[] AABB_OBSTACLES; + + static { + List aabbs = new ArrayList<>(); + + // Trench pillars (4): 12in wide x 53in tall, steel structure + aabbs.add( + new AABB( + 3.96, + TRENCH_WIDTH, + 0, + 3.96 + TRENCH_BLOCK_WIDTH, + TRENCH_WIDTH + TRENCH_BLOCK_WIDTH, + TRENCH_PILLAR_HEIGHT, + COR_STEEL)); + aabbs.add( + new AABB( + 3.96, + FIELD_WIDTH - 1.57 - TRENCH_BLOCK_WIDTH, + 0, + 3.96 + TRENCH_BLOCK_WIDTH, + FIELD_WIDTH - 1.57, + TRENCH_PILLAR_HEIGHT, + COR_STEEL)); + aabbs.add( + new AABB( + FIELD_LENGTH - 3.96 - TRENCH_BLOCK_WIDTH, + TRENCH_WIDTH, + 0, + FIELD_LENGTH - 3.96, + TRENCH_WIDTH + TRENCH_BLOCK_WIDTH, + TRENCH_PILLAR_HEIGHT, + COR_STEEL)); + aabbs.add( + new AABB( + FIELD_LENGTH - 3.96 - TRENCH_BLOCK_WIDTH, + FIELD_WIDTH - 1.57 - TRENCH_BLOCK_WIDTH, + 0, + FIELD_LENGTH - 3.96, + FIELD_WIDTH - 1.57, + TRENCH_PILLAR_HEIGHT, + COR_STEEL)); + + // Trench ceilings (4): steel/aluminum above underpass height + double trenchXBlue1 = 3.96; + double trenchXBlue2 = 5.18; + double trenchXRed1 = FIELD_LENGTH - 5.18; + double trenchXRed2 = FIELD_LENGTH - 3.96; + aabbs.add( + new AABB( + trenchXBlue1, + 1.57, + TRENCH_HEIGHT, + trenchXBlue2, + 3.73, + TRENCH_PILLAR_HEIGHT, + COR_STEEL)); + aabbs.add( + new AABB( + trenchXBlue1, + FIELD_WIDTH - 3.73, + TRENCH_HEIGHT, + trenchXBlue2, + FIELD_WIDTH - 1.57, + TRENCH_PILLAR_HEIGHT, + COR_STEEL)); + aabbs.add( + new AABB( + trenchXRed1, + 1.57, + TRENCH_HEIGHT, + trenchXRed2, + 3.73, + TRENCH_PILLAR_HEIGHT, + COR_STEEL)); + aabbs.add( + new AABB( + trenchXRed1, + FIELD_WIDTH - 3.73, + TRENCH_HEIGHT, + trenchXRed2, + FIELD_WIDTH - 1.57, + TRENCH_PILLAR_HEIGHT, + COR_STEEL)); + + // Tower poles (2): 2in wide x 47in tall + double blueTowerX = 1.067; + double redTowerX = 15.494; + double blueTowerY = 4.039; + double redTowerY = 4.318; + aabbs.add( + new AABB( + blueTowerX - TOWER_POLE_WIDTH / 2, + blueTowerY - TOWER_POLE_WIDTH / 2, + 0, + blueTowerX + TOWER_POLE_WIDTH / 2, + blueTowerY + TOWER_POLE_WIDTH / 2, + TOWER_POLE_HEIGHT, + COR_STEEL)); + aabbs.add( + new AABB( + redTowerX - TOWER_POLE_WIDTH / 2, + redTowerY - TOWER_POLE_WIDTH / 2, + 0, + redTowerX + TOWER_POLE_WIDTH / 2, + redTowerY + TOWER_POLE_WIDTH / 2, + TOWER_POLE_HEIGHT, + COR_STEEL)); + + // Tower uprights (4): 1.5in x 3.5in x 72.1in, two per tower + double halfSpacing = TOWER_UPRIGHT_SPACING / 2.0; + aabbs.add( + new AABB( + 0, + blueTowerY - halfSpacing - TOWER_UPRIGHT_THICK / 2, + 0, + TOWER_UPRIGHT_DEPTH, + blueTowerY - halfSpacing + TOWER_UPRIGHT_THICK / 2, + TOWER_UPRIGHT_HEIGHT, + COR_STEEL)); + aabbs.add( + new AABB( + 0, + blueTowerY + halfSpacing - TOWER_UPRIGHT_THICK / 2, + 0, + TOWER_UPRIGHT_DEPTH, + blueTowerY + halfSpacing + TOWER_UPRIGHT_THICK / 2, + TOWER_UPRIGHT_HEIGHT, + COR_STEEL)); + aabbs.add( + new AABB( + FIELD_LENGTH - TOWER_UPRIGHT_DEPTH, + redTowerY - halfSpacing - TOWER_UPRIGHT_THICK / 2, + 0, + FIELD_LENGTH, + redTowerY - halfSpacing + TOWER_UPRIGHT_THICK / 2, + TOWER_UPRIGHT_HEIGHT, + COR_STEEL)); + aabbs.add( + new AABB( + FIELD_LENGTH - TOWER_UPRIGHT_DEPTH, + redTowerY + halfSpacing - TOWER_UPRIGHT_THICK / 2, + 0, + FIELD_LENGTH, + redTowerY + halfSpacing + TOWER_UPRIGHT_THICK / 2, + TOWER_UPRIGHT_HEIGHT, + COR_STEEL)); + + // Tower bracing (2): between uprights, 28.4-43.4in height + aabbs.add( + new AABB( + 0, + blueTowerY - halfSpacing, + BRACING_BOTTOM, + TOWER_UPRIGHT_DEPTH, + blueTowerY + halfSpacing, + BRACING_TOP, + COR_STEEL)); + aabbs.add( + new AABB( + FIELD_LENGTH - TOWER_UPRIGHT_DEPTH, + redTowerY - halfSpacing, + BRACING_BOTTOM, + FIELD_LENGTH, + redTowerY + halfSpacing, + BRACING_TOP, + COR_STEEL)); + + // Hub ramp colliders (2): 47in x 217in ground-level area around each hub + aabbs.add( + new AABB( + BLUE_HUB_CENTER.getX() - HUB_RAMP_WIDTH / 2, + BLUE_HUB_CENTER.getY() - HUB_RAMP_LENGTH / 2, + 0, + BLUE_HUB_CENTER.getX() + HUB_RAMP_WIDTH / 2, + BLUE_HUB_CENTER.getY() + HUB_RAMP_LENGTH / 2, + HUB_RAMP_HEIGHT, + COR_STEEL)); + aabbs.add( + new AABB( + RED_HUB_CENTER.getX() - HUB_RAMP_WIDTH / 2, + RED_HUB_CENTER.getY() - HUB_RAMP_LENGTH / 2, + 0, + RED_HUB_CENTER.getX() + HUB_RAMP_WIDTH / 2, + RED_HUB_CENTER.getY() + HUB_RAMP_LENGTH / 2, + HUB_RAMP_HEIGHT, + COR_STEEL)); + + // Hub body panels deflect ground-level balls + aabbs.add( + new AABB( + BLUE_HUB_CENTER.getX() - HUB_SIDE / 2, + BLUE_HUB_CENTER.getY() - HUB_SIDE / 2, + 0, + BLUE_HUB_CENTER.getX() + HUB_SIDE / 2, + BLUE_HUB_CENTER.getY() + HUB_SIDE / 2, + HUB_BASE_HEIGHT, + COR_HUB)); + aabbs.add( + new AABB( + RED_HUB_CENTER.getX() - HUB_SIDE / 2, + RED_HUB_CENTER.getY() - HUB_SIDE / 2, + 0, + RED_HUB_CENTER.getX() + HUB_SIDE / 2, + RED_HUB_CENTER.getY() + HUB_SIDE / 2, + HUB_BASE_HEIGHT, + COR_HUB)); + + AABB_OBSTACLES = aabbs.toArray(new AABB[0]); + } + + // Cylinder obstacles (tower rungs + divider pipes) + + private static final CylinderObstacle[] CYLINDER_OBSTACLES; + + static { + List cyls = new ArrayList<>(); + double blueTowerY = 4.039; + double redTowerY = 4.318; + double halfSpacing = TOWER_UPRIGHT_SPACING / 2.0; + + // Blue tower rungs (3) + for (double h : new double[] {RUNG_LOW_HEIGHT, RUNG_MID_HEIGHT, RUNG_HIGH_HEIGHT}) { + cyls.add( + new CylinderObstacle( + 0, + blueTowerY - halfSpacing - RUNG_OVERHANG, + h, + 0, + blueTowerY + halfSpacing + RUNG_OVERHANG, + h, + RUNG_RADIUS, + COR_STEEL)); + } + + // Red tower rungs (3) + for (double h : new double[] {RUNG_LOW_HEIGHT, RUNG_MID_HEIGHT, RUNG_HIGH_HEIGHT}) { + cyls.add( + new CylinderObstacle( + FIELD_LENGTH, + redTowerY - halfSpacing - RUNG_OVERHANG, + h, + FIELD_LENGTH, + redTowerY + halfSpacing + RUNG_OVERHANG, + h, + RUNG_RADIUS, + COR_STEEL)); + } + + CYLINDER_OBSTACLES = cyls.toArray(new CylinderObstacle[0]); + } + + /** Axis-aligned bounding box with restitution coefficient. */ + private record AABB( + double minX, + double minY, + double minZ, + double maxX, + double maxY, + double maxZ, + double cor) {} + + /** Cylinder obstacle defined by two axis endpoints, a radius, and restitution. */ + private record CylinderObstacle( + double ax, + double ay, + double az, + double bx, + double by, + double bz, + double radius, + double cor, + double abx, + double aby, + double abz, + double abLenSq) { + CylinderObstacle( + double ax, + double ay, + double az, + double bx, + double by, + double bz, + double radius, + double cor) { + this( + ax, + ay, + az, + bx, + by, + bz, + radius, + cor, + bx - ax, + by - ay, + bz - az, + (bx - ax) * (bx - ax) + (by - ay) * (by - ay) + (bz - az) * (bz - az)); + } + } + + /** XZ line segment extruded along a Y range, used for bump ramp geometry. */ + private record BumpSegment( + double xStart, + double zStart, + double xEnd, + double zEnd, + double yStart, + double yEnd, + double lineX, + double lineZ, + double lineLen, + double nx, + double nz) { + BumpSegment( + double xStart, + double zStart, + double xEnd, + double zEnd, + double yStart, + double yEnd) { + this( + xStart, + zStart, + xEnd, + zEnd, + yStart, + yEnd, + xEnd - xStart, + zEnd - zStart, + Math.hypot(xEnd - xStart, zEnd - zStart), + // Normal perpendicular to line in XZ, flipped so nz >= 0 (points away from + // ground) + (xEnd - xStart) >= 0 + ? -(zEnd - zStart) / Math.hypot(xEnd - xStart, zEnd - zStart) + : (zEnd - zStart) / Math.hypot(xEnd - xStart, zEnd - zStart), + Math.abs(xEnd - xStart) / Math.hypot(xEnd - xStart, zEnd - zStart)); + } + } + + /** Physics feature toggles. Flip these on/off to debug or simplify the sim. */ + public static class PhysicsConfig { + public boolean dragEnabled = true; + public boolean magnusEnabled = true; + public boolean frictionEnabled = true; + public boolean spinTransferEnabled = true; + public boolean sleepingEnabled = true; + public boolean ccdEnabled = true; + public boolean velocityDependentCOR = true; + public boolean spinDecayEnabled = true; + public int solverIterations = 4; + public int subticks = 5; + public double spinDecayTau = 3.0; // seconds + public double sleepVelocityThreshold = 0.01; // m/s + public int sleepFrameThreshold = 10; // consecutive frames + public double ccdSpeedThreshold = 10.0; // m/s + public double baumgarteBeta = 0.2; + public double baumgarteSlop = 0.005; // 5mm allowed penetration + public boolean deterministic = false; + public long deterministicSeed = 42L; + public boolean conservationMonitor = false; + + /** Default: everything on. */ + public PhysicsConfig() {} + + /** Deep copy. */ + public PhysicsConfig copy() { + PhysicsConfig c = new PhysicsConfig(); + c.dragEnabled = dragEnabled; + c.magnusEnabled = magnusEnabled; + c.frictionEnabled = frictionEnabled; + c.spinTransferEnabled = spinTransferEnabled; + c.sleepingEnabled = sleepingEnabled; + c.ccdEnabled = ccdEnabled; + c.velocityDependentCOR = velocityDependentCOR; + c.spinDecayEnabled = spinDecayEnabled; + c.solverIterations = solverIterations; + c.subticks = subticks; + c.spinDecayTau = spinDecayTau; + c.sleepVelocityThreshold = sleepVelocityThreshold; + c.sleepFrameThreshold = sleepFrameThreshold; + c.ccdSpeedThreshold = ccdSpeedThreshold; + c.baumgarteBeta = baumgarteBeta; + c.baumgarteSlop = baumgarteSlop; + c.deterministic = deterministic; + c.deterministicSeed = deterministicSeed; + c.conservationMonitor = conservationMonitor; + return c; + } + } + + /** One ball in the simulation. Tracks position, velocity, spin, and lifecycle flags. */ + public static class SimBall { + // State + Translation3d pos; // field-frame position (m) + Translation3d vel; // field-frame velocity (m/s) + Translation3d omega; // angular velocity (rad/s), 3D spin axis + + // Previous state (for CCD and scoring detection) + Translation3d prevPos; + Translation3d prevVel; + + // Sleeping + boolean sleeping; + int sleepCounter; + + // Stuck-on-obstacle detection + int elevatedSlowCounter; + + // Lifecycle flags + boolean intaked; + boolean outOfBounds; + + SimBall(Translation3d pos, Translation3d vel, Translation3d omega) { + this.pos = pos; + this.vel = vel; + this.omega = omega; + this.prevPos = pos; + this.prevVel = vel; + this.sleeping = false; + this.sleepCounter = 0; + this.elevatedSlowCounter = 0; + this.intaked = false; + this.outOfBounds = false; + } + + SimBall(Translation3d pos, Translation3d vel) { + this(pos, vel, new Translation3d()); + } + + SimBall(Translation3d pos) { + this(pos, new Translation3d(), new Translation3d()); + } + + /** Get backspin in RPM (from the Y component of omega). */ + public double getSpinRPM() { + return omega.getY() * 60.0 / (2.0 * Math.PI); + } + } + + /** Contact point between two colliding objects. Used by the impulse solver. */ + static class Contact { + int ballIndexA; // index into balls list + int ballIndexB; // index into balls list, or -1 for field geometry + Translation3d normal; // contact normal (A -> B or outward from field) + double penetration; // overlap depth (positive = overlapping) + Translation3d contactPoint; // world-space contact location + double normalImpulseAccum; // warm-start accumulated normal impulse + double tangentImpulseAccum; // warm-start accumulated tangent impulse + double restitution; // effective COR for this pair + double friction; // Coulomb mu for this pair + double restitutionVelocity; // target bounce-back speed, set once before solving + + Contact() { + normal = new Translation3d(); + contactPoint = new Translation3d(); + } + } + + /** A hub that can be scored in. Detects balls falling through the opening. */ + public static class ScoringTarget { + final Translation2d center; + final Translation3d exit; + final int exitVelXSign; // +1 for blue (exits toward red), -1 for red + int score; + + ScoringTarget(Translation2d center, Translation3d exit, int exitVelXSign) { + this.center = center; + this.exit = exit; + this.exitVelXSign = exitVelXSign; + this.score = 0; + } + + boolean didScore(SimBall ball) { + double dist2d = ball.pos.toTranslation2d().getDistance(center); + if (dist2d > HUB_ENTRY_RADIUS) return false; + double currZ = ball.pos.getZ(); + double prevZ = ball.prevPos.getZ(); + // Only count balls falling through the opening (top-down entry) + return prevZ > HUB_ENTRY_HEIGHT && currZ <= HUB_ENTRY_HEIGHT; + } + + Translation3d getDispersalVelocity(Random rng) { + double vx = exitVelXSign * (rng.nextDouble() + 0.1) * 1.5; + double vy = rng.nextDouble() * 2.0 - 1.0; + return new Translation3d(vx, vy, 0); + } + + /** How many balls have scored in this hub. */ + public int getScore() { + return score; + } + + void resetScore() { + score = 0; + } + } + + /** Intake zone defined in robot-relative coordinates. Picks up balls that enter the box. */ + static class IntakeZone { + final double xMin, xMax, yMin, yMax; + final BooleanSupplier active; + final Runnable callback; + + IntakeZone( + double xMin, + double xMax, + double yMin, + double yMax, + BooleanSupplier active, + Runnable callback) { + this.xMin = xMin; + this.xMax = xMax; + this.yMin = yMin; + this.yMax = yMax; + this.active = active; + this.callback = callback; + } + + boolean shouldIntake(SimBall ball, Pose2d robotPose, double bumperHeight) { + if (!active.getAsBoolean() || ball.pos.getZ() > bumperHeight) return false; + Translation2d relPos = + new Pose2d(ball.pos.toTranslation2d(), Rotation2d.kZero) + .relativeTo(robotPose) + .getTranslation(); + boolean inside = + relPos.getX() >= xMin + && relPos.getX() <= xMax + && relPos.getY() >= yMin + && relPos.getY() <= yMax; + if (inside) { + callback.run(); + } + return inside; + } + } + + // State + + private final List balls = new ArrayList<>(); + private final List contacts = new ArrayList<>(); + private final List contactPool = new ArrayList<>(); // pre-allocated contact pool + private int contactPoolIndex = 0; + + private PhysicsConfig config; + private Random rng; + + // Spatial hash grid for ball-ball broadphase + @SuppressWarnings("unchecked") + private final List[][] grid = new ArrayList[GRID_COLS][GRID_ROWS]; + + // Hub targets + private final ScoringTarget blueHub; + private final ScoringTarget redHub; + + // Robot registration + private Supplier robotPoseSupplier; + private Supplier robotSpeedsSupplier; + private double robotWidth; + private double robotLength; + private double bumperHeight; + private int hopperSize = + Integer.MAX_VALUE; // max balls the robot can hold; unlimited by default + + // Intakes + private final List intakes = new ArrayList<>(); + + // Counters + private int totalLaunched; + private int totalScored; + private int totalIntaked; + private double lastLaunchSpeed; + + // Running state + private boolean running; + + // NetworkTables publishing + private StructArrayPublisher positionPublisher; + private StructArrayPublisher inFlightPublisher; + private StructArrayPublisher lastShotArcPublisher; + private IntegerPublisher blueScorePub; + private IntegerPublisher redScorePub; + private IntegerPublisher ballCountPub; + private IntegerPublisher activeBallsPub; + private IntegerPublisher sleepingBallsPub; + private IntegerPublisher contactCountPub; + private DoublePublisher physicsTimePub; + private DoublePublisher totalEnergyPub; + + // Last shot arc for trajectory visualization (predicted path in Field3d) + private Translation3d[] lastShotArc = new Translation3d[0]; + private long lastPhysicsNanos; + + // Conservation monitor state + private double totalKE; + private double totalPE; + private Translation3d totalMomentum = new Translation3d(); + + // Constructor + + /** + * New sim with default physics config. Publishes ball positions to the given NT path. + * + * @param tableKey where to publish in NetworkTables (e.g. "Sim/Fuel") + */ + public FuelPhysicsSim(String tableKey) { + this(tableKey, new PhysicsConfig()); + } + + /** + * New sim with custom physics config. + * + * @param tableKey where to publish in NetworkTables + * @param config physics feature toggles and tuning + */ + public FuelPhysicsSim(String tableKey, PhysicsConfig config) { + this.config = config; + this.rng = config.deterministic ? new Random(config.deterministicSeed) : new Random(); + + // Initialize spatial hash grid + for (int i = 0; i < GRID_COLS; i++) { + for (int j = 0; j < GRID_ROWS; j++) { + grid[i][j] = new ArrayList<>(); + } + } + + // Pre-allocate contact pool + for (int i = 0; i < 200; i++) { + contactPool.add(new Contact()); + } + + // Create hubs + blueHub = + new ScoringTarget( + BLUE_HUB_CENTER, new Translation3d(5.3, FIELD_WIDTH / 2.0, 0.89), 1); + redHub = + new ScoringTarget( + RED_HUB_CENTER, + new Translation3d(FIELD_LENGTH - 5.3, FIELD_WIDTH / 2.0, 0.89), + -1); + + // NT publishers + var nt = NetworkTableInstance.getDefault(); + positionPublisher = + nt.getStructArrayTopic(tableKey + "/Positions", Translation3d.struct).publish(); + inFlightPublisher = + nt.getStructArrayTopic(tableKey + "/InFlight", Translation3d.struct).publish(); + lastShotArcPublisher = + nt.getStructArrayTopic(tableKey + "/LastShotArc", Translation3d.struct).publish(); + blueScorePub = nt.getIntegerTopic(tableKey + "/BlueScore").publish(); + redScorePub = nt.getIntegerTopic(tableKey + "/RedScore").publish(); + ballCountPub = nt.getIntegerTopic(tableKey + "/Stats/BallCount").publish(); + activeBallsPub = nt.getIntegerTopic(tableKey + "/Stats/ActiveBalls").publish(); + sleepingBallsPub = nt.getIntegerTopic(tableKey + "/Stats/SleepingBalls").publish(); + contactCountPub = nt.getIntegerTopic(tableKey + "/Stats/ContactsPerTick").publish(); + physicsTimePub = nt.getDoubleTopic(tableKey + "/Stats/PhysicsMs").publish(); + totalEnergyPub = nt.getDoubleTopic(tableKey + "/Stats/TotalEnergy").publish(); + + running = false; + totalLaunched = 0; + totalScored = 0; + totalIntaked = 0; + lastLaunchSpeed = 0; + } + + /** Default constructor, publishes to "Sim/FuelPositions". */ + public FuelPhysicsSim() { + this("Sim/FuelPositions"); + } + + /** Turn the sim on. You still need to call tick() every cycle. */ + public void enable() { + running = true; + } + + /** Pause the sim. Balls freeze in place. */ + public void disable() { + running = false; + } + + /** Is the sim running? */ + public boolean isRunning() { + return running; + } + + /** + * Tell the sim about your robot so it can handle bumper collisions and intake pickup. + * + * @param width robot width along Y axis (m) + * @param length robot length along X axis (m) + * @param bumperHeight bumper height (m) + * @param hopperSize maximum number of balls the robot's hopper can hold; intake stops when full + * @param poseSupplier field-relative pose supplier + * @param speedsSupplier field-relative chassis speeds supplier + */ + public void configureRobot( + double width, + double length, + double bumperHeight, + int hopperSize, + Supplier poseSupplier, + Supplier speedsSupplier) { + this.robotWidth = width; + this.robotLength = length; + this.bumperHeight = bumperHeight; + this.hopperSize = hopperSize; + this.robotPoseSupplier = poseSupplier; + this.robotSpeedsSupplier = speedsSupplier; + } + + /** + * Tell the sim about your robot so it can handle bumper collisions and intake pickup. Hopper + * capacity is unlimited. + * + * @param width robot width along Y axis (m) + * @param length robot length along X axis (m) + * @param bumperHeight bumper height (m) + * @param poseSupplier field-relative pose supplier + * @param speedsSupplier field-relative chassis speeds supplier + */ + public void configureRobot( + double width, + double length, + double bumperHeight, + Supplier poseSupplier, + Supplier speedsSupplier) { + configureRobot( + width, length, bumperHeight, Integer.MAX_VALUE, poseSupplier, speedsSupplier); + } + + /** + * Add an intake zone. Balls that enter this box (in robot-relative coords) get picked up. + * + * @param xMin front edge in robot frame + * @param xMax back edge in robot frame + * @param yMin left edge in robot frame + * @param yMax right edge in robot frame + * @param active returns true when the intake is actually running + * @param callback fires when a ball gets picked up + */ + public void addIntakeZone( + double xMin, + double xMax, + double yMin, + double yMax, + BooleanSupplier active, + Runnable callback) { + intakes.add(new IntakeZone(xMin, xMax, yMin, yMax, active, callback)); + } + + /** Add an intake zone without a callback. */ + public void addIntakeZone( + double xMin, double xMax, double yMin, double yMax, BooleanSupplier active) { + addIntakeZone(xMin, xMax, yMin, yMax, active, () -> {}); + } + + /** + * Shoot a ball into the sim. + * + * @param pos where the ball leaves the launcher (field frame, meters) + * @param vel launch velocity (field frame, m/s) + * @param spinRPM backspin in RPM (positive = backspin = Magnus lift) + */ + public void launchBall(Translation3d pos, Translation3d vel, double spinRPM) { + // Convert RPM to 3D omega: backspin is rotation around -Y axis + double omegaY = -spinRPM * 2.0 * Math.PI / 60.0; + Translation3d omega = new Translation3d(0, omegaY, 0); + launchBall(pos, vel, omega); + } + + /** + * Shoot a ball with full 3D spin control. + * + * @param pos launch position (field frame, meters) + * @param vel launch velocity (field frame, m/s) + * @param omega 3D angular velocity (rad/s) + */ + public void launchBall(Translation3d pos, Translation3d vel, Translation3d omega) { + if (balls.size() >= MAX_BALLS) return; + SimBall ball = new SimBall(pos, vel, omega); + balls.add(ball); + totalLaunched++; + totalIntaked--; + lastLaunchSpeed = vel.getNorm(); + + // Predict the trajectory arc for Field3d visualization (20 points, gravity + drag only) + lastShotArc = predictArc(pos, vel, 20, 0.05); + + // Wake nearby sleeping balls + if (config.sleepingEnabled) { + wakeNearbyBalls(pos, 1.0); + } + } + + /** Drop a ball at this position, sitting on the ground. */ + public void spawnBall(Translation3d pos) { + if (balls.size() >= MAX_BALLS) return; + balls.add(new SimBall(pos)); + } + + /** Drop a ball with some initial velocity. */ + public void spawnBall(Translation3d pos, Translation3d vel) { + if (balls.size() >= MAX_BALLS) return; + balls.add(new SimBall(pos, vel)); + } + + /** Remove every ball from the sim. */ + public void clearBalls() { + balls.clear(); + } + + /** Spawn all game pieces in their starting positions (neutral zone + depots). */ + public void placeFieldBalls() { + // Neutral zone fuel + double cx = FIELD_LENGTH / 2.0; + double cy = FIELD_WIDTH / 2.0; + int nzCols = 12; // 12 * 5.91in = 70.9in, fits inside the 72.0in depth + int nzRows = 30; // 30 * 5.91in = 177.3in, fits inside the 206.0in width + double halfDivider = 0.0254; // half of the 2.0in center divider + + for (int col = 0; col < nzCols; col++) { + double x = cx + (col - (nzCols - 1) * 0.5) * BALL_DIAMETER; + for (int row = 0; row < nzRows; row++) { + double y = cy + (row - (nzRows - 1) * 0.5) * BALL_DIAMETER; + if (Math.abs(y - cy) < halfDivider) continue; + spawnBall(new Translation3d(x, y, BALL_RADIUS)); + } + } + + // Depot fuel + int depotCols = 4; // 4 * 5.91in = 23.6in fits inside 27.0in depth + int depotRows = 6; // 6 * 5.91in = 35.5in fits inside 42.0in width + + // Blue-side depot + fillBallGrid(FIELD_LENGTH - 0.37, 2.10, depotCols, depotRows); + // Red-side depot + fillBallGrid(0.37, FIELD_WIDTH - 2.10, depotCols, depotRows); + } + + /** Helper: fill a grid of balls centered at (cx, cy). */ + private void fillBallGrid(double cx, double cy, int cols, int rows) { + for (int c = 0; c < cols; c++) { + double x = cx + (c - (cols - 1) * 0.5) * BALL_DIAMETER; + for (int r = 0; r < rows; r++) { + double y = cy + (r - (rows - 1) * 0.5) * BALL_DIAMETER; + spawnBall(new Translation3d(x, y, BALL_RADIUS)); + } + } + } + + /** + * Step the sim forward one period (20ms) and publish ball positions to NT. Does nothing if the + * sim isn't enabled. + */ + public void tick() { + if (!running) return; + long t0 = System.nanoTime(); + advancePhysics(PERIOD); + lastPhysicsNanos = System.nanoTime() - t0; + publishPositions(); + } + + /** + * Advance physics by dt seconds. Splits into subticks for stability. + * + * @param dt time to advance (seconds) + */ + public void advancePhysics(double dt) { + int ticks = Math.max(1, config.subticks); + double subDt = dt / ticks; + + for (int tick = 0; tick < ticks; tick++) { + stepSubtick(subDt); + } + + // Remove flagged balls + removeFlaggedBalls(); + } + + // Core physics pipeline + + private void stepSubtick(double subDt) { + // Reset contact list + contactPoolIndex = 0; + contacts.clear(); + + for (int i = 0; i < balls.size(); i++) { + SimBall ball = balls.get(i); + if (ball.intaked || ball.outOfBounds) continue; + if (ball.sleeping && config.sleepingEnabled) continue; + + // Save previous state + ball.prevPos = ball.pos; + ball.prevVel = ball.vel; + + // Compute forces and get acceleration + Translation3d accel = computeAcceleration(ball); + + // Symplectic Euler: update velocity first so we don't accumulate energy drift + ball.vel = ball.vel.plus(accel.times(subDt)); + ball.pos = ball.pos.plus(ball.vel.times(subDt)); + + // Spin decay + if (config.spinDecayEnabled && ball.omega.getNorm() > 1e-6) { + double decayFactor = Math.exp(-subDt / config.spinDecayTau); + ball.omega = ball.omega.times(decayFactor); + } + } + + // CCD for fast balls + if (config.ccdEnabled) { + for (int i = 0; i < balls.size(); i++) { + SimBall ball = balls.get(i); + if (ball.intaked || ball.outOfBounds) continue; + if (ball.sleeping && config.sleepingEnabled) continue; + if (ball.vel.getNorm() > config.ccdSpeedThreshold) { + handleCCD(ball); + } + } + } + + // Broadphase: build spatial hash + buildSpatialHash(); + + // Generate contacts: ball-ball via spatial hash, ball-field via narrowphase + generateBallBallContacts(); + generateBallFieldContacts(); + + // Sequential impulse solver + solveContacts(); + + // Simple wall/ground handling (direct impulse, not through solver) + for (int i = 0; i < balls.size(); i++) { + SimBall ball = balls.get(i); + if (ball.intaked || ball.outOfBounds) continue; + if (ball.sleeping && config.sleepingEnabled) continue; + handleWallBounce(ball); + handleGroundContact(ball, subDt); + handleBumpCollisions(ball); + } + + // Hub scoring + for (int i = 0; i < balls.size(); i++) { + SimBall ball = balls.get(i); + if (ball.intaked || ball.outOfBounds) continue; + handleHubScoring(ball); + } + + // Net collisions + for (int i = 0; i < balls.size(); i++) { + SimBall ball = balls.get(i); + if (ball.intaked || ball.outOfBounds) continue; + if (ball.sleeping && config.sleepingEnabled) continue; + handleNetCollision(ball, blueHub); + handleNetCollision(ball, redHub); + } + + // Robot interaction + if (robotPoseSupplier != null && robotSpeedsSupplier != null) { + Pose2d robotPose = robotPoseSupplier.get(); + ChassisSpeeds speeds = robotSpeedsSupplier.get(); + Translation2d robotVel = + new Translation2d(speeds.vxMetersPerSecond, speeds.vyMetersPerSecond); + + // Wake radius: robot half-diagonal plus margin so balls react before contact + double wakeRadius = Math.hypot(robotLength, robotWidth) / 2.0 + 0.3; + double wakeRadiusSq = wakeRadius * wakeRadius; + + for (int i = 0; i < balls.size(); i++) { + SimBall ball = balls.get(i); + if (ball.intaked || ball.outOfBounds) continue; + // Wake sleeping balls near the robot so bumpers push them + if (ball.sleeping && config.sleepingEnabled) { + double dx = ball.pos.getX() - robotPose.getX(); + double dy = ball.pos.getY() - robotPose.getY(); + if (dx * dx + dy * dy < wakeRadiusSq) { + wakeBall(ball); + } else { + continue; + } + } + // Intake first so balls are consumed before bumper pushes them away + handleIntakePickup(ball, robotPose); + if (ball.intaked) continue; + handleRobotCollision(ball, robotPose, robotVel); + } + } + + // Sleep update + if (config.sleepingEnabled) { + for (int i = 0; i < balls.size(); i++) { + updateSleepState(balls.get(i)); + } + } + + // Out-of-bounds cleanup (includes NaN guard, upper Z limit, and stuck-on-obstacle removal) + for (int i = 0; i < balls.size(); i++) { + SimBall ball = balls.get(i); + if (!Double.isFinite(ball.pos.getX()) + || !Double.isFinite(ball.pos.getY()) + || !Double.isFinite(ball.pos.getZ()) + || !Double.isFinite(ball.vel.getX()) + || !Double.isFinite(ball.vel.getY()) + || !Double.isFinite(ball.vel.getZ()) + || !Double.isFinite(ball.omega.getX()) + || !Double.isFinite(ball.omega.getY()) + || !Double.isFinite(ball.omega.getZ()) + || ball.pos.getX() < -2.0 + || ball.pos.getX() > FIELD_LENGTH + 2.0 + || ball.pos.getY() < -2.0 + || ball.pos.getY() > FIELD_WIDTH + 2.0 + || ball.pos.getZ() < -1.0 + || ball.pos.getZ() > 15.0) { + ball.outOfBounds = true; + } + // Remove balls stuck on elevated obstacles + if (ball.pos.getZ() > BALL_RADIUS + 0.3 && ball.vel.getNorm() < 0.5) { + ball.elevatedSlowCounter++; + if (ball.elevatedSlowCounter > 250) { + ball.outOfBounds = true; + } + } else { + ball.elevatedSlowCounter = 0; + } + } + + // Conservation monitor + if (config.conservationMonitor) { + computeConservationQuantities(); + } + } + + /** Compute acceleration: gravity + drag + Magnus lift. Magnus only kicks in when airborne. */ + private Translation3d computeAcceleration(SimBall ball) { + // Gravity always acts + double ax = 0, ay = 0, az = -GRAVITY; + + double speed = ball.vel.getNorm(); + boolean airborne = ball.pos.getZ() > BALL_RADIUS + 0.01; + + // Drag acts whether on ground or airborne (air doesn't vanish at carpet level) + if (config.dragEnabled && speed > 1e-6) { + ax -= DRAG_ACCEL_FACTOR * speed * ball.vel.getX(); + ay -= DRAG_ACCEL_FACTOR * speed * ball.vel.getY(); + az -= DRAG_ACCEL_FACTOR * speed * ball.vel.getZ(); + } + + // Magnus only applies airborne (ground friction dominates spin behavior on carpet) + if (airborne && speed > 1e-6) { + if (config.magnusEnabled && ball.omega.getNorm() > 1e-3) { + Translation3d magnusDir = cross(ball.omega, ball.vel); + double magnusMag = magnusDir.getNorm(); + if (magnusMag > 1e-6) { + // a_magnus = MAGNUS_ACCEL_FACTOR * (omega x v) + ax += MAGNUS_ACCEL_FACTOR * magnusDir.getX(); + ay += MAGNUS_ACCEL_FACTOR * magnusDir.getY(); + az += MAGNUS_ACCEL_FACTOR * magnusDir.getZ(); + } + } + } + + return new Translation3d(ax, ay, az); + } + + /** Continuous collision detection: sweep fast balls so they don't tunnel through walls. */ + private void handleCCD(SimBall ball) { + Translation3d delta = ball.pos.minus(ball.prevPos); + double dist = delta.getNorm(); + if (dist < 1e-6) return; + + Translation3d dir = delta.div(dist); + + // Check against field boundaries + double tMin = 1.0; + Translation3d hitNormal = null; + + // X walls + if (dir.getX() < -1e-6) { + double t = (BALL_RADIUS - ball.prevPos.getX()) / (delta.getX()); + if (t > 0 && t < tMin) { + tMin = t; + hitNormal = AXIS_X_POS; + } + } else if (dir.getX() > 1e-6) { + double t = (FIELD_LENGTH - BALL_RADIUS - ball.prevPos.getX()) / (delta.getX()); + if (t > 0 && t < tMin) { + tMin = t; + hitNormal = AXIS_X_NEG; + } + } + + // Y walls + if (dir.getY() < -1e-6) { + double t = (BALL_RADIUS - ball.prevPos.getY()) / (delta.getY()); + if (t > 0 && t < tMin) { + tMin = t; + hitNormal = AXIS_Y_POS; + } + } else if (dir.getY() > 1e-6) { + double t = (FIELD_WIDTH - BALL_RADIUS - ball.prevPos.getY()) / (delta.getY()); + if (t > 0 && t < tMin) { + tMin = t; + hitNormal = AXIS_Y_NEG; + } + } + + // Ground + if (dir.getZ() < -1e-6) { + double t = (BALL_RADIUS - ball.prevPos.getZ()) / (delta.getZ()); + if (t > 0 && t < tMin) { + tMin = t; + hitNormal = AXIS_Z_POS; + } + } + + // AABB obstacles (ray-box intersection) + for (AABB aabb : AABB_OBSTACLES) { + double tHit = sweepSphereAABB(ball.prevPos, delta, aabb); + if (tHit >= 0 && tHit < tMin) { + // Compute normal at hit point + Translation3d hitPos = ball.prevPos.plus(delta.times(tHit)); + Translation3d n = computeAABBNormal(hitPos, aabb); + if (n != null) { + tMin = tHit; + hitNormal = n; + } + } + } + + if (hitNormal != null && tMin < 1.0) { + // Move ball to contact point + ball.pos = ball.prevPos.plus(delta.times(tMin)); + + // Reflect velocity + double vDotN = ball.vel.dot(hitNormal); + if (vDotN < 0) { + double cor = config.velocityDependentCOR ? velocityCOR(COR_WALL, -vDotN) : COR_WALL; + ball.vel = ball.vel.minus(hitNormal.times((1.0 + cor) * vDotN)); + } + } + } + + /** + * Ray-AABB intersection with sphere expansion (Minkowski sum). Returns hit time in [0,1] or -1. + */ + private double sweepSphereAABB(Translation3d origin, Translation3d delta, AABB aabb) { + // Expand AABB by ball radius (Minkowski sum with sphere) + double minX = aabb.minX() - BALL_RADIUS; + double minY = aabb.minY() - BALL_RADIUS; + double minZ = aabb.minZ() - BALL_RADIUS; + double maxX = aabb.maxX() + BALL_RADIUS; + double maxY = aabb.maxY() + BALL_RADIUS; + double maxZ = aabb.maxZ() + BALL_RADIUS; + + double tEnter = 0; + double tExit = 1; + + // X slab + if (Math.abs(delta.getX()) > 1e-9) { + double invD = 1.0 / delta.getX(); + double t1 = (minX - origin.getX()) * invD; + double t2 = (maxX - origin.getX()) * invD; + if (t1 > t2) { + double tmp = t1; + t1 = t2; + t2 = tmp; + } + tEnter = Math.max(tEnter, t1); + tExit = Math.min(tExit, t2); + } else { + if (origin.getX() < minX || origin.getX() > maxX) return -1; + } + + // Y slab + if (Math.abs(delta.getY()) > 1e-9) { + double invD = 1.0 / delta.getY(); + double t1 = (minY - origin.getY()) * invD; + double t2 = (maxY - origin.getY()) * invD; + if (t1 > t2) { + double tmp = t1; + t1 = t2; + t2 = tmp; + } + tEnter = Math.max(tEnter, t1); + tExit = Math.min(tExit, t2); + } else { + if (origin.getY() < minY || origin.getY() > maxY) return -1; + } + + // Z slab + if (Math.abs(delta.getZ()) > 1e-9) { + double invD = 1.0 / delta.getZ(); + double t1 = (minZ - origin.getZ()) * invD; + double t2 = (maxZ - origin.getZ()) * invD; + if (t1 > t2) { + double tmp = t1; + t1 = t2; + t2 = tmp; + } + tEnter = Math.max(tEnter, t1); + tExit = Math.min(tExit, t2); + } else { + if (origin.getZ() < minZ || origin.getZ() > maxZ) return -1; + } + + if (tEnter > tExit || tExit < 0) return -1; + return tEnter > 0 ? tEnter : -1; // Already inside if tEnter <= 0 + } + + // Broadphase (spatial hash) + + private void buildSpatialHash() { + for (int i = 0; i < GRID_COLS; i++) { + for (int j = 0; j < GRID_ROWS; j++) { + grid[i][j].clear(); + } + } + // Include sleeping balls so awake balls can detect them for wake-on-collision + for (int i = 0; i < balls.size(); i++) { + SimBall ball = balls.get(i); + if (ball.intaked || ball.outOfBounds) continue; + int col = (int) (ball.pos.getX() / CELL_SIZE); + int row = (int) (ball.pos.getY() / CELL_SIZE); + if (col >= 0 && col < GRID_COLS && row >= 0 && row < GRID_ROWS) { + grid[col][row].add(i); + } + } + } + + // Narrowphase contact generation + + private void generateBallBallContacts() { + for (int i = 0; i < balls.size(); i++) { + SimBall ballA = balls.get(i); + if (ballA.intaked || ballA.outOfBounds) continue; + if (ballA.sleeping && config.sleepingEnabled) continue; + + int col = (int) (ballA.pos.getX() / CELL_SIZE); + int row = (int) (ballA.pos.getY() / CELL_SIZE); + + for (int di = -1; di <= 1; di++) { + for (int dj = -1; dj <= 1; dj++) { + int ci = col + di; + int cj = row + dj; + if (ci < 0 || ci >= GRID_COLS || cj < 0 || cj >= GRID_ROWS) continue; + List cell = grid[ci][cj]; + for (int k = 0; k < cell.size(); k++) { + int j = cell.get(k); + if (j == i) continue; // same ball + if (j < i && !(config.sleepingEnabled && balls.get(j).sleeping)) continue; + + SimBall ballB = balls.get(j); + double dx = ballA.pos.getX() - ballB.pos.getX(); + double dy = ballA.pos.getY() - ballB.pos.getY(); + double dz = ballA.pos.getZ() - ballB.pos.getZ(); + double distSq = dx * dx + dy * dy + dz * dz; + double minDist = BALL_RADIUS * 2; + + if (distSq < minDist * minDist) { + double dist = Math.sqrt(distSq); + Contact c = allocateContact(); + if (dist < 1e-9) { + c.normal = AXIS_X_POS; + dist = 1e-9; + } else { + c.normal = ballA.pos.minus(ballB.pos).div(dist); + } + c.ballIndexA = i; + c.ballIndexB = j; + c.penetration = minDist - dist; + c.contactPoint = + ballA.pos.plus(ballB.pos).div(2.0); // midpoint between centers + c.restitution = COR_BALL_BALL; + c.friction = config.frictionEnabled ? MU_BALL_BALL : 0; + c.normalImpulseAccum = 0; + c.tangentImpulseAccum = 0; + contacts.add(c); + + // Wake sleeping ball on contact + if (ballB.sleeping) { + wakeBall(ballB); + } + } + } + } + } + } + } + + private void generateBallFieldContacts() { + for (int i = 0; i < balls.size(); i++) { + SimBall ball = balls.get(i); + if (ball.intaked || ball.outOfBounds) continue; + if (ball.sleeping && config.sleepingEnabled) continue; + + // AABB obstacles + for (AABB aabb : AABB_OBSTACLES) { + generateSphereAABBContact(i, ball, aabb); + } + + // Cylinder obstacles + for (CylinderObstacle cyl : CYLINDER_OBSTACLES) { + generateSphereCylinderContact(i, ball, cyl); + } + } + } + + private void generateSphereAABBContact(int ballIndex, SimBall ball, AABB aabb) { + // Find nearest point on AABB to sphere center + double cx = Math.max(aabb.minX(), Math.min(ball.pos.getX(), aabb.maxX())); + double cy = Math.max(aabb.minY(), Math.min(ball.pos.getY(), aabb.maxY())); + double cz = Math.max(aabb.minZ(), Math.min(ball.pos.getZ(), aabb.maxZ())); + + double dx = ball.pos.getX() - cx; + double dy = ball.pos.getY() - cy; + double dz = ball.pos.getZ() - cz; + double distSq = dx * dx + dy * dy + dz * dz; + + if (distSq < BALL_RADIUS * BALL_RADIUS && distSq >= 1e-9) { + double dist = Math.sqrt(distSq); + Contact c = allocateContact(); + c.ballIndexA = ballIndex; + c.ballIndexB = -1; + c.normal = new Translation3d(dx / dist, dy / dist, dz / dist); + c.penetration = BALL_RADIUS - dist; + c.contactPoint = new Translation3d(cx, cy, cz); + c.restitution = aabb.cor(); + c.friction = config.frictionEnabled ? MU_WALL : 0; + c.normalImpulseAccum = 0; + c.tangentImpulseAccum = 0; + contacts.add(c); + } else if (distSq < 1e-9) { + // Ball center is inside AABB. + Translation3d normal = computeEntryFaceNormal(ball.prevPos, ball.pos, aabb); + if (normal != null) { + double pen = computeAABBPenetration(ball.pos, aabb); + Contact c = allocateContact(); + c.ballIndexA = ballIndex; + c.ballIndexB = -1; + c.normal = normal; + c.penetration = pen + BALL_RADIUS; + c.contactPoint = ball.pos; + c.restitution = aabb.cor(); + c.friction = config.frictionEnabled ? MU_WALL : 0; + c.normalImpulseAccum = 0; + c.tangentImpulseAccum = 0; + contacts.add(c); + } + } + } + + /** Which face of the AABB is the point closest to? Returns outward normal of that face. */ + private Translation3d computeAABBNormal(Translation3d p, AABB aabb) { + double dxMin = p.getX() - aabb.minX(); + double dxMax = aabb.maxX() - p.getX(); + double dyMin = p.getY() - aabb.minY(); + double dyMax = aabb.maxY() - p.getY(); + double dzMin = p.getZ() - aabb.minZ(); + double dzMax = aabb.maxZ() - p.getZ(); + + double min = dxMin; + Translation3d normal = AXIS_X_NEG; + + if (dxMax < min) { + min = dxMax; + normal = AXIS_X_POS; + } + if (dyMin < min) { + min = dyMin; + normal = AXIS_Y_NEG; + } + if (dyMax < min) { + min = dyMax; + normal = AXIS_Y_POS; + } + if (dzMin < min) { + min = dzMin; + normal = AXIS_Z_NEG; + } + if (dzMax < min) { + min = dzMax; + normal = AXIS_Z_POS; + } + return normal; + } + + /** How deep is a point inside an AABB? Returns min distance to any face. */ + private double computeAABBPenetration(Translation3d p, AABB aabb) { + double dxMin = p.getX() - aabb.minX(); + double dxMax = aabb.maxX() - p.getX(); + double dyMin = p.getY() - aabb.minY(); + double dyMax = aabb.maxY() - p.getY(); + double dzMin = p.getZ() - aabb.minZ(); + double dzMax = aabb.maxZ() - p.getZ(); + return Math.min( + Math.min(Math.min(dxMin, dxMax), Math.min(dyMin, dyMax)), Math.min(dzMin, dzMax)); + } + + /** + * If a ball is inside an AABB, figure out which face it came in through by raycasting from + * prevPos to pos. Without this, balls pop onto the top of obstacles when they actually entered + * from the side. Falls back to nearest-face if the ray is degenerate (ball spawned inside). + */ + private Translation3d computeEntryFaceNormal(Translation3d from, Translation3d to, AABB aabb) { + if (from == null) return computeAABBNormal(to, aabb); + + double dx = to.getX() - from.getX(); + double dy = to.getY() - from.getY(); + double dz = to.getZ() - from.getZ(); + + // Slab intersection: the entry face is the axis with the latest entry time + double tMax = Double.NEGATIVE_INFINITY; + Translation3d bestNormal = null; + + if (Math.abs(dx) > 1e-12) { + double invD = 1.0 / dx; + double t1 = (aabb.minX() - from.getX()) * invD; + double t2 = (aabb.maxX() - from.getX()) * invD; + double tEntry = Math.min(t1, t2); + if (tEntry > tMax) { + tMax = tEntry; + bestNormal = dx > 0 ? AXIS_X_NEG : AXIS_X_POS; + } + } + if (Math.abs(dy) > 1e-12) { + double invD = 1.0 / dy; + double t1 = (aabb.minY() - from.getY()) * invD; + double t2 = (aabb.maxY() - from.getY()) * invD; + double tEntry = Math.min(t1, t2); + if (tEntry > tMax) { + tMax = tEntry; + bestNormal = dy > 0 ? AXIS_Y_NEG : AXIS_Y_POS; + } + } + if (Math.abs(dz) > 1e-12) { + double invD = 1.0 / dz; + double t1 = (aabb.minZ() - from.getZ()) * invD; + double t2 = (aabb.maxZ() - from.getZ()) * invD; + double tEntry = Math.min(t1, t2); + if (tEntry > tMax) { + tMax = tEntry; + bestNormal = dz > 0 ? AXIS_Z_NEG : AXIS_Z_POS; + } + } + + return bestNormal != null ? bestNormal : computeAABBNormal(to, aabb); + } + + private void generateSphereCylinderContact(int ballIndex, SimBall ball, CylinderObstacle cyl) { + if (cyl.abLenSq() < 1e-12) return; + + // Find nearest point on line segment to ball center + double apx = ball.pos.getX() - cyl.ax(); + double apy = ball.pos.getY() - cyl.ay(); + double apz = ball.pos.getZ() - cyl.az(); + double t = + Math.max( + 0, + Math.min( + 1, + (apx * cyl.abx() + apy * cyl.aby() + apz * cyl.abz()) + / cyl.abLenSq())); + + double nearX = cyl.ax() + t * cyl.abx(); + double nearY = cyl.ay() + t * cyl.aby(); + double nearZ = cyl.az() + t * cyl.abz(); + + double dx = ball.pos.getX() - nearX; + double dy = ball.pos.getY() - nearY; + double dz = ball.pos.getZ() - nearZ; + double distSq = dx * dx + dy * dy + dz * dz; + double minDist = BALL_RADIUS + cyl.radius(); + + if (distSq < minDist * minDist && distSq > 0) { + double dist = Math.sqrt(distSq); + Contact c = allocateContact(); + c.ballIndexA = ballIndex; + c.ballIndexB = -1; + c.normal = new Translation3d(dx / dist, dy / dist, dz / dist); + c.penetration = minDist - dist; + c.contactPoint = new Translation3d(nearX, nearY, nearZ); + c.restitution = cyl.cor(); + c.friction = config.frictionEnabled ? MU_WALL : 0; + c.normalImpulseAccum = 0; + c.tangentImpulseAccum = 0; + contacts.add(c); + } + } + + /** + * Sequential impulse solver: resolve bounces, friction, and spin transfer across all contacts. + */ + private void solveContacts() { + if (contacts.isEmpty()) return; + + // Compute restitution targets from initial approach velocities (before any solving) + for (int i = 0; i < contacts.size(); i++) { + Contact c = contacts.get(i); + SimBall ballA = balls.get(c.ballIndexA); + Translation3d relVel; + if (c.ballIndexB >= 0) { + relVel = ballA.vel.minus(balls.get(c.ballIndexB).vel); + } else { + relVel = ballA.vel; + } + double vn = relVel.dot(c.normal); + double e = c.restitution; + if (config.velocityDependentCOR) { + e = velocityCOR(e, Math.abs(vn)); + } + // Only bounce for significant approach speeds. + c.restitutionVelocity = vn < -0.5 ? -e * vn : 0; + } + + for (int iter = 0; iter < config.solverIterations; iter++) { + for (int i = 0; i < contacts.size(); i++) { + Contact c = contacts.get(i); + solveContact(c); + } + } + + // Position correction + for (int i = 0; i < contacts.size(); i++) { + applyPositionCorrection(contacts.get(i)); + } + } + + private void solveContact(Contact c) { + SimBall ballA = balls.get(c.ballIndexA); + Translation3d relVel; + double invMassSum; + + if (c.ballIndexB >= 0) { + // Ball-ball contact + SimBall ballB = balls.get(c.ballIndexB); + relVel = ballA.vel.minus(ballB.vel); + invMassSum = 2.0 / BALL_MASS; // both balls have same mass + } else { + // Ball-field contact + relVel = ballA.vel; + invMassSum = 1.0 / BALL_MASS; + } + + double vRelNormal = relVel.dot(c.normal); + double jn = -(vRelNormal - c.restitutionVelocity) / invMassSum; + double oldAccum = c.normalImpulseAccum; + c.normalImpulseAccum = Math.max(0, oldAccum + jn); + jn = c.normalImpulseAccum - oldAccum; + + // Apply normal impulse + Translation3d normalImpulse = c.normal.times(jn); + ballA.vel = ballA.vel.plus(normalImpulse.div(BALL_MASS)); + if (c.ballIndexB >= 0) { + SimBall ballB = balls.get(c.ballIndexB); + ballB.vel = ballB.vel.minus(normalImpulse.div(BALL_MASS)); + } + + // Tangential impulse (friction) + if (c.friction > 0 && config.frictionEnabled) { + // Recompute relative velocity after normal impulse + if (c.ballIndexB >= 0) { + relVel = ballA.vel.minus(balls.get(c.ballIndexB).vel); + } else { + relVel = ballA.vel; + } + + // Tangent velocity: remove normal component + double vn = relVel.dot(c.normal); + Translation3d vTangent = relVel.minus(c.normal.times(vn)); + double vTangentMag = vTangent.getNorm(); + + if (vTangentMag > 1e-6) { + Translation3d tangentDir = vTangent.div(vTangentMag); + + // Friction impulse magnitude + double jt = -vTangentMag / invMassSum; + + // Coulomb clamp: |j_t| <= mu * j_n + double maxFriction = c.friction * c.normalImpulseAccum; + double oldTangentAccum = c.tangentImpulseAccum; + c.tangentImpulseAccum = + Math.max(-maxFriction, Math.min(maxFriction, oldTangentAccum + jt)); + jt = c.tangentImpulseAccum - oldTangentAccum; + + // Apply tangent impulse + Translation3d frictionImpulse = tangentDir.times(jt); + ballA.vel = ballA.vel.plus(frictionImpulse.div(BALL_MASS)); + if (c.ballIndexB >= 0) { + SimBall ballB = balls.get(c.ballIndexB); + ballB.vel = ballB.vel.minus(frictionImpulse.div(BALL_MASS)); + } + + // Spin transfer from friction torque + if (config.spinTransferEnabled && Math.abs(jt) > 1e-9) { + Translation3d rContact = c.normal.times(-BALL_RADIUS); // from center to contact + Translation3d torqueImpulse = cross(rContact, frictionImpulse); + Translation3d deltaOmega = torqueImpulse.div(BALL_MOMENT_OF_INERTIA); + ballA.omega = ballA.omega.plus(deltaOmega); + + if (c.ballIndexB >= 0) { + SimBall ballB = balls.get(c.ballIndexB); + Translation3d rContactB = c.normal.times(BALL_RADIUS); + Translation3d torqueImpulseB = + cross(rContactB, frictionImpulse.unaryMinus()); + Translation3d deltaOmegaB = torqueImpulseB.div(BALL_MOMENT_OF_INERTIA); + ballB.omega = ballB.omega.plus(deltaOmegaB); + } + } + } + } + } + + /** Push overlapping objects apart so they don't sink into each other. */ + private void applyPositionCorrection(Contact c) { + double slop = config.baumgarteSlop; + double beta = config.baumgarteBeta; + double correction = Math.max(c.penetration - slop, 0) * beta; + + if (correction < 1e-6) return; + + SimBall ballA = balls.get(c.ballIndexA); + if (c.ballIndexB >= 0) { + SimBall ballB = balls.get(c.ballIndexB); + Translation3d corrVec = c.normal.times(correction * 0.5); + ballA.pos = ballA.pos.plus(corrVec); + ballB.pos = ballB.pos.minus(corrVec); + } else { + ballA.pos = ballA.pos.plus(c.normal.times(correction)); + } + } + + // Wall and ground handling + + private void handleWallBounce(SimBall ball) { + double z = ball.pos.getZ(); + + // X walls (alliance walls: diamond plate + polycarbonate, height-aware) + if (ball.pos.getX() < BALL_RADIUS) { + if (z < ALLIANCE_WALL_HEIGHT) { + ball.pos = new Translation3d(BALL_RADIUS, ball.pos.getY(), ball.pos.getZ()); + if (ball.vel.getX() < 0) { + applyWallSpinTransfer(ball, AXIS_X_POS); + double cor = effectiveCOR(COR_WALL, Math.abs(ball.vel.getX())); + ball.vel = + new Translation3d( + -ball.vel.getX() * cor, ball.vel.getY(), ball.vel.getZ()); + } + } + } else if (ball.pos.getX() > FIELD_LENGTH - BALL_RADIUS) { + if (z < ALLIANCE_WALL_HEIGHT) { + ball.pos = + new Translation3d( + FIELD_LENGTH - BALL_RADIUS, ball.pos.getY(), ball.pos.getZ()); + if (ball.vel.getX() > 0) { + applyWallSpinTransfer(ball, AXIS_X_NEG); + double cor = effectiveCOR(COR_WALL, Math.abs(ball.vel.getX())); + ball.vel = + new Translation3d( + -ball.vel.getX() * cor, ball.vel.getY(), ball.vel.getZ()); + } + } + } + + // Y walls (guardrails: polycarbonate on aluminum extrusion, height-aware) + if (ball.pos.getY() < BALL_RADIUS) { + if (z < GUARDRAIL_HEIGHT) { + ball.pos = new Translation3d(ball.pos.getX(), BALL_RADIUS, ball.pos.getZ()); + if (ball.vel.getY() < 0) { + applyWallSpinTransfer(ball, AXIS_Y_POS); + double cor = effectiveCOR(COR_WALL, Math.abs(ball.vel.getY())); + ball.vel = + new Translation3d( + ball.vel.getX(), -ball.vel.getY() * cor, ball.vel.getZ()); + } + } + } else if (ball.pos.getY() > FIELD_WIDTH - BALL_RADIUS) { + if (z < GUARDRAIL_HEIGHT) { + ball.pos = + new Translation3d( + ball.pos.getX(), FIELD_WIDTH - BALL_RADIUS, ball.pos.getZ()); + if (ball.vel.getY() > 0) { + applyWallSpinTransfer(ball, AXIS_Y_NEG); + double cor = effectiveCOR(COR_WALL, Math.abs(ball.vel.getY())); + ball.vel = + new Translation3d( + ball.vel.getX(), -ball.vel.getY() * cor, ball.vel.getZ()); + } + } + } + } + + private void handleGroundContact(SimBall ball, double subDt) { + if (ball.pos.getZ() < BALL_RADIUS) { + ball.pos = new Translation3d(ball.pos.getX(), ball.pos.getY(), BALL_RADIUS); + + if (ball.vel.getZ() < 0) { + double cor = effectiveCOR(COR_CARPET, Math.abs(ball.vel.getZ())); + + // If bounce would be very small, just zero out vertical velocity + if (Math.abs(ball.vel.getZ() * cor) < 0.05) { + ball.vel = new Translation3d(ball.vel.getX(), ball.vel.getY(), 0); + } else { + ball.vel = + new Translation3d( + ball.vel.getX(), ball.vel.getY(), -ball.vel.getZ() * cor); + } + } + + // Ground friction: uses surface velocity (vel + omega x r) so spin-on-contact works + if (config.frictionEnabled) { + double surfVelX = ball.vel.getX() - ball.omega.getY() * BALL_RADIUS; + double surfVelY = ball.vel.getY() + ball.omega.getX() * BALL_RADIUS; + double surfSpeed = Math.sqrt(surfVelX * surfVelX + surfVelY * surfVelY); + double hSpeed = + Math.sqrt( + ball.vel.getX() * ball.vel.getX() + + ball.vel.getY() * ball.vel.getY()); + + if (surfSpeed > 0.01) { + // friction opposes surface velocity + double fdx = -surfVelX / surfSpeed; + double fdy = -surfVelY / surfSpeed; + + double maxImpulse = (2.0 / 7.0) * BALL_MASS * surfSpeed; + double frictionImpulse = + Math.min(MU_GROUND_KINETIC * BALL_MASS * GRAVITY * subDt, maxImpulse); + + // Friction changes both linear and angular velocity + ball.vel = + new Translation3d( + ball.vel.getX() + fdx * frictionImpulse / BALL_MASS, + ball.vel.getY() + fdy * frictionImpulse / BALL_MASS, + ball.vel.getZ()); + if (config.spinTransferEnabled) { + // Torque + ball.omega = + new Translation3d( + ball.omega.getX() + + BALL_RADIUS + * fdy + * frictionImpulse + / BALL_MOMENT_OF_INERTIA, + ball.omega.getY() + - BALL_RADIUS + * fdx + * frictionImpulse + / BALL_MOMENT_OF_INERTIA, + ball.omega.getZ()); + } + } else if (hSpeed > 1e-4) { + // Rolling resistance decelerates vel and omega together + double decel = MU_GROUND_ROLLING * GRAVITY * subDt; + double scale = Math.max(0, hSpeed - decel) / hSpeed; + ball.vel = + new Translation3d( + ball.vel.getX() * scale, + ball.vel.getY() * scale, + ball.vel.getZ()); + if (config.spinTransferEnabled) { + ball.omega = + new Translation3d( + ball.omega.getX() * scale, + ball.omega.getY() * scale, + ball.omega.getZ()); + } + } + } + + // Settle near-zero vertical velocity when on ground + if (Math.abs(ball.vel.getZ()) < 0.05 && ball.pos.getZ() <= BALL_RADIUS + 0.01) { + ball.vel = new Translation3d(ball.vel.getX(), ball.vel.getY(), 0); + } + } + } + + /** Handle collisions with the tent-shaped bump ramps. */ + private void handleBumpCollisions(SimBall ball) { + for (BumpSegment seg : BUMP_SEGMENTS) { + if (ball.pos.getY() < seg.yStart() || ball.pos.getY() > seg.yEnd()) continue; + + // Parametric projection onto line segment + double px = ball.pos.getX() - seg.xStart(); + double pz = ball.pos.getZ() - seg.zStart(); + double t = (px * seg.lineX() + pz * seg.lineZ()) / (seg.lineLen() * seg.lineLen()); + if (t < 0 || t > 1) continue; + + // Distance from ball center to nearest point on line (in XZ plane) + double nearX = seg.xStart() + t * seg.lineX(); + double nearZ = seg.zStart() + t * seg.lineZ(); + double dx = ball.pos.getX() - nearX; + double dz = ball.pos.getZ() - nearZ; + double dist = Math.sqrt(dx * dx + dz * dz); + + if (dist < BALL_RADIUS) { + double nx = seg.nx(), nz = seg.nz(); + + // Push out + ball.pos = + ball.pos.plus( + new Translation3d( + nx * (BALL_RADIUS - dist), 0, nz * (BALL_RADIUS - dist))); + + // Velocity reflection + double vDotN = ball.vel.getX() * nx + ball.vel.getZ() * nz; + if (vDotN < 0) { + double cor = effectiveCOR(COR_HDPE, Math.abs(vDotN)); + ball.vel = + ball.vel.minus( + new Translation3d( + nx * (1 + cor) * vDotN, 0, nz * (1 + cor) * vDotN)); + } + } + } + } + + // Hub scoring + + private void handleHubScoring(SimBall ball) { + if (blueHub.didScore(ball)) { + ball.pos = blueHub.exit; + ball.vel = blueHub.getDispersalVelocity(rng); + ball.omega = new Translation3d(); + blueHub.score++; + totalScored++; + } else if (redHub.didScore(ball)) { + ball.pos = redHub.exit; + ball.vel = redHub.getDispersalVelocity(rng); + ball.omega = new Translation3d(); + redHub.score++; + totalScored++; + } + } + + /** Hub net collision: catches overshots, but lets balls pass through from behind. */ + private void handleNetCollision(SimBall ball, ScoringTarget hub) { + if (ball.pos.getZ() > NET_HEIGHT_MAX || ball.pos.getZ() < NET_HEIGHT_MIN) return; + if (ball.pos.getY() > hub.center.getY() + NET_WIDTH / 2.0 + || ball.pos.getY() < hub.center.getY() - NET_WIDTH / 2.0) return; + + double netX = hub.center.getX() + NET_OFFSET * hub.exitVelXSign; + double distToNet = ball.pos.getX() - netX; + + if (Math.abs(distToNet) < BALL_RADIUS) { + // Only block balls moving toward the net + boolean movingTowardNet = ball.vel.getX() * hub.exitVelXSign > 0; + if (!movingTowardNet) return; + + // Push ball out of net + double pushDir = distToNet >= 0 ? 1 : -1; + ball.pos = + new Translation3d( + netX + pushDir * BALL_RADIUS, ball.pos.getY(), ball.pos.getZ()); + ball.vel = + new Translation3d( + -ball.vel.getX() * COR_NET, ball.vel.getY() * COR_NET, ball.vel.getZ()); + } + } + + private void handleRobotCollision(SimBall ball, Pose2d robotPose, Translation2d robotVel) { + if (ball.pos.getZ() > bumperHeight) return; + + Translation2d relPos = + new Pose2d(ball.pos.toTranslation2d(), Rotation2d.kZero) + .relativeTo(robotPose) + .getTranslation(); + + double halfL = robotLength / 2.0 + BALL_RADIUS; + double halfW = robotWidth / 2.0 + BALL_RADIUS; + + if (relPos.getX() < -halfL + || relPos.getX() > halfL + || relPos.getY() < -halfW + || relPos.getY() > halfW) { + return; + } + + // Find nearest face and push out + double dxMin = relPos.getX() + halfL; + double dxMax = halfL - relPos.getX(); + double dyMin = relPos.getY() + halfW; + double dyMax = halfW - relPos.getY(); + + double minDist = dxMin; + Translation2d pushDir = new Translation2d(-1, 0); + + if (dxMax < minDist) { + minDist = dxMax; + pushDir = new Translation2d(1, 0); + } + if (dyMin < minDist) { + minDist = dyMin; + pushDir = new Translation2d(0, -1); + } + if (dyMax < minDist) { + minDist = dyMax; + pushDir = new Translation2d(0, 1); + } + + // Rotate push direction back to field frame + pushDir = pushDir.rotateBy(robotPose.getRotation()); + ball.pos = ball.pos.plus(new Translation3d(pushDir.times(minDist))); + + // Velocity reflection + Translation3d normal3d = new Translation3d(pushDir.getX(), pushDir.getY(), 0); + double vDotN = ball.vel.toTranslation2d().dot(pushDir); + double robotVDotN = robotVel.dot(pushDir); + double closingVel = vDotN - robotVDotN; + + if (closingVel < 0) { + ball.vel = ball.vel.minus(normal3d.times((1 + COR_BUMPER) * closingVel)); + } + } + + private void handleIntakePickup(SimBall ball, Pose2d robotPose) { + if (totalIntaked >= hopperSize) return; // hopper is full + for (IntakeZone intake : intakes) { + if (intake.shouldIntake(ball, robotPose, bumperHeight)) { + ball.intaked = true; + totalIntaked++; + return; + } + } + } + + // Sleeping + + private void updateSleepState(SimBall ball) { + double speed = ball.vel.getNorm(); + double omegaMag = ball.omega.getNorm(); + + if (speed < config.sleepVelocityThreshold + && omegaMag < 0.1 + && ball.pos.getZ() <= BALL_RADIUS + 0.01) { + ball.sleepCounter++; + if (ball.sleepCounter >= config.sleepFrameThreshold) { + ball.sleeping = true; + ball.vel = new Translation3d(); + ball.omega = new Translation3d(); + } + } else { + ball.sleepCounter = 0; + ball.sleeping = false; + } + } + + private void wakeBall(SimBall ball) { + ball.sleeping = false; + ball.sleepCounter = 0; + } + + /** Wake up sleeping balls near a point (so launched balls disturb resting ones). */ + private void wakeNearbyBalls(Translation3d pos, double radius) { + double radiusSq = radius * radius; + for (SimBall ball : balls) { + if (ball.sleeping) { + double dx = ball.pos.getX() - pos.getX(); + double dy = ball.pos.getY() - pos.getY(); + double dz = ball.pos.getZ() - pos.getZ(); + if (dx * dx + dy * dy + dz * dz < radiusSq) { + wakeBall(ball); + } + } + } + } + + // Conservation monitor + + private void computeConservationQuantities() { + totalKE = 0; + totalPE = 0; + double mx = 0, my = 0, mz = 0; + + for (SimBall ball : balls) { + double speed = ball.vel.getNorm(); + totalKE += 0.5 * BALL_MASS * speed * speed; + + // Rotational KE + double omegaMag = ball.omega.getNorm(); + totalKE += 0.5 * BALL_MOMENT_OF_INERTIA * omegaMag * omegaMag; + + // Gravitational PE (relative to ground) + totalPE += BALL_MASS * GRAVITY * ball.pos.getZ(); + + // Linear momentum + mx += BALL_MASS * ball.vel.getX(); + my += BALL_MASS * ball.vel.getY(); + mz += BALL_MASS * ball.vel.getZ(); + } + + totalMomentum = new Translation3d(mx, my, mz); + } + + /** 3D cross product. Using Translation3d directly to avoid WPILib Vector conversion. */ + private static Translation3d cross(Translation3d a, Translation3d b) { + return new Translation3d( + a.getY() * b.getZ() - a.getZ() * b.getY(), + a.getZ() * b.getX() - a.getX() * b.getZ(), + a.getX() * b.getY() - a.getY() * b.getX()); + } + + /** COR drops at higher impact speeds (balls deform more and lose more energy). */ + private double velocityCOR(double e0, double impactSpeed) { + if (!config.velocityDependentCOR) return e0; + if (impactSpeed <= COR_VREF) return e0; + return e0 * Math.pow(COR_VREF / impactSpeed, COR_EXPONENT); + } + + /** Get the COR to use, with optional velocity-dependent scaling. */ + private double effectiveCOR(double baseCOR, double impactSpeed) { + if (config.velocityDependentCOR) { + return velocityCOR(baseCOR, impactSpeed); + } + return baseCOR; + } + + /** When a ball hits a wall, friction changes its spin. This handles that. */ + private void applyWallSpinTransfer(SimBall ball, Translation3d wallNormal) { + if (!config.spinTransferEnabled || !config.frictionEnabled) return; + + // Compute tangential velocity at contact point + Translation3d rContact = wallNormal.times(-BALL_RADIUS); + Translation3d surfaceVel = ball.vel.plus(cross(ball.omega, rContact)); + + // Remove normal component + double vn = surfaceVel.dot(wallNormal); + Translation3d vTangent = surfaceVel.minus(wallNormal.times(vn)); + double vTangentMag = vTangent.getNorm(); + + if (vTangentMag > 1e-4) { + // Total normal impulse includes restitution: j_n = m * |v_n| * (1 + e) + double impactSpeed = Math.abs(ball.vel.dot(wallNormal)); + double cor = effectiveCOR(COR_WALL, impactSpeed); + double normalImpulse = BALL_MASS * impactSpeed * (1.0 + cor); + double frictionImpulse = Math.min(MU_WALL * normalImpulse, BALL_MASS * vTangentMag); + Translation3d frictionDir = vTangent.div(vTangentMag).unaryMinus(); + + // Friction affects both linear velocity and spin (Newton's 3rd law) + ball.vel = ball.vel.plus(frictionDir.times(frictionImpulse / BALL_MASS)); + Translation3d torqueImpulse = cross(rContact, frictionDir.times(frictionImpulse)); + ball.omega = ball.omega.plus(torqueImpulse.div(BALL_MOMENT_OF_INERTIA)); + } + } + + /** Grab a contact from the pool (grows the pool if we run out). */ + private Contact allocateContact() { + if (contactPoolIndex >= contactPool.size()) { + Contact c = new Contact(); + contactPool.add(c); + contactPoolIndex++; + return c; + } + return contactPool.get(contactPoolIndex++); + } + + /** Clean up balls that got eaten by intakes or flew out of bounds. */ + private void removeFlaggedBalls() { + balls.removeIf(b -> b.intaked || b.outOfBounds); + } + + // Trajectory prediction + + /** + * Predict where a shot will go (gravity + drag only, no collisions). Returns points you can + * plot in AdvantageScope Field3d. Stops at the ground. + */ + private Translation3d[] predictArc(Translation3d pos, Translation3d vel, int steps, double dt) { + List arc = new ArrayList<>(); + arc.add(pos); + double px = pos.getX(), py = pos.getY(), pz = pos.getZ(); + double vx = vel.getX(), vy = vel.getY(), vz = vel.getZ(); + for (int i = 0; i < steps; i++) { + double speed = Math.sqrt(vx * vx + vy * vy + vz * vz); + double drag = config.dragEnabled && speed > 1e-6 ? DRAG_ACCEL_FACTOR * speed : 0; + vx -= drag * vx * dt; + vy -= drag * vy * dt; + vz -= (GRAVITY + drag * vz) * dt; + px += vx * dt; + py += vy * dt; + pz += vz * dt; + if (pz < BALL_RADIUS) break; + arc.add(new Translation3d(px, py, pz)); + } + return arc.toArray(new Translation3d[0]); + } + + /** Push ball positions and sim stats to NetworkTables for visualization. */ + public void publishPositions() { + // All ball positions (for Field3d rendering) + Translation3d[] positions = new Translation3d[balls.size()]; + for (int i = 0; i < balls.size(); i++) { + positions[i] = balls.get(i).pos; + } + positionPublisher.set(positions); + + // In-flight positions only (airborne balls, can render separately in Field3d) + List inFlight = new ArrayList<>(); + int sleeping = 0; + for (SimBall b : balls) { + if (b.intaked || b.outOfBounds) continue; + if (b.pos.getZ() > BALL_RADIUS + 0.1) { + inFlight.add(b.pos); + } + if (b.sleeping) sleeping++; + } + inFlightPublisher.set(inFlight.toArray(new Translation3d[0])); + + // Last shot arc (predicted trajectory visible in Field3d) + lastShotArcPublisher.set(lastShotArc); + + // Scoring + blueScorePub.set(blueHub.score); + redScorePub.set(redHub.score); + + // Stats + ballCountPub.set(balls.size()); + activeBallsPub.set(balls.size() - sleeping); + sleepingBallsPub.set(sleeping); + contactCountPub.set(contacts.size()); + physicsTimePub.set(lastPhysicsNanos / 1_000_000.0); + computeConservationQuantities(); + totalEnergyPub.set(totalKE + totalPE); + } + + public int getBallCount() { + return balls.size(); + } + + // Only counts balls above ground level + small margin + public int getBallsInFlight() { + int count = 0; + for (SimBall ball : balls) { + if (ball.pos.getZ() > BALL_RADIUS + 0.1) count++; + } + return count; + } + + public int getBallsOnGround() { + int count = 0; + for (SimBall ball : balls) { + if (ball.pos.getZ() <= BALL_RADIUS + 0.1) count++; + } + return count; + } + + public List getBallPositions() { + List positions = new ArrayList<>(balls.size()); + for (SimBall ball : balls) { + positions.add(ball.pos); + } + return positions; + } + + public List getBallVelocities() { + List velocities = new ArrayList<>(balls.size()); + for (SimBall ball : balls) { + velocities.add(ball.vel); + } + return velocities; + } + + public List getBallOmegas() { + List omegas = new ArrayList<>(balls.size()); + for (SimBall ball : balls) { + omegas.add(ball.omega); + } + return omegas; + } + + public PhysicsConfig getConfig() { + return config; + } + + public void setConfig(PhysicsConfig config) { + this.config = config; + if (config.deterministic) { + this.rng = new Random(config.deterministicSeed); + } + } + + /** Lock the RNG seed so tests are repeatable. */ + public void setDeterministic(long seed) { + config.deterministic = true; + config.deterministicSeed = seed; + this.rng = new Random(seed); + } + + // Translational + rotational KE + public double getTotalKineticEnergy() { + computeConservationQuantities(); + return totalKE; + } + + public double getTotalPotentialEnergy() { + computeConservationQuantities(); + return totalPE; + } + + public Translation3d getTotalMomentum() { + computeConservationQuantities(); + return totalMomentum; + } + + public int getTotalLaunched() { + return totalLaunched; + } + + public int getTotalScored() { + return totalScored; + } + + public int getTotalIntaked() { + return totalIntaked; + } + + public double getLastLaunchSpeed() { + return lastLaunchSpeed; + } + + public int getBlueScore() { + return blueHub.score; + } + + public int getRedScore() { + return redHub.score; + } + + public ScoringTarget getBlueHub() { + return blueHub; + } + + public ScoringTarget getRedHub() { + return redHub; + } + + // Package-private, for tests + List getBalls() { + return balls; + } + + public int getSleepingBallCount() { + int count = 0; + for (SimBall ball : balls) { + if (ball.sleeping) count++; + } + return count; + } + + public static double getFieldLength() { + return FIELD_LENGTH; + } + + public static double getFieldWidth() { + return FIELD_WIDTH; + } + + public static double getBallRadius() { + return BALL_RADIUS; + } + + public static double getBallMass() { + return BALL_MASS; + } + + public static double getMomentOfInertia() { + return BALL_MOMENT_OF_INERTIA; + } + + public static double getDragAccelFactor() { + return DRAG_ACCEL_FACTOR; + } + + public static double getMagnusAccelFactor() { + return MAGNUS_ACCEL_FACTOR; + } + + public static double getFieldCOR() { + return COR_CARPET; + } + + public static double getBallBallCOR() { + return COR_BALL_BALL; + } + + public void resetCounters() { + totalLaunched = 0; + totalScored = 0; + totalIntaked = 0; + lastLaunchSpeed = 0; + blueHub.resetScore(); + redHub.resetScore(); + } +} diff --git a/src/main/java/frc/rebuilt/RobotBumpSim.java b/src/main/java/frc/rebuilt/RobotBumpSim.java new file mode 100644 index 00000000..daa24bc9 --- /dev/null +++ b/src/main/java/frc/rebuilt/RobotBumpSim.java @@ -0,0 +1,472 @@ +// Copyright (c) 2025-2026 KAISER 6989 +// https://github.com/haar09/FRC-Rebuilt-BumpSim +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file at +// the root directory of this project. +// +// Claude Sonnet 4.6 is used for code generation and refactoring. + +package frc.rebuilt; + +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.math.kinematics.ChassisSpeeds; + +/** + * RobotBumpSim — standalone robot-bump physics simulation for MapleSim. + * + *

What this class does

+ * + *

Simulates the robot's 3D pose (Z height, pitch, and roll) as it drives over the raised bumps + * on the 2026 FRC REBUILT field. It also implements a frictionless-slide model that prevents the + * robot from "ghosting" through a bump when it lacks the speed to crest it. + * + *

How it works

+ * + *
    + *
  • Four swerve module contact points are tracked independently in the XZ plane. + *
  • When any module first touches a bump's ascending face the sim enters ramp mode: it + * captures the robot's current field-X velocity ({@code simXVel}) and absolute field-X + * position ({@code simXPos}), then owns them for the duration of the crossing attempt. + *
  • While on the ramp, only gravity acts along X (frictionless surface). If {@code simXVel} + * decays to zero before the peak the robot slides back to flat ground. + *
  • The robot exits ramp mode when it either backs off (all module Z ≈ 0) or successfully + * crosses over the peak (modules come back to flat ground on the far side). + *
  • A diagonal approach reduces effective deceleration via {@code contactFactor = |cos(2 * + * robotYaw)|}, making 45° crossings easier. + *
+ * + *

Minimum crossing speed: {@code sqrt(2·g·h) ≈ sqrt(2·9.81·0.165) ≈ 1.80 m/s}. + * + *

Quick-start integration with MapleSim

+ * + *
{@code
+ * // 1. Construct once, typically in startSimThread() alongside your MapleSim drivetrain.
+ * RobotBumpSim robotBumpSim = new RobotBumpSim(drivetrain.getModuleLocations());
+ *
+ * // 2. Call every simulationPeriodic() AFTER MapleSim has stepped.
+ * Pose2d simPose = mapleSimDrive.getSimulatedDriveTrainPose();
+ * ChassisSpeeds fieldRelativeSpeeds = mapleSimSwerveDrivetrain.mapleSimDrive.getDriveTrainSimulatedChassisSpeedsFieldRelative();
+ *
+ * // subticks should match the number of physics sub-steps you pass to your ball/object sim.
+ * // A value of 5 works well for a 20 ms loop (4 ms sub-steps).
+ * Pose3d simPose3d = robotBumpSim.update(simPose, fieldSpeeds, subticks);
+ *
+ * // 3. When on the ramp, override MapleSim's pose so the robot physically slides back
+ * //    instead of the correction being purely visual.
+ * if (robotBumpSim.isOnRamp()) {
+ *     mapleSimDrive.setSimulationWorldPose(robotBumpSim.getSimWorldPose(simPose));
+ * }
+ *
+ * // 4. Log or visualise the 3D pose (e.g. with AdvantageScope).
+ * Logger.recordOutput("Drive/Pose3d", simPose3d);
+ * }
+ * + *

Tunable constants

+ * + *
    + *
  • {@link #WHEEL_RADIUS} — effective contact radius of a wheel against the ramp surface + * (metres). Increase this to make the robot appear to "float" higher above the bump. + *
  • {@link #CHASSIS_HEIGHT} — offset from the average module-contact Z to the robot body + * origin. Set to your chassis clearance height if you want a visually accurate robot body Z. + *
  • {@link #BUMP_COR} — coefficient of restitution for vertical collisions with the bump. 0 = + * perfectly inelastic (no bounce), 1 = perfectly elastic. + *
+ * + *

Field geometry

+ * + *

The bump geometry is encoded as XZ line segments with Y-range guards. Each bump has two + * segments per side (ascending face + descending face) at X positions symmetric about field centre. + * See {@link #BUMP_LINE_STARTS} / {@link #BUMP_LINE_ENDS} for the raw coordinates. All positions + * are in metres, origin at the Blue Alliance driver-station corner. + */ +public class RobotBumpSim { + + // ------------------------------------------------------------------------- + // Field / physics constants + // ------------------------------------------------------------------------- + + /** Robot control-loop period (seconds). Matches the WPILib default of 20 ms. */ + private static final double PERIOD = 0.02; + + /** Gravitational acceleration vector (m/s², pointing in the -Z direction). */ + private static final Translation3d GRAVITY = new Translation3d(0, 0, -9.81); + + /** Full length of the REBUILT field (metres). */ + private static final double FIELD_LENGTH = 16.51; + + /** Full width of the REBUILT field (metres). */ + private static final double FIELD_WIDTH = 8.04; + + /** + * Start points of the eight bump XZ line segments (four per alliance, ascending + descending). + * + *

Each {@link Translation3d} stores {@code (fieldX, yMin, fieldZ)}: the world-X start of the + * ramp face, the minimum field-Y at which this segment is present, and the ramp Z at that X. + * The companion {@link #BUMP_LINE_ENDS} array stores the end point including the maximum Y. + * + *

Indices 0–3 are the Blue-side bump (near X ≈ 3.96–5.18 m); indices 4–7 are the Red-side + * bump (near X ≈ FIELD_LENGTH−5.18 – FIELD_LENGTH−3.96 m). + */ + static final Translation3d[] BUMP_LINE_STARTS = { + // Blue bump — ascending faces (Z rises from 0 → 0.165 m) + new Translation3d(3.96, 1.57, 0), + new Translation3d(3.96, FIELD_WIDTH / 2 + 0.60, 0), + // Blue bump — descending faces (Z falls from 0.165 → 0 m) + new Translation3d(4.61, 1.57, 0.165), + new Translation3d(4.61, FIELD_WIDTH / 2 + 0.60, 0.165), + // Red bump — ascending faces + new Translation3d(FIELD_LENGTH - 5.18, 1.57, 0), + new Translation3d(FIELD_LENGTH - 5.18, FIELD_WIDTH / 2 + 0.60, 0), + // Red bump — descending faces + new Translation3d(FIELD_LENGTH - 4.61, 1.57, 0.165), + new Translation3d(FIELD_LENGTH - 4.61, FIELD_WIDTH / 2 + 0.60, 0.165), + }; + + /** + * End points of the eight bump XZ line segments. + * + *

Each {@link Translation3d} stores {@code (fieldX, yMax, fieldZ)}: the world-X end of the + * ramp face, the maximum field-Y at which this segment is present, and the ramp Z at that X. + */ + static final Translation3d[] BUMP_LINE_ENDS = { + // Blue bump — ascending faces + new Translation3d(4.61, FIELD_WIDTH / 2 - 0.60, 0.165), + new Translation3d(4.61, FIELD_WIDTH - 1.57, 0.165), + // Blue bump — descending faces + new Translation3d(5.18, FIELD_WIDTH / 2 - 0.60, 0), + new Translation3d(5.18, FIELD_WIDTH - 1.57, 0), + // Red bump — ascending faces + new Translation3d(FIELD_LENGTH - 4.61, FIELD_WIDTH / 2 - 0.60, 0.165), + new Translation3d(FIELD_LENGTH - 4.61, FIELD_WIDTH - 1.57, 0.165), + // Red bump — descending faces + new Translation3d(FIELD_LENGTH - 3.96, FIELD_WIDTH / 2 - 0.60, 0), + new Translation3d(FIELD_LENGTH - 3.96, FIELD_WIDTH - 1.57, 0), + }; + + /** Index of the first bump segment in {@link #BUMP_LINE_STARTS} (inclusive). */ + private static final int BUMP_LINE_FIRST = 0; + + /** Index of the last bump segment in {@link #BUMP_LINE_STARTS} (inclusive). */ + private static final int BUMP_LINE_LAST = BUMP_LINE_STARTS.length - 1; + + // ------------------------------------------------------------------------- + // Tunable physics constants — adjust for your robot + // ------------------------------------------------------------------------- + + /** Effective wheel contact radius against the bump surface (metres). */ + private static final double WHEEL_RADIUS = 0.048; + + /** Height offset from the average module-contact Z to the robot-body origin (metres). */ + private static final double CHASSIS_HEIGHT = 0.0; + + /** + * Coefficient of restitution for vertical (Z) robot-bump collisions. 0 = perfectly inelastic + * (no bounce), 1 = perfectly elastic. + */ + private static final double BUMP_COR = 0.15; + + /** + * Floating-point epsilon used to decide whether a projected point falls within a line segment. + * A small positive tolerance prevents false misses caused by rounding when the module lands + * exactly on a segment endpoint. + */ + private static final double SEGMENT_PROJECTION_TOLERANCE = 1e-6; + + // ------------------------------------------------------------------------- + // Per-instance state + // ------------------------------------------------------------------------- + + /** Robot-relative module positions, order: FL(0), FR(1), BL(2), BR(3). */ + private final Translation2d[] moduleOffsets; + + /** Absolute Z position of each module contact point (metres above the floor). */ + private final double[] moduleZPos; + + /** Z velocity of each module contact point (m/s). */ + private final double[] moduleZVel; + + /** Distance between front and back module pairs along the robot X axis (metres). */ + private final double frontBackDist; + + /** Distance between left and right module pairs along the robot Y axis (metres). */ + private final double leftRightDist; + + /** + * True while the robot is on the ramp and this sim owns the field-X position. The caller must + * call {@link #getSimWorldPose(Pose2d)} and apply it to MapleSim via {@code + * setSimulationWorldPose} whenever this is true. + */ + private boolean onRamp = false; + + /** + * Absolute field-X position of the robot while on the ramp (frictionless model). Ignored when + * {@link #onRamp} is false. + */ + private double simXPos = 0.0; + + /** + * Field-X velocity of the robot on the ramp (m/s, frictionless model). Initialized to the + * robot's field-Vx at first ramp contact; decelerated by gravity-along-ramp each subtick. + * Ignored when {@link #onRamp} is false. + */ + private double simXVel = 0.0; + + // ------------------------------------------------------------------------- + // Constructor + // ------------------------------------------------------------------------- + + /** + * Creates a new {@code RobotBumpSim} for a swerve drivetrain. + * + * @param moduleOffsets Robot-relative module positions in order FL, FR, BL, BR (metres). + * Typically obtained via {@code CommandSwerveDrivetrain#getModuleLocations()}. + */ + public RobotBumpSim(Translation2d[] moduleOffsets) { + this.moduleOffsets = moduleOffsets; + this.moduleZPos = new double[4]; + this.moduleZVel = new double[4]; + + double frontX = (moduleOffsets[0].getX() + moduleOffsets[1].getX()) / 2.0; + double backX = (moduleOffsets[2].getX() + moduleOffsets[3].getX()) / 2.0; + frontBackDist = Math.max(Math.abs(frontX - backX), 1e-3); + + double leftY = (moduleOffsets[0].getY() + moduleOffsets[2].getY()) / 2.0; + double rightY = (moduleOffsets[1].getY() + moduleOffsets[3].getY()) / 2.0; + leftRightDist = Math.max(Math.abs(leftY - rightY), 1e-3); + } + + // ------------------------------------------------------------------------- + // Public API + // ------------------------------------------------------------------------- + + /** + * Returns {@code true} while the robot is on the ramp in frictionless-slide mode. + * + *

When {@code true} the caller must call {@link #getSimWorldPose(Pose2d)} to obtain the + * corrected 2D pose and feed it to MapleSim via {@code setSimulationWorldPose}, so the robot + * actually slides backward rather than just appearing to. + */ + public boolean isOnRamp() { + return onRamp; + } + + /** + * Returns the 2D pose that should be set on MapleSim while on the ramp. + * + *

The X coordinate is replaced with the frictionless {@link #simXPos}; Y and rotation are + * taken from {@code latestMaplePose} so MapleSim continues to own lateral motion. + * + * @param latestMaplePose The most recent 2D pose read from MapleSim (used for Y and rotation). + * @return A corrected {@link Pose2d} to pass to {@code setSimulationWorldPose}. + */ + public Pose2d getSimWorldPose(Pose2d latestMaplePose) { + return new Pose2d(simXPos, latestMaplePose.getY(), latestMaplePose.getRotation()); + } + + /** + * Advances the bump simulation by one 20 ms period and returns the robot's 3D pose. + * + *

When the robot is on the ramp the returned pose uses {@link #simXPos} for X, giving a + * physically accurate visual position. The caller must also call {@link + * #getSimWorldPose(Pose2d)} and apply it to MapleSim so the actual simulation position matches + * (see the class-level Javadoc for a complete integration example). + * + * @param robotPose2d Robot's 2D pose from the MapleSim drivetrain. + * @param fieldRelativeSpeeds Field-relative chassis speeds from the MapleSim drivetrain. + * @param subticks Physics sub-steps per period. Must match the {@code subticks} value used by + * any companion ball/object sim so they stay in sync. Typical value: 5 (= 4 ms sub-steps + * per 20 ms loop). + * @return A {@link Pose3d} with physically correct X, Z, pitch, and roll. + */ + public Pose3d update(Pose2d robotPose2d, ChassisSpeeds fieldRelativeSpeeds, int subticks) { + double vx = fieldRelativeSpeeds.vxMetersPerSecond; + double dt = PERIOD / subticks; + + // contactFactor reduces effective gravity deceleration for diagonal crossings. + // yaw = 0° -> |cos(0)| = 1.0 -> full deceleration (straight-on, hardest to cross) + // yaw = 45° -> |cos(90°)| = 0.71 -> reduced deceleration (diagonal, easier) + // yaw = 90° -> |cos(180°)| = 1.0 -> full deceleration (sideways approach also hard) + double contactFactor = Math.abs(Math.cos(2 * robotPose2d.getRotation().getRadians())); + + // Y positions of each module (MapleSim-owned, constant for the whole period) + double[] worldY = new double[4]; + for (int i = 0; i < 4; i++) { + Translation2d wo = moduleOffsets[i].rotateBy(robotPose2d.getRotation()); + worldY[i] = robotPose2d.getY() + wo.getY(); + } + + for (int tick = 0; tick < subticks; tick++) { + // Use simXPos (frictionless) when on ramp, else follow MapleSim's X + double currentRobotX = onRamp ? simXPos : robotPose2d.getX(); + + double gravAccelXSum = 0.0; + int contactCount = 0; + + for (int i = 0; i < 4; i++) { + // Compute this module's world-X from current robot-X + Translation2d wo = moduleOffsets[i].rotateBy(robotPose2d.getRotation()); + double wx = currentRobotX + wo.getX(); + + // Z gravity and integration + moduleZVel[i] += GRAVITY.getZ() * dt; + moduleZPos[i] += moduleZVel[i] * dt; + + // Bump collisions + for (int lineIdx = BUMP_LINE_FIRST; lineIdx <= BUMP_LINE_LAST; lineIdx++) { + double gax = + handleModuleBumpCollision( + i, wx, worldY[i], onRamp ? simXVel : vx, lineIdx); + if (!Double.isNaN(gax)) { + gravAccelXSum += gax; + contactCount++; + } + } + + // Floor + if (moduleZPos[i] < 0.0) { + moduleZPos[i] = 0.0; + if (moduleZVel[i] < 0.0) moduleZVel[i] = -moduleZVel[i] * BUMP_COR; + } + } + + if (contactCount > 0) { + if (!onRamp) { + // First ramp contact: capture current kinematics + onRamp = true; + simXPos = robotPose2d.getX(); + simXVel = vx; + } + // Apply average gravity-along-ramp deceleration (frictionless surface) + double avgGravAccelX = (gravAccelXSum / contactCount) * contactFactor; + simXVel += avgGravAccelX * dt; + simXPos += simXVel * dt; + } else if (onRamp) { + // No ramp contact; check if all modules have settled onto flat ground + boolean allFlat = true; + for (int i = 0; i < 4; i++) { + if (moduleZPos[i] > 0.01) { + allFlat = false; + break; + } + } + if (allFlat) { + // Robot has backed out or crossed — MapleSim reclaims X + onRamp = false; + } else { + // Briefly airborne after cresting the peak: keep sliding under simXVel + simXPos += simXVel * dt; + } + } + } + + return computePose3d(robotPose2d); + } + + // ------------------------------------------------------------------------- + // Private helpers + // ------------------------------------------------------------------------- + + /** + * Handles the XZ-plane bump collision for module {@code moduleIdx} against line segment {@code + * lineIdx}. Applies a Z position correction and Z velocity impulse. Returns the + * gravity-along-ramp X acceleration (m/s²) if in contact, or {@link Double#NaN} if not. + * + *

The returned acceleration is: + * + *

+     *   a_X = -g * normalX * normalZ
+     * 
+ * + * Ascending face (normalX < 0, normalZ > 0): a_X < 0 — gravity pulls robot back.
+ * Descending face (normalX > 0, normalZ > 0): a_X > 0 — gravity helps robot forward. + * + * @param moduleIdx Index of the module (0–3). + * @param worldX Module's world-X position (metres). + * @param worldY Module's world-Y position (metres). + * @param currentXVel The robot's current field-X velocity (simXVel when on ramp, else vx). + * @param lineIdx Index into {@link #BUMP_LINE_STARTS} / {@link #BUMP_LINE_ENDS}. + * @return Gravity-along-ramp X acceleration, or {@link Double#NaN} if not in contact. + */ + private double handleModuleBumpCollision( + int moduleIdx, double worldX, double worldY, double currentXVel, int lineIdx) { + Translation3d lineStart = BUMP_LINE_STARTS[lineIdx]; + Translation3d lineEnd = BUMP_LINE_ENDS[lineIdx]; + + // Y-range guard + if (worldY < lineStart.getY() || worldY > lineEnd.getY()) return Double.NaN; + + // Project into the XZ plane + Translation2d start2d = new Translation2d(lineStart.getX(), lineStart.getZ()); + Translation2d end2d = new Translation2d(lineEnd.getX(), lineEnd.getZ()); + Translation2d pos2d = new Translation2d(worldX, moduleZPos[moduleIdx]); + Translation2d lineVec = end2d.minus(start2d); + + // Closest point on the XZ segment to the module (parametric projection) + Translation2d toModule = pos2d.minus(start2d); + double projectionT = toModule.dot(lineVec) / lineVec.getSquaredNorm(); + Translation2d projected = start2d.plus(lineVec.times(projectionT)); + + if (projected.getDistance(start2d) + projected.getDistance(end2d) + > lineVec.getNorm() + SEGMENT_PROJECTION_TOLERANCE) + return Double.NaN; // off segment + + double dist = pos2d.getDistance(projected); + if (dist > WHEEL_RADIUS) return Double.NaN; // not intersecting + + // Outward normal in XZ: lineVec = (deltaX, deltaZ) -> normal = (-deltaZ, deltaX) / + // |lineVec| + double normalX = -lineVec.getY() / lineVec.getNorm(); + double normalZ = lineVec.getX() / lineVec.getNorm(); + + // Z position correction: push module to WHEEL_RADIUS from ramp surface + moduleZPos[moduleIdx] += normalZ * (WHEEL_RADIUS - dist); + + // Z velocity impulse (visual tilt only) + double velDotNormal = currentXVel * normalX + moduleZVel[moduleIdx] * normalZ; + if (velDotNormal < 0.0) { + moduleZVel[moduleIdx] += normalZ * (-(1.0 + BUMP_COR) * velDotNormal); + } + + // Gravity-along-ramp X acceleration (frictionless surface, no drive force contribution). + // GRAVITY.getZ() = -9.81 m/s². + // Ascending face: normalX < 0, normalZ > 0 + // -> a_X = -(-9.81) * (negative) * (positive) < 0 -- decelerates +X travel + // Descending face: normalX > 0, normalZ > 0 + // -> a_X = -(-9.81) * (positive) * (positive) > 0 -- accelerates +X travel + return -GRAVITY.getZ() * normalX * normalZ; + } + + /** + * Derives a {@link Pose3d} from the robot's 2D pose and the four module Z positions. Pitch and + * roll come from front/back and left/right height differences. X uses {@link #simXPos} when on + * the ramp for visual accuracy. + */ + private Pose3d computePose3d(Pose2d robotPose2d) { + // FL=0, FR=1, BL=2, BR=3 + double frontZ = (moduleZPos[0] + moduleZPos[1]) / 2.0; + double backZ = (moduleZPos[2] + moduleZPos[3]) / 2.0; + double leftZ = (moduleZPos[0] + moduleZPos[2]) / 2.0; + double rightZ = (moduleZPos[1] + moduleZPos[3]) / 2.0; + double centerZ = (frontZ + backZ) / 2.0 + CHASSIS_HEIGHT; + + // Pitch: positive -> nose up (front higher than back) + // Negated because WPILib Rotation3d pitch positive = nose-down (right-hand rule) + double pitch = -Math.atan2(frontZ - backZ, frontBackDist); + // Roll: positive -> left side higher than right side + double roll = Math.atan2(leftZ - rightZ, leftRightDist); + + // X: use frictionless sim position on ramp, else follow MapleSim + double visualX = onRamp ? simXPos : robotPose2d.getX(); + + return new Pose3d( + visualX, + robotPose2d.getY(), + centerZ, + new Rotation3d(roll, pitch, robotPose2d.getRotation().getRadians())); + } +} diff --git a/src/main/java/frc/rebuilt/ShiftHelpers.java b/src/main/java/frc/rebuilt/ShiftHelpers.java index 97c9bf4c..585b3173 100644 --- a/src/main/java/frc/rebuilt/ShiftHelpers.java +++ b/src/main/java/frc/rebuilt/ShiftHelpers.java @@ -113,7 +113,7 @@ private static ShiftInfo getShiftInfo( && fieldTeleopTime <= 135 && DriverStation.isFMSAttached()) { shiftTimerOffset += currentTime - fieldTeleopTime; - currentTime = timerValue + shiftTimerOffset; + currentTime = timerValue - shiftTimerOffset; } int currentShiftIndex = -1; for (int i = 0; i < shiftStartTimes.length; i++) { diff --git a/src/main/java/frc/rebuilt/ShotCalculator.java b/src/main/java/frc/rebuilt/ShotCalculator.java index 5fb98ce0..5734a324 100644 --- a/src/main/java/frc/rebuilt/ShotCalculator.java +++ b/src/main/java/frc/rebuilt/ShotCalculator.java @@ -9,21 +9,23 @@ import edu.wpi.first.math.geometry.Twist2d; import edu.wpi.first.math.interpolation.InterpolatingDoubleTreeMap; import edu.wpi.first.math.kinematics.ChassisSpeeds; -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.Commands; -import frc.rebuilt.launchingMaps.AndyMarkMap; +import edu.wpi.first.math.util.Units; import frc.rebuilt.targetFactories.FeedTargetFactory; import frc.rebuilt.targetFactories.HubTargetFactory; import frc.robot.Robot; -import frc.robot.RobotStates; -import frc.spectrumLib.Telemetry; -import lombok.Getter; +import frc.robot.subsystems.SuperStructure; +import frc.spectrumLib.telemetry.Telemetry; +import java.text.DecimalFormat; public class ShotCalculator { private static ShotCalculator instance; + private final SuperStructure superStructure = Robot.getSuperStructure(); - // Offset from robot center to launcher center (leave zero if launcher is centered) - private static final Transform2d robotToLauncher = Transform2d.kZero; + // Offset from robot center to turret center (leave zero if turret is centered) + private static final Transform2d robotToTurret = + new Transform2d( + new Translation2d(Units.inchesToMeters(-5.5), Units.inchesToMeters(5.0)), + new Rotation2d()); public static ShotCalculator getInstance() { if (instance == null) instance = new ShotCalculator(); @@ -32,37 +34,34 @@ public static ShotCalculator getInstance() { public record ShootingParameters( boolean isValid, - Rotation2d driveAngle, - double driveAngularVelocity, - double hoodAngle, - double hoodVelocity, - double flywheelSpeed, - double distance, - double distanceNoLookahead, - double timeOfFlight) {} + Rotation2d turretAngle, + double turretAngularVelocityRotPerSec, + double flywheelSpeed) {} private ShootingParameters latestParameters = null; - public static final double STARTING_HOOD_ANGLE_OFFSET = 0; // degrees - public static double HOOD_ANGLE_OFFSET = STARTING_HOOD_ANGLE_OFFSET; + private static final DecimalFormat df = new DecimalFormat("0.00"); - public static final double STARTING_DRIVE_ANGLE_OFFSET = 0; // degrees - public static double DRIVE_ANGLE_OFFSET = STARTING_DRIVE_ANGLE_OFFSET; + public static final double STARTING_FLYWHEEL_SPEED_OFFSET = 0; // percent + public static double FLYWHEEL_SPEED_OFFSET = STARTING_FLYWHEEL_SPEED_OFFSET; - public static Command increaseHoodAngleOffset() { - return Commands.runOnce(() -> HOOD_ANGLE_OFFSET += 0.1).ignoringDisable(true); + public static final double STARTING_TURRET_ANGLE_OFFSET_DEGREES = 0; + public static double TURRET_ANGLE_OFFSET_DEGREES = STARTING_TURRET_ANGLE_OFFSET_DEGREES; + + public static void increaseFlywheelSpeedOffset() { + FLYWHEEL_SPEED_OFFSET += 1; } - public static Command decreaseHoodAngleOffset() { - return Commands.runOnce(() -> HOOD_ANGLE_OFFSET -= 0.1).ignoringDisable(true); + public static void decreaseFlywheelSpeedOffset() { + FLYWHEEL_SPEED_OFFSET -= 1; } - public static Command increaseDriveAngleOffset() { - return Commands.runOnce(() -> DRIVE_ANGLE_OFFSET += 1).ignoringDisable(true); + public static void increaseTurretAngleOffsetDegrees() { + TURRET_ANGLE_OFFSET_DEGREES += 1; } - public static Command decreaseDriveAngleOffset() { - return Commands.runOnce(() -> DRIVE_ANGLE_OFFSET -= 1).ignoringDisable(true); + public static void decreaseTurretAngleOffsetDegrees() { + TURRET_ANGLE_OFFSET_DEGREES -= 1; } // ===== Config / maps ===== @@ -70,44 +69,63 @@ public static Command decreaseDriveAngleOffset() { private static double maxDistance; private static double phaseDelay; - @Getter private static InterpolatingDoubleTreeMap hoodAngleMap = AndyMarkMap.getHoodAngleMap(); - - @Getter - private static InterpolatingDoubleTreeMap launcherSpeedMap = AndyMarkMap.getLauncherSpeedMap(); + private static final InterpolatingDoubleTreeMap shotFlywheelSpeedMap = + new InterpolatingDoubleTreeMap(); - @Getter - private static InterpolatingDoubleTreeMap timeOfFlightMap = new InterpolatingDoubleTreeMap(); + private static final InterpolatingDoubleTreeMap timeOfFlightMap = + new InterpolatingDoubleTreeMap(); - // ===== Velocity and angle calculation ===== + // ===== Turret angular velocity calculation ===== + // If you have a known loop period constant, swap it in here. + // WPILib TimedRobot default is 0.02s, but use your actual period. private static final double loopPeriodSecs = 0.02; - private final LinearFilter hoodAngleFilter = - LinearFilter.movingAverage((int) (0.1 / loopPeriodSecs)); // ~100ms window - - private final LinearFilter driveAngleFilter = + private final LinearFilter turretOmegaFilter = LinearFilter.movingAverage((int) (0.1 / loopPeriodSecs)); // ~100ms window - private double lastHoodAngle = Double.NaN; - private Rotation2d lastDriveAngle = null; + private Rotation2d lastTurretAngle = null; static { minDistance = 1.34; maxDistance = 5.60; + phaseDelay = 0.03; - // TOF map (in seconds) - timeOfFlightMap.put(5.68, 1.10); - timeOfFlightMap.put(4.55, 1.07); - timeOfFlightMap.put(3.15, 1.05); - timeOfFlightMap.put(1.88, 1.00); - timeOfFlightMap.put(1.38, 0.86); + // Flywheel map + shotFlywheelSpeedMap.put(1.50, 2250.0 + 100); + shotFlywheelSpeedMap.put(1.78, 2300.0 + 100); + shotFlywheelSpeedMap.put(2.00, 2450.0 + 100); + shotFlywheelSpeedMap.put(2.35, 2600.0 + 100); + shotFlywheelSpeedMap.put(2.56, 2650.0 + 100); + shotFlywheelSpeedMap.put(2.96, 2750.0 + 100); + shotFlywheelSpeedMap.put(3.16, 2900.0 + 100); + shotFlywheelSpeedMap.put(3.50, 3200.0 + 100); + shotFlywheelSpeedMap.put(4.00, 3300.0 + 100); + shotFlywheelSpeedMap.put(4.20, 3650.0 + 100); + shotFlywheelSpeedMap.put(5.00, 4000.0 + 100); + + // TOF map + timeOfFlightMap.put(3.41, 1.10); + timeOfFlightMap.put(3.08, 1.07); + timeOfFlightMap.put(2.75, 1.05); + timeOfFlightMap.put(2.33, 0.95); + timeOfFlightMap.put(2.03, 0.85); + timeOfFlightMap.put(1.68, 0.76); } public ShootingParameters getParameters() { if (latestParameters != null) return latestParameters; // Target selection - boolean feed = RobotStates.robotInFeedZone.getAsBoolean(); + boolean feed = + superStructure.isRobotInFeedZone() + && (!SuperStructure.CurrentSuperState.LAUNCH_WITH_SQUEEZE.equals( + Robot.getSuperStructure().getCurrentSuperState()) + || !SuperStructure.CurrentSuperState.LAUNCH_WITHOUT_SQUEEZE.equals( + Robot.getSuperStructure().getCurrentSuperState()) + || !SuperStructure.CurrentSuperState + .LAUNCH_WITH_SQUEEZE_WITH_NO_DELAY + .equals(Robot.getSuperStructure().getCurrentSuperState())); Translation2d target = feed ? FeedTargetFactory.generate() : HubTargetFactory.generate().toTranslation2d(); @@ -121,40 +139,39 @@ public ShootingParameters getParameters() { robotRelativeVelocity.vyMetersPerSecond * phaseDelay, robotRelativeVelocity.omegaRadiansPerSecond * phaseDelay)); - // Launcher pose + base distance - Pose2d launcherPose = estimatedPose.transformBy(robotToLauncher); - double launcherToTargetDistance = target.getDistance(launcherPose.getTranslation()); - double distanceNoLookahead = launcherToTargetDistance; + // Turret pose + base distance + Pose2d turretPose = estimatedPose.transformBy(robotToTurret); + double turretToTargetDistance = target.getDistance(turretPose.getTranslation()); // Field-relative velocity of robot ChassisSpeeds fieldVelocity = ChassisSpeeds.fromRobotRelativeSpeeds( robotRelativeVelocity, estimatedPose.getRotation()); - // Launcher tangential velocity due to robot rotation about robot center + // Turret tangential velocity due to robot rotation about robot center double robotAngle = estimatedPose.getRotation().getRadians(); - double launcherVelocityX = + double turretVelocityX = fieldVelocity.vxMetersPerSecond + fieldVelocity.omegaRadiansPerSecond - * (robotToLauncher.getY() * Math.cos(robotAngle) - - robotToLauncher.getX() * Math.sin(robotAngle)); - double launcherVelocityY = + * (robotToTurret.getY() * Math.cos(robotAngle) + - robotToTurret.getX() * Math.sin(robotAngle)); + double turretVelocityY = fieldVelocity.vyMetersPerSecond + fieldVelocity.omegaRadiansPerSecond - * (robotToLauncher.getX() * Math.cos(robotAngle) - - robotToLauncher.getY() * Math.sin(robotAngle)); + * (robotToTurret.getX() * Math.cos(robotAngle) + - robotToTurret.getY() * Math.sin(robotAngle)); // Lookahead iteration: converge distance - double lookaheadDistance = launcherToTargetDistance; + double lookaheadDistance = turretToTargetDistance; for (int i = 0; i < 20; i++) { double tof = timeOfFlightMap.get(lookaheadDistance); - double offsetX = launcherVelocityX * tof; - double offsetY = launcherVelocityY * tof; + double offsetX = turretVelocityX * tof; + double offsetY = turretVelocityY * tof; - Translation2d lookaheadLauncherTranslation = - launcherPose.getTranslation().plus(new Translation2d(offsetX, offsetY)); + Translation2d lookaheadTurretTranslation = + turretPose.getTranslation().plus(new Translation2d(offsetX, offsetY)); - double newDistance = target.getDistance(lookaheadLauncherTranslation); + double newDistance = target.getDistance(lookaheadTurretTranslation); if (Math.abs(newDistance - lookaheadDistance) < 0.01) { lookaheadDistance = newDistance; break; @@ -162,63 +179,49 @@ public ShootingParameters getParameters() { lookaheadDistance = newDistance; } - // Final compensated launcher translation using final TOF + // Final compensated turret translation using final TOF double tofFinal = timeOfFlightMap.get(lookaheadDistance); - Translation2d compensatedLauncherTranslation = - launcherPose + Translation2d compensatedTurretTranslation = + turretPose .getTranslation() .plus( new Translation2d( - launcherVelocityX * tofFinal, - launcherVelocityY * tofFinal)); + turretVelocityX * tofFinal, turretVelocityY * tofFinal)); - // Commanded drive angle (robot angle to aim launcher at target) - Rotation2d driveAngle = target.minus(compensatedLauncherTranslation).getAngle(); - driveAngle = driveAngle.plus(Rotation2d.fromDegrees(DRIVE_ANGLE_OFFSET)); + // Commanded turret angle (with preference offset) + Rotation2d turretAngle = target.minus(compensatedTurretTranslation).getAngle(); + turretAngle = turretAngle.plus(Rotation2d.fromDegrees(TURRET_ANGLE_OFFSET_DEGREES)); - // Drive angular velocity (rad/s) for your position controller feedforward - if (lastDriveAngle == null) lastDriveAngle = driveAngle; + // Turret angular velocity (rot/s) for your position controller feedforward + if (lastTurretAngle == null) lastTurretAngle = turretAngle; double deltaRot = - MathUtil.inputModulus(driveAngle.minus(lastDriveAngle).getRotations(), -0.5, 0.5); - - double rawDriveOmega = deltaRot / loopPeriodSecs; - double driveAngularVelocity = driveAngleFilter.calculate(rawDriveOmega); - lastDriveAngle = driveAngle; - - // Hood angle from map with offset - double hoodAngle = hoodAngleMap.get(lookaheadDistance); - if (Double.isNaN(lastHoodAngle)) lastHoodAngle = hoodAngle; - double hoodVelocity = - hoodAngleFilter.calculate((hoodAngle - lastHoodAngle) / loopPeriodSecs); - lastHoodAngle = hoodAngle; + MathUtil.inputModulus(turretAngle.minus(lastTurretAngle).getRotations(), -0.5, 0.5); - // Apply hood angle offset - hoodAngle += HOOD_ANGLE_OFFSET; + double rawOmega = deltaRot / loopPeriodSecs; + double turretAngularVelocityRotPerSec = turretOmegaFilter.calculate(rawOmega); + lastTurretAngle = turretAngle; // Flywheel from map + preference offset (%) - double flywheelSpeed = launcherSpeedMap.get(lookaheadDistance); + double flywheelSpeed = shotFlywheelSpeedMap.get(lookaheadDistance); + flywheelSpeed += flywheelSpeed * (FLYWHEEL_SPEED_OFFSET / 100.0); boolean isValid = lookaheadDistance >= minDistance && lookaheadDistance <= maxDistance; latestParameters = new ShootingParameters( - isValid, - driveAngle, - driveAngularVelocity, - hoodAngle, - hoodVelocity, - flywheelSpeed, - lookaheadDistance, - distanceNoLookahead, - tofFinal); - - Telemetry.log("ShotCalc/DistanceMeters", lookaheadDistance, "meters"); - Telemetry.log("ShotCalc/DriveAngleDeg", driveAngle.getDegrees(), "degrees"); - Telemetry.log("ShotCalc/HoodAngleDeg", hoodAngle, "degrees"); - Telemetry.log("ShotCalc/FlywheelSpeedRPM", flywheelSpeed, "RPM"); - Telemetry.log("ShotCalc/DriveAngleOffsetDegrees", DRIVE_ANGLE_OFFSET, "degrees"); - Telemetry.log("ShotCalc/HoodAngleOffsetDegrees", HOOD_ANGLE_OFFSET, "degrees"); - Telemetry.log("ShotCalc/Target", target); + isValid, turretAngle, turretAngularVelocityRotPerSec, flywheelSpeed); + + Telemetry.log("ShotCalc/IsValid", isValid); + Telemetry.log("ShotCalc/DistanceMeters", df.format(lookaheadDistance)); + Telemetry.log("ShotCalc/TurretAngleDeg", df.format(turretAngle.getDegrees())); + Telemetry.log("ShotCalc/TurretOmegaRadPerSec", df.format(turretAngularVelocityRotPerSec)); + Telemetry.log("ShotCalc/FlywheelSpeedRPM", df.format(flywheelSpeed)); + Telemetry.log("ShotCalc/TurretPose", turretPose); + Telemetry.log("ShotCalc/LookaheadPose", compensatedTurretTranslation); + Telemetry.log( + "ShotCalc/TargetPose", new Pose2d(target.getX(), target.getY(), new Rotation2d())); + Telemetry.log("ShotCalc/FlywheelSpeedOffset", FLYWHEEL_SPEED_OFFSET); + Telemetry.log("ShotCalc/TurretAngleOffsetDegrees", TURRET_ANGLE_OFFSET_DEGREES); return latestParameters; } diff --git a/src/main/java/frc/rebuilt/TagProperties.java b/src/main/java/frc/rebuilt/TagProperties.java deleted file mode 100644 index 05336a1b..00000000 --- a/src/main/java/frc/rebuilt/TagProperties.java +++ /dev/null @@ -1,74 +0,0 @@ -package frc.rebuilt; - -import edu.wpi.first.math.util.Units; -import frc.robot.Robot; -import lombok.Getter; - -public class TagProperties { - @Getter private final double[] frontOffset = new double[2]; - @Getter private final double[] rearOffset = new double[2]; - @Getter private final double[] frontCenterOffset = new double[2]; - @Getter private final double[] rearCenterOffset = new double[2]; - @Getter private final double taGoal; - @Getter private final double angle; - - /** - * @param frontOffsetInchesLeft the front left offset in inches from the center of the robot to - * the tag - * @param frontOffsetInchesRight the front right offset in inches from the center of the robot - * to the tag - * @param rearOffsetInchesLeft the rear left offset in inches from the center of the robot to - * the tag - * @param rearOffsetInchesRight the rear right offset in inches from the center of the robot to - * the tag - * @param frontCenterOffsetInchesLeft the front left offset in inches from the center of the - * robot to the center of the tag - * @param frontCenterOffsetInchesRight the front right offset in inches from the center of the - * robot to the center of the tag - * @param rearCenterOffsetInchesLeft the rear left offset in inches from the center of the robot - * to the center of the tag - * @param rearCenterOffsetInchesRight the rear right offset in inches from the center of the - * robot to the center of the tag - * @param taGoal the target area goal for the tag - * @param angleDegrees the angle in degrees from the robot to the tag - */ - public TagProperties( - double frontOffsetInchesLeft, - double frontOffsetInchesRight, - double rearOffsetInchesLeft, - double rearOffsetInchesRight, - double frontCenterOffsetInchesLeft, - double frontCenterOffsetInchesRight, - double rearCenterOffsetInchesLeft, - double rearCenterOffsetInchesRight, - double taGoal, - double angleDegrees) { - - frontOffset[0] = meterOffsetWithRobot(frontOffsetInchesLeft); - frontOffset[1] = meterOffsetWithRobot(frontOffsetInchesRight); - rearOffset[0] = meterOffsetWithRobot(rearOffsetInchesLeft); - rearOffset[1] = meterOffsetWithRobot(rearOffsetInchesRight); - frontCenterOffset[0] = Units.inchesToMeters(frontCenterOffsetInchesLeft); - frontCenterOffset[1] = Units.inchesToMeters(frontCenterOffsetInchesRight); - rearCenterOffset[0] = Units.inchesToMeters(rearCenterOffsetInchesLeft); - rearCenterOffset[1] = Units.inchesToMeters(rearCenterOffsetInchesRight); - this.taGoal = taGoal; - this.angle = radianConverter(angleDegrees); - } - - private static double meterOffsetWithRobot(double offsetInches) { - double meterConversion = Units.inchesToMeters(offsetInches); - - return addRobotLength(meterConversion); - } - - private static double addRobotLength(double meterOffset) { - double halfRobotLength = Robot.getSwerve().getConfig().getRobotLength() / 2; - - return meterOffset + halfRobotLength; - } - - private static double radianConverter(double offsetDegrees) { - return Units.degreesToRadians(offsetDegrees); - } -} diff --git a/src/main/java/frc/rebuilt/Zones.java b/src/main/java/frc/rebuilt/Zones.java deleted file mode 100644 index 941a07c8..00000000 --- a/src/main/java/frc/rebuilt/Zones.java +++ /dev/null @@ -1,14 +0,0 @@ -package frc.rebuilt; - -import edu.wpi.first.wpilibj2.command.button.Trigger; -import frc.robot.Robot; -import frc.robot.swerve.Swerve; - -public class Zones { - - private static final Swerve swerve = Robot.getSwerve(); - - public static final Trigger blueFieldSide = swerve.inXzone(0, Field.getHalfLength()); - public static final Trigger opponentFieldSide = - new Trigger(() -> blueFieldSide.getAsBoolean() != Field.isBlue()); -} diff --git a/src/main/java/frc/rebuilt/launchingMaps/AndyMarkMap.java b/src/main/java/frc/rebuilt/launchingMaps/AndyMarkMap.java deleted file mode 100644 index 3dff4692..00000000 --- a/src/main/java/frc/rebuilt/launchingMaps/AndyMarkMap.java +++ /dev/null @@ -1,50 +0,0 @@ -package frc.rebuilt.launchingMaps; - -import edu.wpi.first.math.interpolation.InterpolatingDoubleTreeMap; -import lombok.Getter; - -public class AndyMarkMap { - - @Getter - private static final InterpolatingDoubleTreeMap hoodAngleMap = new InterpolatingDoubleTreeMap(); - - @Getter - private static final InterpolatingDoubleTreeMap launcherSpeedMap = - new InterpolatingDoubleTreeMap(); - - static { - hoodAngleMap.put(1.85, 9.5); - hoodAngleMap.put(2.00, 10.5); - hoodAngleMap.put(2.35, 12.3); - hoodAngleMap.put(2.55, 13.8); - hoodAngleMap.put(2.65, 14.5); - hoodAngleMap.put(2.96, 16.0); - hoodAngleMap.put(3.30, 19.5); - - hoodAngleMap.put(3.31, 19.6); - hoodAngleMap.put(3.65, 22.3); - hoodAngleMap.put(4.00, 24.0); - - hoodAngleMap.put(4.01, 20.5); - hoodAngleMap.put(4.20, 22.0); - hoodAngleMap.put(4.50, 22.0); - - hoodAngleMap.put(5.20, 30.0); - hoodAngleMap.put(5.60, 40.0); - - /* Flywheel map (in RPM) */ - // Near Trench - launcherSpeedMap.put(0.00, 2000.0); - launcherSpeedMap.put(3.30, 2000.0); - - // Near Tower - launcherSpeedMap.put(3.31, 2000.0); - launcherSpeedMap.put(4.00, 2000.0); - - launcherSpeedMap.put(4.01, 2400.0); - launcherSpeedMap.put(8.99, 2400.0); - - launcherSpeedMap.put(9.99, 4000.0); - launcherSpeedMap.put(20.00, 4000.0); - } -} diff --git a/src/main/java/frc/rebuilt/launchingMaps/HomeMap.java b/src/main/java/frc/rebuilt/launchingMaps/HomeMap.java deleted file mode 100644 index aa9f287a..00000000 --- a/src/main/java/frc/rebuilt/launchingMaps/HomeMap.java +++ /dev/null @@ -1,43 +0,0 @@ -package frc.rebuilt.launchingMaps; - -import edu.wpi.first.math.interpolation.InterpolatingDoubleTreeMap; -import lombok.Getter; - -public class HomeMap { - - @Getter - private static final InterpolatingDoubleTreeMap hoodAngleMap = new InterpolatingDoubleTreeMap(); - - @Getter - private static final InterpolatingDoubleTreeMap launcherSpeedMap = - new InterpolatingDoubleTreeMap(); - - static { - - /* Hood angle map (in degrees from horizontal) */ - hoodAngleMap.put(1.34, 16.0); - hoodAngleMap.put(2.00, 17.0); - hoodAngleMap.put(2.35, 19.0); - hoodAngleMap.put(2.65, 21.0); - hoodAngleMap.put(2.96, 23.0); - hoodAngleMap.put(3.23, 25.0); - hoodAngleMap.put(3.65, 26.0); - hoodAngleMap.put(4.00, 28.0); - hoodAngleMap.put(4.20, 30.0); - hoodAngleMap.put(4.50, 32.0); - hoodAngleMap.put(5.60, 33.0); - - /* Flywheel map (in RPM) */ - // Near Trench - launcherSpeedMap.put(0.00, 1800.0); - launcherSpeedMap.put(3.30, 1800.0); - - // Near Tower - launcherSpeedMap.put(3.31, 2000.0); - launcherSpeedMap.put(5.00, 2000.0); - - // Feeding shots - launcherSpeedMap.put(5.01, 2300.0); - launcherSpeedMap.put(6.00, 2400.0); - } -} diff --git a/src/main/java/frc/rebuilt/offsets/HomeOffsets.java b/src/main/java/frc/rebuilt/offsets/HomeOffsets.java deleted file mode 100644 index 78fe6fa5..00000000 --- a/src/main/java/frc/rebuilt/offsets/HomeOffsets.java +++ /dev/null @@ -1,3 +0,0 @@ -package frc.rebuilt.offsets; - -public class HomeOffsets {} diff --git a/src/main/java/frc/rebuilt/targetFactories/FeedTargetFactory.java b/src/main/java/frc/rebuilt/targetFactories/FeedTargetFactory.java index f4c4ce98..832b7b7b 100644 --- a/src/main/java/frc/rebuilt/targetFactories/FeedTargetFactory.java +++ b/src/main/java/frc/rebuilt/targetFactories/FeedTargetFactory.java @@ -7,7 +7,7 @@ import edu.wpi.first.math.util.Units; import frc.rebuilt.Field; import frc.robot.Robot; -import frc.robot.swerve.Swerve; +import frc.robot.subsystems.swerve.Swerve; public class FeedTargetFactory { diff --git a/src/main/java/frc/robot/Coordinator.java b/src/main/java/frc/robot/Coordinator.java deleted file mode 100644 index 94c042ac..00000000 --- a/src/main/java/frc/robot/Coordinator.java +++ /dev/null @@ -1,139 +0,0 @@ -package frc.robot; - -import frc.robot.fuelIntake.FuelIntakeStates; -import frc.robot.hood.HoodStates; -import frc.robot.indexerBed.IndexerBedStates; -import frc.robot.indexerTower.IndexerTowerStates; -import frc.robot.intakeExtension.IntakeExtensionStates; -import frc.robot.launcher.LauncherStates; - -public class Coordinator { - - public void update() {} - - public void applyRobotState(State state) { - switch (state) { - case IDLE -> { - FuelIntakeStates.stop(); - IndexerTowerStates.neutral(); - IndexerBedStates.neutral(); - IntakeExtensionStates.neutral(); - LauncherStates.idlePrep(); - HoodStates.home(); - } - case INTAKE_FUEL -> { - FuelIntakeStates.intakeFuel(); - IndexerTowerStates.neutral(); - IndexerBedStates.slowIndex(); - IntakeExtensionStates.fullExtend(); - LauncherStates.idlePrep(); - HoodStates.home(); - } - case TRACK_TARGET -> { - FuelIntakeStates.stop(); - IndexerTowerStates.neutral(); - IndexerBedStates.neutral(); - IntakeExtensionStates.fullExtendConditional(); - LauncherStates.aimAtTarget(); - HoodStates.aimAtTarget(); - } - case TRACK_TARGET_WITH_NO_SWERVE -> { - FuelIntakeStates.stop(); - IndexerTowerStates.neutral(); - IndexerBedStates.neutral(); - IntakeExtensionStates.fullExtendConditional(); - LauncherStates.aimAtTarget(); - HoodStates.aimAtTarget(); - } - case LAUNCH_WITH_SQUEEZE -> { - FuelIntakeStates.intakeFuel(); - IndexerTowerStates.indexMax(); - IndexerBedStates.indexMax(); - IntakeExtensionStates.slowIntakeCloseWithDelay(); - LauncherStates.aimAtTarget(); - HoodStates.aimAtTarget(); - } - case LAUNCH_WITH_SQUEEZE_WITH_NO_DELAY -> { - FuelIntakeStates.intakeFuel(); - IndexerTowerStates.indexMax(); - IndexerBedStates.indexMax(); - IntakeExtensionStates.slowIntakeCloseWithoutDelay(); - LauncherStates.aimAtTarget(); - HoodStates.aimAtTarget(); - } - case LAUNCH_WITHOUT_SQUEEZE -> { - FuelIntakeStates.intakeFuel(); - IndexerTowerStates.indexMax(); - IndexerBedStates.indexMax(); - IntakeExtensionStates.fullExtendConditional(); - LauncherStates.aimAtTarget(); - HoodStates.aimAtTarget(); - } - case AUTON_TRACK_TARGET -> { - FuelIntakeStates.stop(); - IndexerTowerStates.neutral(); - IndexerBedStates.neutral(); - IntakeExtensionStates.fullExtendConditional(); - LauncherStates.autonAimAtTarget(); - HoodStates.autonAimAtTarget(); - } - case AUTON_LAUNCH_WITH_SQUEEZE -> { - FuelIntakeStates.intakeFuel(); - IndexerTowerStates.indexMax(); - IndexerBedStates.indexMax(); - IntakeExtensionStates.slowIntakeCloseWithDelay(); - LauncherStates.autonAimAtTarget(); - HoodStates.autonAimAtTarget(); - } - case UNJAM -> { - FuelIntakeStates.stop(); - IndexerTowerStates.unjam(); - IndexerBedStates.unjam(); - IntakeExtensionStates.fullExtendConditional(); - LauncherStates.neutral(); - HoodStates.home(); - } - case FORCE_HOME -> { - FuelIntakeStates.stop(); - IndexerTowerStates.neutral(); - IndexerBedStates.neutral(); - IntakeExtensionStates.fullRetract(); - LauncherStates.neutral(); - HoodStates.home(); - } - case CUSTOM_SPEED_TURRET_LAUNCH -> { - FuelIntakeStates.stop(); - IndexerTowerStates.indexMax(); - IndexerBedStates.indexMax(); - IntakeExtensionStates.fullExtendConditional(); - LauncherStates.customLaunchSpeed(); - HoodStates.aimAtTarget(); - } - case TEST_INFINITE_LAUNCH -> { - FuelIntakeStates.slowIntakeFuel(); - IndexerTowerStates.slowIndex(); - IndexerBedStates.slowIndex(); - LauncherStates.slowLaunch(); - HoodStates.neutral(); - } - case TEST_IDLE -> { - FuelIntakeStates.stop(); - IndexerTowerStates.neutral(); - IndexerBedStates.neutral(); - LauncherStates.neutral(); - HoodStates.home(); - } - case COAST -> { - IntakeExtensionStates.coastMode(); - HoodStates.coastMode(); - } - case BRAKE -> { - IntakeExtensionStates.brakeMode(); - HoodStates.ensureBrakeMode(); - } - default -> { - // Handle other states or throw an error - } - } - } -} diff --git a/src/main/java/frc/robot/Robot.java b/src/main/java/frc/robot/Robot.java index 98489f1d..16fef6f4 100644 --- a/src/main/java/frc/robot/Robot.java +++ b/src/main/java/frc/robot/Robot.java @@ -1,7 +1,6 @@ package frc.robot; import com.ctre.phoenix6.CANBus; -import com.ctre.phoenix6.Utils; import com.pathplanner.lib.auto.AutoBuilder; import com.pathplanner.lib.commands.FollowPathCommand; import com.pathplanner.lib.commands.PathPlannerAuto; @@ -14,6 +13,7 @@ import edu.wpi.first.wpilibj.DriverStation; import edu.wpi.first.wpilibj.DriverStation.Alliance; import edu.wpi.first.wpilibj.Filesystem; +import edu.wpi.first.wpilibj.RobotBase; import edu.wpi.first.wpilibj.RobotController; import edu.wpi.first.wpilibj.Timer; import edu.wpi.first.wpilibj.smartdashboard.Field2d; @@ -25,35 +25,39 @@ import frc.rebuilt.ShotCalculator; import frc.robot.auton.Auton; import frc.robot.configs.FM2026; +import frc.robot.configs.OM2026; import frc.robot.configs.PHOTON2026; import frc.robot.configs.PM2026; -import frc.robot.fuelIntake.FuelIntake; -import frc.robot.fuelIntake.FuelIntake.FuelIntakeConfig; -import frc.robot.hood.Hood; -import frc.robot.hood.Hood.HoodConfig; -import frc.robot.indexerBed.IndexerBed; -import frc.robot.indexerBed.IndexerBed.IndexerBedConfig; -import frc.robot.indexerTower.IndexerTower; -import frc.robot.indexerTower.IndexerTower.IndexerTowerConfig; -import frc.robot.intakeExtension.IntakeExtension; -import frc.robot.intakeExtension.IntakeExtension.IntakeExtensionConfig; -import frc.robot.launcher.Launcher; -import frc.robot.launcher.Launcher.LauncherConfig; import frc.robot.operator.Operator; import frc.robot.operator.Operator.OperatorConfig; import frc.robot.pilot.Pilot; import frc.robot.pilot.Pilot.PilotConfig; -import frc.robot.swerve.Swerve; -import frc.robot.swerve.SwerveConfig; -import frc.robot.vision.Vision; -import frc.robot.vision.Vision.VisionConfig; -import frc.robot.vision.VisionSystem; -import frc.spectrumLib.BatteryLogger; -import frc.spectrumLib.Rio; -import frc.spectrumLib.SpectrumRobot; -import frc.spectrumLib.Telemetry; -import frc.spectrumLib.Telemetry.PrintPriority; +import frc.robot.subsystems.SuperStructure; +import frc.robot.subsystems.SuperStructure.WantedSuperState; +import frc.robot.subsystems.fuelIntake.FuelIntake; +import frc.robot.subsystems.fuelIntake.FuelIntake.FuelIntakeConfig; +import frc.robot.subsystems.indexerTower.IndexerTower; +import frc.robot.subsystems.indexerTower.IndexerTower.IndexerTowerConfig; +import frc.robot.subsystems.intakeExtension.IntakeExtension; +import frc.robot.subsystems.intakeExtension.IntakeExtension.IntakeExtensionConfig; +import frc.robot.subsystems.launcher.Launcher; +import frc.robot.subsystems.launcher.Launcher.LauncherConfig; +import frc.robot.subsystems.leds.Leds; +import frc.robot.subsystems.spindexer.Spindexer; +import frc.robot.subsystems.spindexer.Spindexer.SpindexerConfig; +import frc.robot.subsystems.swerve.Swerve; +import frc.robot.subsystems.swerve.SwerveConfig; +import frc.robot.subsystems.turret.Turret; +import frc.robot.subsystems.turret.Turret.TurretConfig; +import frc.robot.subsystems.vision.Vision; +import frc.robot.subsystems.vision.Vision.VisionConfig; +import frc.spectrumLib.framework.SpectrumRobot; +import frc.spectrumLib.hardware.Rio; +import frc.spectrumLib.telemetry.BatteryLogger; +import frc.spectrumLib.telemetry.Telemetry; +import frc.spectrumLib.telemetry.Telemetry.PrintPriority; import frc.spectrumLib.util.CrashTracker; +import frc.spectrumLib.util.Util; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -76,31 +80,31 @@ public class Robot extends SpectrumRobot { public static boolean autonWarmedUp = false; public static class Config { - public SwerveConfig swerve = new SwerveConfig(); - public PilotConfig pilot = new PilotConfig(); - public OperatorConfig operator = new OperatorConfig(); - public FuelIntakeConfig fuelIntake = new FuelIntakeConfig(); - public IntakeExtensionConfig intakeExtension = new IntakeExtensionConfig(); - public IndexerTowerConfig indexerTower = new IndexerTowerConfig(); - public IndexerBedConfig indexerBed = new IndexerBedConfig(); - public LauncherConfig launcher = new LauncherConfig(); - public HoodConfig hood = new HoodConfig(); - public VisionConfig vision = new VisionConfig(); + public final SwerveConfig swerve = new SwerveConfig(); + public final PilotConfig pilot = new PilotConfig(); + public final OperatorConfig operator = new OperatorConfig(); + public final FuelIntakeConfig fuelIntake = new FuelIntakeConfig(); + public final IntakeExtensionConfig intakeExtension = new IntakeExtensionConfig(); + public final IndexerTowerConfig indexerTower = new IndexerTowerConfig(); + public final SpindexerConfig spindexer = new SpindexerConfig(); + public final LauncherConfig launcher = new LauncherConfig(); + public final VisionConfig vision = new VisionConfig(); + public final TurretConfig turret = new TurretConfig(); } @Getter private static Swerve swerve; @Getter private static FuelIntake fuelIntake; @Getter private static IntakeExtension intakeExtension; @Getter private static IndexerTower indexerTower; - @Getter private static IndexerBed indexerBed; + @Getter private static Spindexer spindexer; @Getter private static Operator operator; @Getter private static Pilot pilot; - @Getter private static VisionSystem visionSystem; + @Getter private static Turret turret; @Getter private static Launcher launcher; - @Getter private static Hood hood; @Getter private static Vision vision; + @Getter private static Leds leds; @Getter private static Auton auton; - @Getter private static Coordinator coordinator; + @Getter private static SuperStructure superStructure; @Getter private static BatteryLogger batteryLogger; @Getter private static CANBus mainCANBus; @@ -119,48 +123,65 @@ public Robot() { case PM_2026: config = new PM2026(); break; - // case FM_2026: - // config = new FM2026(); - // break; - default: // SIM and UNKNOWN + case FM_2026: config = new FM2026(); break; + case OM_2026: + config = new OM2026(); + break; + default: // SIM and UNKNOWN + config = new OM2026(); + break; } - /* - * Initialize the Subsystems of the robot. Subsystems are how we divide up the robot - * code. Anything with an output that needs to be independently controlled is a - * subsystem Something that don't have an output are also subsystems. - */ double canInitDelay = 0.1; // Delay between any mechanism with motor/can configs mainCANBus = new CANBus(Rio.CANIVORE); // Use the first CANivore bus found - coordinator = new Coordinator(); - operator = new Operator(config.operator); pilot = new Pilot(config.pilot); + operator = new Operator(config.operator); + swerve = new Swerve(config.swerve); Timer.delay(canInitDelay); - vision = new Vision(config.vision); - Timer.delay(canInitDelay); + intakeExtension = new IntakeExtension(config.intakeExtension); Timer.delay(canInitDelay); + fuelIntake = new FuelIntake(config.fuelIntake); Timer.delay(canInitDelay); - hood = new Hood(config.hood); + + turret = new Turret(config.turret); Timer.delay(canInitDelay); + launcher = new Launcher(config.launcher); Timer.delay(canInitDelay); + indexerTower = new IndexerTower(config.indexerTower); Timer.delay(canInitDelay); - indexerBed = new IndexerBed(config.indexerBed); - auton = new Auton(); + + spindexer = new Spindexer(config.spindexer); + Timer.delay(canInitDelay); + + superStructure = + new SuperStructure( + swerve, + fuelIntake, + intakeExtension, + indexerTower, + spindexer, + launcher, + turret); + + auton = new Auton(superStructure); + vision = new Vision(config.vision); batteryLogger = new BatteryLogger(); + // leds = new Leds(); - if (Utils.isSimulation()) { - robotSim = new RobotSim(); + if (RobotBase.isSimulation()) { + robotSim = new RobotSim(superStructure); } - setupDefaultCommands(); + configureBindings(); + batteryLogger.setEnabled(true); Telemetry.print("--- Robot Init Complete ---"); @@ -187,45 +208,87 @@ public Robot() { }); } - /** - * This method cancels all commands and returns subsystems to their default commands and the - * gamepad configs are reset so that new bindings can be assigned based on mode. This method - * should be called when each mode is initialized. - * - *

Warning: This method will cause a very large loop overrun, as it rebinds all states - * to their triggers. Be careful when you call this as it will cause delays in the robot code. - * It is recommended to call this method at the end of disabledInit and teleopInit, as those are - * the most common places to need to reset commands and bindings. - */ - public void resetCommandsAndButtons() { - CommandScheduler.getInstance().cancelAll(); // Disable any currently running commands - CommandScheduler.getInstance().getActiveButtonLoop().clear(); - - // Reset Config for all gamepads and other button bindings - pilot.resetConfig(); - operator.resetConfig(); - - // Bind Triggers for all subsystems - setupStates(); - RobotStates.setupStates(); + public void configureBindings() { + // LT alone → intake fuel; do nothing if RT is already held (RT+LT handled below) + pilot.LT.onTrue( + Commands.either( + superStructure.setStateCommand(WantedSuperState.INTAKE_FUEL), + Commands.none(), + pilot.RT.negate())); + + // RT alone → launch; do nothing if LT is already held (RT+LT handled below) + pilot.RT.onTrue( + Commands.either( + superStructure.setStateCommand(WantedSuperState.LAUNCH_WITH_SQUEEZE), + Commands.none(), + pilot.LT.negate())); + + // RT + LT both held → launch (intake stays extended; resolves to LAUNCH_WITHOUT_SQUEEZE) + pilot.RT + .and(pilot.LT) + .onTrue(superStructure.setStateCommand(WantedSuperState.LAUNCH_WITHOUT_SQUEEZE)); + + // LT released while RT still held → launch (no delay; resolves to + // LAUNCH_WITH_SQUEEZE_WITH_NO_DELAY) + pilot.LT.onFalse( + Commands.either( + superStructure.setStateCommand( + WantedSuperState.LAUNCH_WITH_SQUEEZE_WITH_NO_DELAY), + Commands.none(), + pilot.RT)); + + // RT released while LT still held → resume intaking + pilot.RT.onFalse( + Commands.either( + superStructure.setStateCommand(WantedSuperState.INTAKE_FUEL), + Commands.none(), + pilot.LT)); + + // Both released → idle + pilot.RT.or(pilot.LT).onFalse(superStructure.setStateCommand(WantedSuperState.IDLE)); + + pilot.XButton.whileTrue(superStructure.setStateCommand(WantedSuperState.TRACK_TARGET)); + pilot.XButton.onFalse(superStructure.setStateCommand(WantedSuperState.IDLE)); + + pilot.AButton.whileTrue(superStructure.setStateCommand(WantedSuperState.UNJAM)); + pilot.AButton.onFalse(superStructure.setStateCommand(WantedSuperState.IDLE)); + + pilot.selectButton.onTrue(superStructure.setStateCommand(WantedSuperState.FORCE_HOME)); + pilot.selectButton.onFalse(superStructure.setStateCommand(WantedSuperState.IDLE)); + + // operator.dPadDown.onTrue(ShotCalculator.decreaseFlywheelSpeedOffset()); + // operator.dPadUp.onTrue(ShotCalculator.increaseFlywheelSpeedOffset()); + // operator.dPadRight.onTrue(ShotCalculator.decreaseTurretAngleOffsetDegrees()); + // operator.dPadLeft.onTrue(ShotCalculator.increaseTurretAngleOffsetDegrees()); + + // Reset hub shift timer when enabling + Util.teleop.onTrue(Commands.runOnce(ShiftHelpers::initialize)); + Util.autoMode.onTrue(Commands.runOnce(ShiftHelpers::initialize)); + Util.disabled.onTrue(Commands.runOnce(ShiftHelpers::initialize).ignoringDisable(true)); + + // Auton Triggers + Auton.autonIntake.onTrue( + superStructure.setStateCommand(WantedSuperState.AUTON_INTAKE_FUEL)); + Auton.autonShotPrep.onTrue( + superStructure.setStateCommand(WantedSuperState.AUTON_TRACK_TARGET)); + Auton.autonUnjam.onTrue( + Commands.sequence( + superStructure.setStateCommand(WantedSuperState.UNJAM), + Commands.waitSeconds(1), + superStructure.setStateCommand(WantedSuperState.LAUNCH_WITH_SQUEEZE))); + Auton.autonClearState.onTrue(superStructure.setStateCommand(WantedSuperState.IDLE)); + + if (RobotBase.isSimulation()) { + pilot.YButton.whileTrue( + superStructure.setStateCommand(WantedSuperState.LAUNCH_WITH_SQUEEZE)); + pilot.YButton.onFalse(superStructure.setStateCommand(WantedSuperState.IDLE)); + pilot.BButton.whileTrue(superStructure.setStateCommand(WantedSuperState.INTAKE_FUEL)); + pilot.BButton.onFalse(superStructure.setStateCommand(WantedSuperState.IDLE)); + } } - /** - * This method cancels all commands and returns subsystems to their default commands. This - * method should be called when each mode is initialized. - * - *

Warning: This method will cause a very large loop overrun, as it rebinds all states - * to their triggers. Be careful when you call this as it will cause delays in the robot code. - * It is recommended to call this method at the end of disabledInit and teleopInit, as those are - * the most common places to need to reset commands and bindings. - */ - public void clearCommandsAndButtons() { - CommandScheduler.getInstance().cancelAll(); // Disable any currently running commands - CommandScheduler.getInstance().getActiveButtonLoop().clear(); - - // Bind Triggers for all subsystems - setupStates(); - RobotStates.setupStates(); + public void configureSimBindings() { + RobotSim.simLaunching().whileTrue(robotSim.ballSimLaunchFuel()); } /** Sets up the SmartDashboard data for visualization. */ @@ -264,7 +327,6 @@ public void robotPeriodic() { "Match Data/TimeLeftInShift", ShiftHelpers.getOfficialShiftInfo().remainingTime(), "seconds"); - Telemetry.log("Applied State", RobotStates.getAppliedState().toString()); batteryLogger.setBatteryVoltage(RobotController.getBatteryVoltage()); batteryLogger.setRioCurrent(RobotController.getInputCurrent()); @@ -291,8 +353,6 @@ public void robotPeriodic() { @Override public void disabledInit() { Telemetry.print("### Disabled Init Starting ### "); - clearCommandsAndButtons(); - resetCommandsAndButtons(); if (!autonWarmedUp) { Command autonStartCommand = @@ -315,30 +375,30 @@ public void disabledInit() { @Override public void disabledPeriodic() { - String newAutoName; - boolean leftStart = true; + String fullAutoName = auton.getAutonomousCommand().getName(); + boolean leftStart = !fullAutoName.endsWith(" - Right"); List pathPlannerPaths = new ArrayList<>(); - newAutoName = auton.getAutonomousCommand().getName(); - leftStart = !newAutoName.endsWith(" - Right"); - if (newAutoName.equals("Do Nothing")) { + if (fullAutoName.equals("Do Nothing")) { field2d.getObject("Auto Routine").setPoses(new ArrayList<>()); - autoName = newAutoName; + autoName = fullAutoName; return; } - // Remove " - Left" or " - Right" suffix if present - if (newAutoName.endsWith(" - Left") || newAutoName.endsWith(" - Right")) { - newAutoName = newAutoName.substring(0, newAutoName.lastIndexOf(" - ")); + // Strip " - Left" / " - Right" suffix to get the base path name + String baseAutoName = fullAutoName; + if (baseAutoName.endsWith(" - Left") || baseAutoName.endsWith(" - Right")) { + baseAutoName = baseAutoName.substring(0, baseAutoName.lastIndexOf(" - ")); } - if (!autoName.equals(newAutoName)) { - autoName = newAutoName; + // Reload whenever the full name changes — catches both auto switches and side switches + if (!autoName.equals(fullAutoName)) { + autoName = fullAutoName; Telemetry.log("Auton Warmed Up", false); - if (AutoBuilder.getAllAutoNames().contains(autoName)) { + if (AutoBuilder.getAllAutoNames().contains(baseAutoName)) { try { - pathPlannerPaths = PathPlannerAuto.getPathGroupFromAutoFile(autoName); + pathPlannerPaths = PathPlannerAuto.getPathGroupFromAutoFile(baseAutoName); } catch (IOException | ParseException e) { Telemetry.print("Could not load path planner paths"); } @@ -360,23 +420,31 @@ public void disabledPeriodic() { .collect(Collectors.toList()); } - // Set the robot pose to the starting pose of the first path - swerve.resetPose( - pathPlannerPaths.get(0).getStartingHolonomicPose().orElse(new Pose2d())); - - // Warm up the starting path - Command warmUpPath = - Commands.sequence( - AutoBuilder.followPath(pathPlannerPaths.get(0)) - .withTimeout(0.5), - Commands.runOnce( - () -> { - Telemetry.print( - "Auton Warmed Up", PrintPriority.HIGH); - Telemetry.log("Auton Warmed Up", true); - })) - .ignoringDisable(true); - CommandScheduler.getInstance().schedule(warmUpPath); + if (!pathPlannerPaths.isEmpty()) { + // Set the robot pose to the starting pose of the first path + swerve.resetPose( + pathPlannerPaths + .get(0) + .getStartingHolonomicPose() + .orElse(new Pose2d())); + + // Warm up the starting path + Command warmUpPath = + Commands.sequence( + AutoBuilder.followPath(pathPlannerPaths.get(0)) + .withTimeout(0.5), + Commands.runOnce( + () -> { + Telemetry.print( + "Auton Warmed Up", + PrintPriority.HIGH); + Telemetry.log("Auton Warmed Up", true); + })) + .ignoringDisable(true); + CommandScheduler.getInstance().schedule(warmUpPath); + } else { + Telemetry.print("Warning: No paths loaded for auto: " + baseAutoName); + } // Convert path points to poses List poses = new ArrayList<>(); @@ -413,9 +481,10 @@ public void disabledExit() { @Override public void autonomousInit() { Telemetry.print("@@@ Auton Init @@@ "); - if (Utils.isSimulation()) { - SimulatedArena.getInstance().resetFieldForAuto(); - } + // if (Utils.isSimulation()) { + // robotSim.getBallSim().clearBalls(); + // robotSim.getBallSim().placeFieldBalls(); + // } try { auton.init(); } catch (Throwable t) { @@ -438,7 +507,7 @@ public void autonomousExit() { public void teleopInit() { try { Telemetry.print("!!! Teleop Init Starting !!! "); - RobotStates.clearState(); + field2d.getObject("Auto Routine").setPoses(new ArrayList<>()); // clears auto visualizer Telemetry.print("!!! Teleop Init Complete !!! "); @@ -474,7 +543,6 @@ public void testInit() { try { Telemetry.print("~~~ Test Init Starting ~~~ "); - resetCommandsAndButtons(); Telemetry.print("~~~ Test Init Complete ~~~ "); } catch (Throwable t) { @@ -509,7 +577,8 @@ public void simulationInit() { /** This method is called periodically during simulation. */ @Override public void simulationPeriodic() { - SmartDashboard.putNumber( - "Sim/FuelCount", RobotSim.getIntakeSimulation().getGamePiecesAmount()); + robotSim.getBallSim().tick(); // runs physics, publishes ball positions to NT + robotSim.updateArticulatedMechanisms(); + Telemetry.log("Sim/Fuel", robotSim.getBallSim().getTotalIntaked()); } } diff --git a/src/main/java/frc/robot/RobotSim.java b/src/main/java/frc/robot/RobotSim.java index 45ca239f..ed5ac2c3 100644 --- a/src/main/java/frc/robot/RobotSim.java +++ b/src/main/java/frc/robot/RobotSim.java @@ -1,15 +1,15 @@ package frc.robot; -import static edu.wpi.first.units.Units.Degrees; -import static edu.wpi.first.units.Units.Inches; -import static edu.wpi.first.units.Units.MetersPerSecond; - import com.ctre.phoenix6.Utils; +import edu.wpi.first.math.geometry.Pose2d; import edu.wpi.first.math.geometry.Pose3d; import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Transform2d; +import edu.wpi.first.math.geometry.Transform3d; import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.geometry.Translation3d; import edu.wpi.first.math.util.Units; -import edu.wpi.first.wpilibj.RobotBase; import edu.wpi.first.wpilibj.smartdashboard.Mechanism2d; import edu.wpi.first.wpilibj.smartdashboard.MechanismLigament2d; import edu.wpi.first.wpilibj.smartdashboard.MechanismRoot2d; @@ -19,15 +19,15 @@ import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.Commands; import edu.wpi.first.wpilibj2.command.SequentialCommandGroup; -import edu.wpi.first.wpilibj2.command.WaitCommand; +import edu.wpi.first.wpilibj2.command.button.Trigger; +import frc.rebuilt.FuelPhysicsSim; import frc.rebuilt.ShotCalculator; -import frc.spectrumLib.Telemetry; +import frc.robot.subsystems.SuperStructure; +import frc.robot.subsystems.SuperStructure.CurrentSuperState; +import frc.spectrumLib.sim.Circle; +import frc.spectrumLib.telemetry.Telemetry; import java.util.Set; import lombok.Getter; -import org.ironmaple.simulation.IntakeSimulation; -import org.ironmaple.simulation.SimulatedArena; -import org.ironmaple.simulation.gamepieces.GamePieceProjectile; -import org.ironmaple.simulation.seasonspecific.rebuilt2026.RebuiltFuelOnFly; // General Sim principles // Always move the root/origin to change it's display position @@ -38,33 +38,119 @@ public class RobotSim { @Getter public static final double leftViewHeight = 75; @Getter public static final double leftViewWidth = 75; - @Getter - private static final IntakeSimulation intakeSimulation = - RobotBase.isSimulation() - ? IntakeSimulation.OverTheBumperIntake( - "Fuel", - Robot.getSwerve().getMapleSimSwerveDrivetrain().mapleSimDrive, - Inches.of(29), - Inches.of(12), - IntakeSimulation.IntakeSide.FRONT, - 80) - : null; - public static final Translation2d origin = new Translation2d(0.0, 0.0); public static final Mechanism2d leftView = new Mechanism2d( Units.inchesToMeters(leftViewWidth), Units.inchesToMeters(leftViewHeight)); - public RobotSim() { + public static final Mechanism2d topView = + new Mechanism2d( + Units.inchesToMeters(topViewWidth), Units.inchesToMeters(topViewHeight)); + + public static Trigger simLaunching() { + return new Trigger( + () -> + Utils.isSimulation() + && (Robot.getSuperStructure().getCurrentSuperState() + == CurrentSuperState.LAUNCH_WITH_SQUEEZE + || Robot.getSuperStructure().getCurrentSuperState() + == CurrentSuperState.LAUNCH_WITHOUT_SQUEEZE + || Robot.getSuperStructure().getCurrentSuperState() + == CurrentSuperState + .LAUNCH_WITH_SQUEEZE_WITH_NO_DELAY)); + } + + @Getter private static double simRobotWidth = Units.inchesToMeters(33); + @Getter private static double simRobotLength = Units.inchesToMeters(32.75); + + private static final Translation3d SHOOTER_TURRET_PIVOT_POINT = + new Translation3d(-Units.inchesToMeters(11.00), 0, Units.inchesToMeters(19.237)); + + @Getter private FuelPhysicsSim ballSim; + private int singleLaneBPS = 8; + private double timeBetweenBallLaunches = 1.0 / singleLaneBPS; + private double launcherWidth = 6; + private int numOfLanes = 1; + private double laneWidth = launcherWidth / numOfLanes; + private double lane1 = -2 * laneWidth / 2; + + private SuperStructure robotSuperStructure; + + public RobotSim(SuperStructure superStructure) { + this.robotSuperStructure = superStructure; SmartDashboard.putData("Sim/LeftView", RobotSim.leftView); + SmartDashboard.putData("Sim/TopView", RobotSim.topView); leftView.setBackgroundColor(new Color8Bit(Color.kLightGray)); - + topView.setBackgroundColor(new Color8Bit(Color.kLightGray)); drawRobot(); + + ballSim = new FuelPhysicsSim("Sim/Fuel"); + ballSim.enable(); + ballSim.placeFieldBalls(); // spawns all the game pieces + configBallSimRobot(); + } + + public void updateArticulatedMechanisms() { + double intakeExtensionPose = + Units.inchesToMeters(12) + * robotSuperStructure.getIntakeExtension().getPositionPercentage() + / 100; + var intakePose3d = + Pose3d.kZero.plus( + new Transform3d( + new Translation3d(intakeExtensionPose, 0, 0), Rotation3d.kZero)); + + double turretAngleDegrees = robotSuperStructure.getTurret().getPositionDegrees(); + var turretPose3d = + Pose3d.kZero.rotateAround( + SHOOTER_TURRET_PIVOT_POINT, + new Rotation3d(0, -Math.toRadians(turretAngleDegrees - 9), 0)); + + Pose3d[] mechanismPoses = {intakePose3d, turretPose3d}; + + Telemetry.log("Sim/Components", mechanismPoses); } public void drawRobot() { drawSideRobot(); + drawTopRobot(); + drawTurretCircle(); + } + + @SuppressWarnings("unused") + public void drawTurretCircle() { + MechanismRoot2d circleRoot = + topView.getRoot( + "Turret Circle Root", + Units.inchesToMeters(topViewHeight / 2 + 30), + Units.inchesToMeters(topViewWidth / 2)); + Circle circle = new Circle(50, 30, "Turret Circle", circleRoot, topView); + } + + public void drawTopRobot() { + MechanismRoot2d robotRoot = + topView.getRoot( + "Top Robot Root", + Units.inchesToMeters(topViewWidth / 2.0 + 50), + Units.inchesToMeters(topViewHeight / 2.0 - 25)); + + double rectWidthIn = 50.0; + double rectHeightIn = 80.0; + double rectWidthM = Units.inchesToMeters(rectWidthIn); + double rectHeightM = Units.inchesToMeters(rectHeightIn); + + MechanismLigament2d tr = + robotRoot.append(new MechanismLigament2d("TopEdge", rectHeightM, 180.0)); + MechanismLigament2d br = tr.append(new MechanismLigament2d("RightEdge", rectWidthM, 270.0)); + MechanismLigament2d bl = br.append(new MechanismLigament2d("BottomEdge", rectHeightM, 270)); + MechanismLigament2d ll = bl.append(new MechanismLigament2d("LeftEdge", rectWidthM, 270.0)); + + Color8Bit edgeColor = new Color8Bit(Color.kPurple); + tr.setColor(edgeColor); + br.setColor(edgeColor); + bl.setColor(edgeColor); + ll.setColor(edgeColor); } public void drawSideRobot() { @@ -90,79 +176,79 @@ public void drawSideRobot() { br.setColor(edgeColor); bl.setColor(edgeColor); ll.setColor(edgeColor); + + MechanismLigament2d shooter = bl.append(new MechanismLigament2d("shooter", 0.4, 135)); + shooter.setColor(new Color8Bit(Color.kBlack)); } - // Maple Sim Fuel Intaking - public static Command mapleSimIntakeFuel() { - if (!Utils.isSimulation() || RobotSim.getIntakeSimulation() == null) { - return Commands.none(); - } - return new Command() { - @Override - public void initialize() { - RobotSim.getIntakeSimulation().startIntake(); - } - - @Override - public void end(boolean interrupted) { - RobotSim.getIntakeSimulation().stopIntake(); - } - }.withName("RobotSim.mapleSimIntakeFuel"); + private void configBallSimRobot() { + double bumperHeight = Units.inchesToMeters(4.56); + double intakeWidth = Units.inchesToMeters(10); + double intakeLength = Units.inchesToMeters(28.5); + double intakeXMin = Units.inchesToMeters(0); + double intakeXMax = simRobotWidth / 2 + intakeWidth; + double intakeYMin = -intakeLength / 2; + double intakeYMax = intakeLength / 2; + ballSim.configureRobot( + simRobotWidth, + simRobotLength, + bumperHeight, + 50, + () -> Robot.getSwerve().getRobotPose(), + () -> Robot.getSwerve().getCurrentRobotChassisSpeeds()); + ballSim.addIntakeZone( + intakeXMin, + intakeXMax, + intakeYMin, + intakeYMax, + () -> + Robot.getSuperStructure().getCurrentSuperState() + == CurrentSuperState.INTAKE_FUEL); } - // Maple Sim Fuel Projectile Creator - public static Command mapleSimCreateFuelProjectile() { - if (!Utils.isSimulation() || RobotSim.getIntakeSimulation() == null) { - return Commands.none(); - } + private Command createSimBallLaunch(double laneOffset) { return Commands.runOnce( - () -> { - var parameters = ShotCalculator.getInstance().getParameters(); - GamePieceProjectile fuelProjectile = - new RebuiltFuelOnFly( - Robot.getSwerve() - .getRobotPose() - .getTranslation(), - new Translation2d(), - Robot.getSwerve() - .getCurrentRobotChassisSpeeds(), - Rotation2d.kZero, - Inches.of(29), - MetersPerSecond.of( - parameters.flywheelSpeed() * 0.0025), - Degrees.of(65)) - .withProjectileTrajectoryDisplayCallBack( - (pose3ds) -> - Telemetry.log( - "SimShot/FuelProjectileSuccessfulShot", - pose3ds.toArray(Pose3d[]::new)), - (pose3ds) -> - Telemetry.log( - "SimShot/FuelProjectileUnsuccessfulShot", - pose3ds.toArray( - Pose3d[]::new))); - SimulatedArena.getInstance().addGamePieceProjectile(fuelProjectile); - RobotSim.getIntakeSimulation().obtainGamePieceFromIntake(); - }) - .withName("RobotSim.mapleSimCreateFuelProjectile"); + () -> { + var params = ShotCalculator.getInstance().getParameters(); + double launchSpeed = params.flywheelSpeed(); + double launchAngle = Math.toRadians(90 - params.turretAngle().getDegrees()); + double launchYaw = + Robot.getSwerve().getRobotPose().getRotation().getRadians() + + Math.toRadians(180); + Rotation3d launchRotation = new Rotation3d(0, -launchAngle, launchYaw); + Translation3d launchVelocity = new Translation3d(launchSpeed, launchRotation); + + Pose2d robotPos = Robot.getSwerve().getRobotPose(); + Transform2d robotToLauncher = + new Transform2d( + new Translation2d( + Units.inchesToMeters(-5.5), + Units.inchesToMeters(laneOffset)), + Rotation2d.k180deg); + Translation3d launcherPose = + new Translation3d( + robotPos.transformBy(robotToLauncher).getTranslation()) + .plus(new Translation3d(0, 0, Units.inchesToMeters(17.5))); + ballSim.launchBall(launcherPose, launchVelocity, 500); + }); } - public static Command mapleSimLaunchFuel() { - if (!Utils.isSimulation() || RobotSim.getIntakeSimulation() == null) { + public Command ballSimLaunchFuel() { + if (!Utils.isSimulation()) { return Commands.none(); } return Commands.defer( () -> { - if (!Utils.isSimulation() || RobotSim.getIntakeSimulation() == null) { - return Commands.none(); - } - - int fuelCount = RobotSim.getIntakeSimulation().getGamePiecesAmount(); - SequentialCommandGroup group = new SequentialCommandGroup(); - for (int i = 0; i < fuelCount; i++) { - group.addCommands(mapleSimCreateFuelProjectile(), new WaitCommand(0.066)); + int fuelCount = ballSim.getTotalIntaked(); + int numToLaunchPerLane = fuelCount / numOfLanes; + SequentialCommandGroup group1 = + new SequentialCommandGroup(Commands.waitSeconds(Math.random() * 0.3)); + for (int i = 0; i < numToLaunchPerLane; i++) { + group1.addCommands( + createSimBallLaunch(lane1), + Commands.waitSeconds(timeBetweenBallLaunches)); } - return group.withName("RobotSim.mapleSimLaunchFuel"); + return Commands.parallel(group1).withName("RobotSim.ballSimLaunchFuel"); }, Set.of() // no subsystem requirements ); diff --git a/src/main/java/frc/robot/RobotStates.java b/src/main/java/frc/robot/RobotStates.java deleted file mode 100644 index 9ee1abc4..00000000 --- a/src/main/java/frc/robot/RobotStates.java +++ /dev/null @@ -1,133 +0,0 @@ -package frc.robot; - -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.Commands; -import edu.wpi.first.wpilibj2.command.button.Trigger; -import frc.rebuilt.ShiftHelpers; -import frc.robot.auton.Auton; -import frc.robot.launcher.LauncherStates; -import frc.robot.operator.Operator; -import frc.robot.pilot.Pilot; -import frc.robot.swerve.Swerve; -import frc.spectrumLib.Telemetry; -import frc.spectrumLib.util.Util; -import lombok.Getter; - -/** - * Manages the high-level robot states. This class coordinates multiple subsystems based on the - * current robot state. - */ -public class RobotStates { - private static final Coordinator coordinator = Robot.getCoordinator(); - private static final Pilot pilot = Robot.getPilot(); - private static final Operator operator = Robot.getOperator(); - private static final Swerve swerve = Robot.getSwerve(); - - @Getter private static State appliedState = State.IDLE; - - /** - * Define Robot States here and how they can be triggered States should be triggers that command - * multiple mechanism or can be used in teleop or auton Use onTrue/whileTrue to run a command - * when entering the state Use onFalse/whileFalse to run a command when leaving the state - * RobotType Triggers - */ - - // Define triggers here - public static final Trigger robotInNeutralZone = swerve.inNeutralZone(); - - public static final Trigger robotInEnemyZone = swerve.inEnemyAllianceZone(); - public static final Trigger robotInFeedZone = robotInEnemyZone.or(robotInNeutralZone); - public static final Trigger robotInScoreZone = robotInFeedZone.not(); - public static final Trigger launcherOnTarget = LauncherStates.aimingAtTarget(); - - public static final Trigger autoUpdatePose = Auton.autonPoseUpdate; - - // Setup any binding to set states - public static void setupStates() { - - // Pilot Triggers - - pilot.RT.onTrue( - Commands.either(applyState(State.INTAKE_FUEL), Commands.none(), pilot.LT.negate())); - - pilot.LT.onTrue( - Commands.either( - applyState(State.LAUNCH_WITH_SQUEEZE), Commands.none(), pilot.RT.negate())); - - pilot.LT.and(pilot.RT).onTrue(applyState(State.LAUNCH_WITHOUT_SQUEEZE)); - - pilot.RT.onFalse( - Commands.either( - applyState(State.LAUNCH_WITH_SQUEEZE_WITH_NO_DELAY), - Commands.none(), - pilot.LT)); - - pilot.LT.onFalse(Commands.either(applyState(State.INTAKE_FUEL), Commands.none(), pilot.RT)); - - pilot.LT.or(pilot.RT).onFalse(applyState(State.IDLE)); - - pilot.XButton.whileTrue(applyState(State.TRACK_TARGET)); - pilot.XButton.onFalse(applyState(State.IDLE)); - - pilot.startButton.whileTrue(applyState(State.CUSTOM_SPEED_TURRET_LAUNCH)); - pilot.startButton.onFalse(applyState(State.IDLE)); - - pilot.AButton.whileTrue(applyState(State.UNJAM)); - pilot.AButton.onFalse(applyState(State.IDLE)); - - pilot.home_select.and(pilot.fn).onTrue(applyState(State.FORCE_HOME)); - pilot.home_select.and(pilot.fn).onFalse(applyState(State.IDLE)); - - operator.testX.onTrue(applyState(State.TEST_INFINITE_LAUNCH)); - operator.testX.onFalse(applyState(State.TEST_IDLE)); - - pilot.home_select.onTrue(clearState()); - pilot.home_select.onFalse(clearState()); // forces initial state to be cleared on startup - - // Telemetry bindings (keep logs in sync with trigger state) - bindTriggerTelemetry("LauncherPrep/LauncherOnTarget", launcherOnTarget); - - // Reset hub shift timer when enabling - Util.teleop.onTrue(Commands.runOnce(ShiftHelpers::initialize)); - Util.autoMode.onTrue(Commands.runOnce(ShiftHelpers::initialize)); - Util.disabled.onTrue(Commands.runOnce(ShiftHelpers::initialize).ignoringDisable(true)); - - // Auton Triggers - Auton.autonIntake.onTrue(applyState(State.INTAKE_FUEL)); - Auton.autonShotPrep.onTrue(applyState(State.TRACK_TARGET_WITH_NO_SWERVE)); - Auton.autonShoot.onTrue(applyState(State.LAUNCH_WITH_SQUEEZE)); - Auton.autonUnjam.onTrue( - Commands.sequence( - applyState(State.UNJAM), - Commands.waitSeconds(1), - applyState(State.LAUNCH_WITH_SQUEEZE))); - Auton.autonClearState.onTrue(clearState()); - } - - private RobotStates() { - throw new IllegalStateException("Utility class"); - } - - private static void bindTriggerTelemetry(String name, Trigger trigger) { - trigger.onTrue(Commands.runOnce(() -> Telemetry.log(name, true))); - trigger.onFalse(Commands.runOnce(() -> Telemetry.log(name, false))); - } - - public static Command applyState(State state) { - return Commands.runOnce( - () -> { - appliedState = state; - coordinator.applyRobotState(state); - }) - .withName("APPLYING STATE: " + state); - } - - public static Command clearState() { - return Commands.runOnce( - () -> { - appliedState = State.IDLE; - coordinator.applyRobotState(State.IDLE); - }) - .withName("CLEARING STATE TO IDLE"); - } -} diff --git a/src/main/java/frc/robot/State.java b/src/main/java/frc/robot/State.java deleted file mode 100644 index f8c1a59d..00000000 --- a/src/main/java/frc/robot/State.java +++ /dev/null @@ -1,59 +0,0 @@ -package frc.robot; - -import com.google.common.collect.ImmutableMap; -import java.util.Map; -import java.util.function.BooleanSupplier; - -public enum State { - IDLE, - - INTAKE_FUEL, - SNAKE_INTAKE, - - TRACK_TARGET, - TRACK_TARGET_WITH_NO_SWERVE, - LAUNCH_WITH_SQUEEZE, - LAUNCH_WITH_SQUEEZE_WITH_NO_DELAY, - LAUNCH_WITHOUT_SQUEEZE, - - AUTON_TRACK_TARGET, - AUTON_LAUNCH_WITH_SQUEEZE, - - CUSTOM_SPEED_TURRET_LAUNCH, - UNJAM, - FORCE_HOME, - - COAST, - BRAKE, - TEST_INFINITE_LAUNCH, - TEST_IDLE; - - private State() {} - - // Define the scoring sequence map, the 2nd state is the next state after the - // current one - private static final ImmutableMap scoreSequence = - ImmutableMap.ofEntries(Map.entry(TRACK_TARGET, LAUNCH_WITH_SQUEEZE)); - - // ------ STATE ATTRIBUTES ------// - - public State getNextState(State state) { - return scoreSequence.getOrDefault(state, state); - } - - private static BooleanSupplier isReadyState(State state) { - return () -> - switch (state) { - case TRACK_TARGET -> true; - default -> false; - }; - } - - public BooleanSupplier isReady() { - return isReadyState(this); - } - - public State getNext() { - return getNextState(this); - } -} diff --git a/src/main/java/frc/robot/auton/Auton.java b/src/main/java/frc/robot/auton/Auton.java index ee0e65f8..c093bd0c 100644 --- a/src/main/java/frc/robot/auton/Auton.java +++ b/src/main/java/frc/robot/auton/Auton.java @@ -17,11 +17,10 @@ import edu.wpi.first.wpilibj2.command.CommandScheduler; import edu.wpi.first.wpilibj2.command.Commands; import edu.wpi.first.wpilibj2.command.PrintCommand; -import frc.robot.RobotStates; -import frc.robot.State; -import frc.robot.swerve.SwerveStates; -import frc.spectrumLib.SpectrumState; -import frc.spectrumLib.Telemetry; +import frc.robot.subsystems.SuperStructure; +import frc.robot.subsystems.SuperStructure.WantedSuperState; +import frc.spectrumLib.framework.SpectrumState; +import frc.spectrumLib.telemetry.Telemetry; import java.io.IOException; import org.json.simple.parser.ParseException; @@ -78,7 +77,10 @@ public void setupSelectors() { SmartDashboard.putData("Auto Chooser", pathChooser); } - public Auton() { + private SuperStructure robotSuperStructure; + + public Auton(SuperStructure robotSuperStructure) { + this.robotSuperStructure = robotSuperStructure; setupSelectors(); // runs the command to start the chooser for auto on shuffleboard Telemetry.print("Auton Subsystem Initialized"); } @@ -103,14 +105,13 @@ public Command doNothing() { } public Command launch() { - return Commands.deadline( - Commands.sequence( + return Commands.sequence( autonLaunching.setTrue(), - RobotStates.applyState(State.LAUNCH_WITH_SQUEEZE), + robotSuperStructure.setStateCommand(WantedSuperState.LAUNCH_WITH_SQUEEZE), Commands.waitSeconds(2.5), - RobotStates.applyState(State.IDLE), - autonLaunching.setFalse()), - SwerveStates.autonAimAtTarget()); + robotSuperStructure.setStateCommand(WantedSuperState.IDLE), + autonLaunching.setFalse()) + .withName("Auton.launch"); } public Command secondMan_TBTB(boolean mirrored) { diff --git a/src/main/java/frc/robot/configs/FM2026.java b/src/main/java/frc/robot/configs/FM2026.java index f4d9ce8c..0906a718 100644 --- a/src/main/java/frc/robot/configs/FM2026.java +++ b/src/main/java/frc/robot/configs/FM2026.java @@ -16,7 +16,7 @@ public FM2026() { intakeExtension.setAttached(true); launcher.setAttached(true); indexerTower.setAttached(true); - indexerBed.setAttached(true); - hood.setAttached(true); + spindexer.setAttached(true); + // hood.setAttached(true); } } diff --git a/src/main/java/frc/robot/configs/OM2026.java b/src/main/java/frc/robot/configs/OM2026.java new file mode 100644 index 00000000..e8d3e814 --- /dev/null +++ b/src/main/java/frc/robot/configs/OM2026.java @@ -0,0 +1,21 @@ +package frc.robot.configs; + +import frc.robot.Robot.Config; + +public class OM2026 extends Config { + public OM2026() { + super(); + + // TODO: change when robot is built and can be updated + swerve.configEncoderOffsets(0, 0, 0, 0); + + pilot.setAttached(true); + operator.setAttached(true); + fuelIntake.setAttached(true); + intakeExtension.setAttached(true); + launcher.setAttached(true); + indexerTower.setAttached(true); + spindexer.setAttached(true); + turret.setAttached(true); + } +} diff --git a/src/main/java/frc/robot/configs/PHOTON2026.java b/src/main/java/frc/robot/configs/PHOTON2026.java index 80c360a2..71457dd3 100644 --- a/src/main/java/frc/robot/configs/PHOTON2026.java +++ b/src/main/java/frc/robot/configs/PHOTON2026.java @@ -16,6 +16,6 @@ public PHOTON2026() { intakeExtension.setAttached(true); launcher.setAttached(true); indexerTower.setAttached(true); - indexerBed.setAttached(true); + // indexerBed.setAttached(true); } } diff --git a/src/main/java/frc/robot/configs/PM2026.java b/src/main/java/frc/robot/configs/PM2026.java index 0990aeb1..9c12c55e 100644 --- a/src/main/java/frc/robot/configs/PM2026.java +++ b/src/main/java/frc/robot/configs/PM2026.java @@ -17,6 +17,6 @@ public PM2026() { intakeExtension.setAttached(true); launcher.setAttached(true); indexerTower.setAttached(true); - indexerBed.setAttached(true); + // indexerBed.setAttached(true); } } diff --git a/src/main/java/frc/robot/configs/XM2026.java b/src/main/java/frc/robot/configs/XM2026.java index 3fc5b142..8348606b 100644 --- a/src/main/java/frc/robot/configs/XM2026.java +++ b/src/main/java/frc/robot/configs/XM2026.java @@ -20,6 +20,6 @@ public XM2026() { intakeExtension.setAttached(false); launcher.setAttached(true); indexerTower.setAttached(true); - indexerBed.setAttached(true); + // indexerBed.setAttached(true); } } diff --git a/src/main/java/frc/robot/fuelIntake/FuelIntakeStates.java b/src/main/java/frc/robot/fuelIntake/FuelIntakeStates.java deleted file mode 100644 index 26db8b71..00000000 --- a/src/main/java/frc/robot/fuelIntake/FuelIntakeStates.java +++ /dev/null @@ -1,80 +0,0 @@ -package frc.robot.fuelIntake; - -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.CommandScheduler; -import edu.wpi.first.wpilibj2.command.Commands; -import frc.robot.Robot; -import frc.spectrumLib.Telemetry; - -public class FuelIntakeStates { - private static FuelIntake intake = Robot.getFuelIntake(); - private static FuelIntake.FuelIntakeConfig config = Robot.getConfig().fuelIntake; - - public static void setupDefaultCommand() { - intake.setDefaultCommand( - intake.stopMotor().ignoringDisable(true).withName("Intake.default")); - } - - public static void neutral() { - scheduleIfNotRunning(intake.runVoltage(() -> 0).withName("Intake.neutral")); - } - - public static void intakeFuel() { - scheduleIfNotRunning( - intake.runTorqueFOC(config::getFuelIntakeTorqueCurrent) - .withName("Intake.intakeFuel")); - } - - public static void slowIntakeFuel() { - scheduleIfNotRunning( - intake.runTorqueFOC(config::getFuelSlowIntakeTorqueCurrent) - .withName("Intake.slowIntakeFuel")); - } - - public static void agitateFuel() { - scheduleIfNotRunning( - Commands.repeatingSequence( - intake.runTorqueFOC(config::getFuelAgitationTorqueCurrent), - Commands.waitSeconds(0.5)) - .withName("Intake.agitate")); - } - - public static Command ejectCommand() { - return intake.runTorqueCurrentFoc(config::getEjectTorqueCurrent).withName("Intake.eject"); - } - - public static void stop() { - scheduleIfNotRunning(intake.stopMotor().withName("Intake.stop")); - } - - public static void coastMode() { - scheduleIfNotRunning(intake.coastMode()); - } - - public static void ensureBrakeMode() { - scheduleIfNotRunning(intake.ensureBrakeMode()); - } - - // Log Command - protected static Command log(Command cmd) { - return Telemetry.log(cmd); - } - - /** - * Schedules a command for the fuel intake subsystem only if it's not already the running - * command - * - * @param command the command to schedule - */ - public static void scheduleIfNotRunning(Command command) { - CommandScheduler commandScheduler = CommandScheduler.getInstance(); - - // Check what command is currently requiring this subsystem - Command current = commandScheduler.requiring(intake); - - // Only schedule if it's not already the same same command - if (current != command) { - commandScheduler.schedule(command); - } - } -} diff --git a/src/main/java/frc/robot/hood/Hood.java b/src/main/java/frc/robot/hood/Hood.java deleted file mode 100644 index 0ea25e66..00000000 --- a/src/main/java/frc/robot/hood/Hood.java +++ /dev/null @@ -1,241 +0,0 @@ -package frc.robot.hood; - -import com.ctre.phoenix6.sim.TalonFXSimState; -import edu.wpi.first.math.util.Units; -import edu.wpi.first.networktables.DoubleSubscriber; -import edu.wpi.first.wpilibj.smartdashboard.Mechanism2d; -import edu.wpi.first.wpilibj2.command.Command; -import frc.rebuilt.ShotCalculator; -import frc.robot.RobotSim; -import frc.spectrumLib.Rio; -import frc.spectrumLib.Telemetry; -import frc.spectrumLib.mechanism.Mechanism; -import frc.spectrumLib.sim.ArmConfig; -import frc.spectrumLib.sim.ArmSim; -import java.util.function.DoubleSupplier; -import lombok.Getter; -import lombok.Setter; - -public class Hood extends Mechanism { - - public static class HoodConfig extends Config { - - @Getter private final double initPosition = 9; - - /* Hood Voltages and Current */ - @Getter @Setter private double hoodVoltageOut = 6; - @Getter @Setter private double hoodTorqueCurrent = 30; - - @Getter @Setter private double maxRotations = 0.137; - @Getter @Setter private double minRotations = 0.024; - - @Getter @Setter private double autoTrenchShot = 25.0; - - @Getter - private final DoubleSubscriber onTheFlyAngle = Telemetry.tunable("Hood/OnTheFlyAngle", 9.0); - - /* Hood config values */ - @Getter private final double currentLimit = 40; - @Getter private final double torqueCurrentLimit = 80; - @Getter private final double positionKp = 3000; - @Getter private final double positionKi = 0; - @Getter private final double positionKd = 220; - @Getter private final double positionKv = 0; - @Getter private final double positionKs = 25; - @Getter private final double positionKa = 0; - @Getter private final double positionKg = 0; - - @Getter private final double gearRatio = 51.667; - @Getter private final double mmCruiseVelocity = 50; - @Getter private final double mmAcceleration = 200; - @Getter private final double mmJerk = 1000; - @Getter private final double holdMaxSpeedRPM = 18; - - /* Sim Configs */ - @Getter private double hoodX = Units.inchesToMeters(62.5); - @Getter private double hoodY = Units.inchesToMeters(50); - @Getter private double simRatio = 5; - @Getter private double length = Units.inchesToMeters(10); - - public HoodConfig() { - super("Hood", 15, Rio.CANIVORE); - configMinMaxRotations(minRotations, maxRotations); - configPIDGains(0, positionKp, positionKi, positionKd); - configFeedForwardGains(positionKs, positionKv, positionKa, positionKg); - configMotionMagic(mmCruiseVelocity, mmAcceleration, mmJerk); - configGearRatio(gearRatio); - configSupplyCurrentLimit(currentLimit, true); - configStatorCurrentLimit(torqueCurrentLimit, true); - configForwardTorqueCurrentLimit(torqueCurrentLimit); - configReverseTorqueCurrentLimit(-1 * torqueCurrentLimit); - configForwardSoftLimit(maxRotations, true); - configReverseSoftLimit(minRotations, true); - configNeutralBrakeMode(true); - configClockwise_Positive(); - } - } - - private HoodConfig config; - @Getter private HoodSim sim; - - public Hood(HoodConfig config) { - super(config); - this.config = config; - - setInitialPosition(); - - simulationInit(); - Telemetry.print(getName() + " Subsystem Initialized"); - } - - private void setInitialPosition() { - if (isAttached()) { - motor.setPosition(degreesToRotations(() -> config.getInitPosition())); - } - } - - @Override - public void periodic() { - logBatteryUsage(); - Telemetry.log("Hood/CurrentCommand", getCurrentCommandName()); - Telemetry.log("Hood/Voltage", getVoltage(), "volts"); - Telemetry.log("Hood/StatorCurrent", getStatorCurrent(), "amps"); - Telemetry.log("Hood/SupplyCurrent", getSupplyCurrent(), "amps"); - Telemetry.log("Hood/PositionDegrees", getPositionDegrees(), "degrees"); - Telemetry.log("Hood/RPM", getVelocityRPM(), "RPM"); - Telemetry.log("Hood/Temp", getTemp(), "deg_C"); - } - - @Override - public void setupStates() {} - - @Override - public void setupDefaultCommand() { - HoodStates.setupDefaultCommand(); - } - - // -------------------------------------------------------------------------------- - // Custom Commands - // -------------------------------------------------------------------------------- - - public Command runTorqueFOC(DoubleSupplier torque) { - return run(() -> setTorqueCurrentFoc(torque)); - } - - public void setVoltageAndCurrentLimits( - DoubleSupplier voltage, DoubleSupplier supply, DoubleSupplier torque) { - setVoltageOutput(voltage); - setCurrentLimits(supply, torque); - } - - public Command runVoltageCurrentLimits( - DoubleSupplier voltage, DoubleSupplier supplyCurrent, DoubleSupplier torqueCurrent) { - return runVoltage(voltage).alongWith(runCurrentLimits(supplyCurrent, torqueCurrent)); - } - - public Command runTCcurrentLimits(DoubleSupplier torqueCurrent, DoubleSupplier supplyCurrent) { - return runTorqueCurrentFoc(torqueCurrent) - .alongWith(runCurrentLimits(supplyCurrent, torqueCurrent)); - } - - public Command trackTargetCommand() { - return run(() -> { - var params = ShotCalculator.getInstance().getParameters(); - setMMPositionFoc(() -> degreesToRotations(() -> params.hoodAngle())); - }) - .withName("Hood.trackTargetCommand"); - } - - public Command moveToDegrees(double degrees) { - return run(() -> setMMPositionFoc(() -> degreesToRotations(() -> degrees))); - } - - public Command onTheFlyLaunch() { - return run(() -> { - setMMPositionFoc(() -> config.getOnTheFlyAngle().get()); - }) - .withName("Launcher.onTheFlyLaunch"); - } - - /** Holds the position of the Hood. */ - public Command runHoldHood() { - return new Command() { - double holdPosition = 0; // rotations - - // constructor - { - setName("IntakeExtension.holdPosition"); - addRequirements(Hood.this); - } - - @Override - public boolean runsWhenDisabled() { - return true; - } - - @Override - public void initialize() { - holdPosition = getPositionRotations(); - stop(); - } - - @Override - public void execute() { - if (Math.abs(getVelocityRPM()) > config.holdMaxSpeedRPM) { - stop(); - holdPosition = getPositionRotations(); - } else { - setDynMMPositionFoc( - () -> holdPosition, - () -> config.getMmCruiseVelocity(), - () -> config.getMmAcceleration(), - () -> 20); - } - } - - @Override - public void end(boolean interrupted) { - stop(); - } - }; - } - - public Command stopMotor() { - return run(() -> stop()); - } - - // -------------------------------------------------------------------------------- - // Simulation - // -------------------------------------------------------------------------------- - public void simulationInit() { - if (isAttached()) { - sim = new HoodSim(RobotSim.leftView, motor.getSimState()); - } - } - - // Must be called to enable the simulation - // if roller position changes configure x and y to set position. - @Override - public void simulationPeriodic() { - if (isAttached()) { - sim.simulationPeriodic(); - } - } - - class HoodSim extends ArmSim { - public HoodSim(Mechanism2d mech, TalonFXSimState rollerMotorSim) { - super( - new ArmConfig( - config.hoodX, - config.hoodY, - config.simRatio, - config.length, - 90, - 180, - 90), - mech, - rollerMotorSim, - config.getName()); - } - } -} diff --git a/src/main/java/frc/robot/hood/HoodStates.java b/src/main/java/frc/robot/hood/HoodStates.java deleted file mode 100644 index 295b4204..00000000 --- a/src/main/java/frc/robot/hood/HoodStates.java +++ /dev/null @@ -1,62 +0,0 @@ -package frc.robot.hood; - -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.CommandScheduler; -import frc.robot.Robot; -import frc.spectrumLib.Telemetry; - -public class HoodStates { - private static Hood hood = Robot.getHood(); - private static Hood.HoodConfig config = Robot.getConfig().hood; - - public static void setupDefaultCommand() { - hood.setDefaultCommand(hood.runHoldHood().ignoringDisable(true).withName("Hood.default")); - } - - public static void neutral() { - scheduleIfNotRunning(hood.runVoltage(() -> 0).withName("Hood.neutral")); - } - - public static void home() { - scheduleIfNotRunning(hood.moveToDegrees(config::getInitPosition).withName("Hood.home")); - } - - public static void aimAtTarget() { - scheduleIfNotRunning(hood.trackTargetCommand().withName("Hood.aimAtHub")); - } - - public static void autonAimAtTarget() { - scheduleIfNotRunning( - hood.moveToDegrees(config::getAutoTrenchShot).withName("Hood.autonAimAtTarget")); - } - - public static void coastMode() { - scheduleIfNotRunning(hood.coastMode()); - } - - public static void ensureBrakeMode() { - scheduleIfNotRunning(hood.ensureBrakeMode()); - } - - // Log Command - protected static Command log(Command cmd) { - return Telemetry.log(cmd); - } - - /** - * Schedules a command for the hood subsystem only if it's not already the running command - * - * @param command the command to schedule - */ - public static void scheduleIfNotRunning(Command command) { - CommandScheduler commandScheduler = CommandScheduler.getInstance(); - - // Check what command is currently requiring this subsystem - Command current = commandScheduler.requiring(hood); - - // Only schedule if it's not already the same same command - if (current != command) { - commandScheduler.schedule(command); - } - } -} diff --git a/src/main/java/frc/robot/indexerBed/IndexerBed.java b/src/main/java/frc/robot/indexerBed/IndexerBed.java deleted file mode 100644 index e7b42242..00000000 --- a/src/main/java/frc/robot/indexerBed/IndexerBed.java +++ /dev/null @@ -1,152 +0,0 @@ -package frc.robot.indexerBed; - -import com.ctre.phoenix6.signals.MotorAlignmentValue; -import edu.wpi.first.math.util.Units; -import edu.wpi.first.networktables.DoubleSubscriber; -import edu.wpi.first.wpilibj2.command.Command; -import frc.spectrumLib.Rio; -import frc.spectrumLib.Telemetry; -import frc.spectrumLib.mechanism.Mechanism; -import java.util.function.DoubleSupplier; -import lombok.Getter; -import lombok.Setter; - -public class IndexerBed extends Mechanism { - - public static class IndexerBedConfig extends Config { - - // Intake Voltages and Current - @Getter @Setter private double indexerVoltageOut = 8; - @Getter @Setter private double indexerSlowVoltageOut = 4; - @Getter @Setter private double unjamVoltageOut = -4; - @Getter @Setter private double indexerTorqueCurrent = 120; - @Getter @Setter private double indexerVelocityRPM = 5000; - @Getter @Setter private double indexerSlowVelocityRPM = 1000; - @Getter @Setter private double indexerUnjamRPM = -2000; - - @Getter - private final DoubleSubscriber indexerBedFeedRPM = - Telemetry.tunable("Tunable/IndexerBedFeedRPM", indexerVelocityRPM); - - /* Indexer config values */ - @Getter @Setter private double currentLimit = 60; - @Getter @Setter private double torqueCurrentLimit = 100; - @Getter @Setter private double lowerCurrentLimit = 50; - @Getter @Setter private double timeUntilLowerCurrent = 0; - @Getter @Setter private double velocityKp = 30; - @Getter @Setter private double velocityKv = 0; - @Getter @Setter private double velocityKs = 4; - - /* Sim Configs */ - @Getter @Setter private double intakeX = Units.inchesToMeters(60); - @Getter @Setter private double intakeY = Units.inchesToMeters(75); - @Getter @Setter private double wheelDiameter = 12; - - public IndexerBedConfig() { - super("IndexerBed", 8, Rio.CANIVORE); - configPIDGains(0, velocityKp, 0, 0); - configFeedForwardGains(velocityKs, velocityKv, 0, 0); - configGearRatio(1); - configSupplyCurrentLimit(currentLimit, true); - configStatorCurrentLimit(torqueCurrentLimit, true); - configForwardTorqueCurrentLimit(torqueCurrentLimit); - configReverseTorqueCurrentLimit(torqueCurrentLimit); - configLowerSupplyCurrentLimit(lowerCurrentLimit); - configLowerSupplyCurrentTime(timeUntilLowerCurrent); - configNeutralBrakeMode(false); - configClockwise_Positive(); - setFollowerConfigs( - new FollowerConfig( - "IndexerBed Follower 1", 9, Rio.CANIVORE, MotorAlignmentValue.Opposed)); - } - } - - // private IndexerBedConfig config; - // private IndexerSim sim; - - public IndexerBed(IndexerBedConfig config) { - super(config); - this.config = config; - - // simulationInit(); - Telemetry.print(getName() + " Subsystem Initialized"); - } - - @Override - public void periodic() { - logBatteryUsage(); - Telemetry.log("IndexerBed/CurrentCommand", getCurrentCommandName()); - Telemetry.log("IndexerBed/Voltage", getVoltage(), "volts"); - Telemetry.log("IndexerBed/StatorCurrent", getStatorCurrent(), "amps"); - Telemetry.log("IndexerBed/SupplyCurrent", getSupplyCurrent(), "amps"); - Telemetry.log("IndexerBed/RPM", getVelocityRPM(), "RPM"); - Telemetry.log("IndexerBed/Temp", getTemp(), "deg_C"); - } - - @Override - public void setupStates() {} - - @Override - public void setupDefaultCommand() { - IndexerBedStates.setupDefaultCommand(); - } - - // -------------------------------------------------------------------------------- - // Custom Commands - // -------------------------------------------------------------------------------- - - public Command runTorqueFOC(DoubleSupplier torque) { - return run(() -> setTorqueCurrentFoc(torque)); - } - - public void setVoltageAndCurrentLimits( - DoubleSupplier voltage, DoubleSupplier supply, DoubleSupplier torque) { - setVoltageOutput(voltage); - setCurrentLimits(supply, torque); - } - - public Command runVoltageCurrentLimits( - DoubleSupplier voltage, DoubleSupplier supplyCurrent, DoubleSupplier torqueCurrent) { - return runVoltage(voltage).alongWith(runCurrentLimits(supplyCurrent, torqueCurrent)); - } - - public Command runTCcurrentLimits(DoubleSupplier torqueCurrent, DoubleSupplier supplyCurrent) { - return runTorqueCurrentFoc(torqueCurrent) - .alongWith(runCurrentLimits(supplyCurrent, torqueCurrent)); - } - - public Command stopMotor() { - return run(() -> stop()); - } - - // -------------------------------------------------------------------------------- - // Simulation - // -------------------------------------------------------------------------------- - // public void simulationInit() { - // if (isAttached()) { - // // Create a new RollerSim with the left view, the motor's sim state, and a 6 in - // // diameter - // sim = new IndexerSim(RobotSim.topView, motor.getSimState()); - // } - // } - - // // Must be called to enable the simulation - // // if roller position changes configure x and y to set position. - // @Override - // public void simulationPeriodic() { - // if (isAttached()) { - // sim.simulationPeriodic(); - // } - // } - - // class IndexerSim extends RollerSim { - // public IndexerSim(Mechanism2d mech, TalonFXSimState rollerMotorSim) { - // super( - // new RollerConfig(config.getWheelDiameter()) - // .setPosition(config.getIntakeX(), config.getIntakeY()), - // mech, - // rollerMotorSim, - // config.getName()); - // } - // } -} diff --git a/src/main/java/frc/robot/indexerBed/IndexerBedStates.java b/src/main/java/frc/robot/indexerBed/IndexerBedStates.java deleted file mode 100644 index 5c833fa9..00000000 --- a/src/main/java/frc/robot/indexerBed/IndexerBedStates.java +++ /dev/null @@ -1,72 +0,0 @@ -package frc.robot.indexerBed; - -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.CommandScheduler; -import frc.robot.Robot; -import frc.spectrumLib.Telemetry; - -public class IndexerBedStates { - private static IndexerBed indexerBed = Robot.getIndexerBed(); - private static IndexerBed.IndexerBedConfig config = Robot.getConfig().indexerBed; - - public static void setupDefaultCommand() { - indexerBed.setDefaultCommand( - indexerBed.stopMotor().ignoringDisable(true).withName("IndexerBed.default")); - } - - public static void neutral() { - scheduleIfNotRunning(indexerBed.runVoltage(() -> 0).withName("IndexerBed.neutral")); - } - - public static void indexMax() { - scheduleIfNotRunning( - indexerBed - .runVelocityTcFocRPM(config.getIndexerBedFeedRPM()) - .withName("IndexerBed.feedMax")); - } - - public static void slowIndex() { - scheduleIfNotRunning( - indexerBed - .runVelocityTcFocRPM(config::getIndexerSlowVelocityRPM) - .withName("IndexerBed.slowFeed")); - } - - public static void unjam() { - scheduleIfNotRunning(indexerBed.runVelocityTcFocRPM(config::getIndexerUnjamRPM)); - } - - public static void coastMode() { - scheduleIfNotRunning(indexerBed.coastMode()); - } - - public static void ensureBrakeMode() { - scheduleIfNotRunning(indexerBed.ensureBrakeMode()); - } - - public static Command unjamCommand() { - return indexerBed.runVelocityTcFocRPM(config::getIndexerUnjamRPM); - } - - // Log Command - protected static Command log(Command cmd) { - return Telemetry.log(cmd); - } - - /** - * Schedules a command for the indexer subsystem only if it's not already the running command - * - * @param command the command to schedule - */ - public static void scheduleIfNotRunning(Command command) { - CommandScheduler commandScheduler = CommandScheduler.getInstance(); - - // Check what command is currently requiring this subsystem - Command current = commandScheduler.requiring(indexerBed); - - // Only schedule if it's not already the same same command - if (current != command) { - commandScheduler.schedule(command); - } - } -} diff --git a/src/main/java/frc/robot/indexerTower/IndexerTower.java b/src/main/java/frc/robot/indexerTower/IndexerTower.java deleted file mode 100644 index 6aec98b9..00000000 --- a/src/main/java/frc/robot/indexerTower/IndexerTower.java +++ /dev/null @@ -1,155 +0,0 @@ -package frc.robot.indexerTower; - -import com.ctre.phoenix6.signals.MotorAlignmentValue; -import edu.wpi.first.math.util.Units; -import edu.wpi.first.networktables.DoubleSubscriber; -import edu.wpi.first.wpilibj2.command.Command; -import frc.spectrumLib.Rio; -import frc.spectrumLib.Telemetry; -import frc.spectrumLib.mechanism.Mechanism; -import java.util.function.DoubleSupplier; -import lombok.Getter; -import lombok.Setter; - -public class IndexerTower extends Mechanism { - - public static class IndexerTowerConfig extends Config { - - // Intake Voltages and Current - @Getter @Setter private double indexVoltageOut = 10; - @Getter @Setter private double unjamVoltageOut = -10; - @Getter @Setter private double indexerTorqueCurrent = 80; - - @Getter @Setter private double indexerVelocityRPM = 4000; - @Getter @Setter private double indexerSlowVelocityRPM = 1000; - @Getter @Setter private double indexerUnjamRPM = -1500; - - @Getter - private final DoubleSubscriber indexerTowerFeedRPM = - Telemetry.tunable("Tunable/IndexerTowerFeedRPM", indexerVelocityRPM); - - /* Intake config values */ - @Getter @Setter private double currentLimit = 80; - @Getter @Setter private double torqueCurrentLimit = 140; - @Getter @Setter private double lowerCurrentLimit = 60; - @Getter @Setter private double timeUntilLowerCurrent = 1; - @Getter @Setter private double velocityKp = 50; - @Getter @Setter private double velocityKv = 0; - @Getter @Setter private double velocityKs = 40; - - /* Sim Configs */ - @Getter private double intakeX = Units.inchesToMeters(60); - @Getter private double intakeY = Units.inchesToMeters(75); - @Getter private double wheelDiameter = 12; - - public IndexerTowerConfig() { - super("IndexerTower", 51, Rio.CANIVORE); - configPIDGains(0, velocityKp, 0, 0); - configFeedForwardGains(velocityKs, velocityKv, 0, 0); - configGearRatio(1); - configSupplyCurrentLimit(currentLimit, true); - configStatorCurrentLimit(torqueCurrentLimit, true); - configForwardTorqueCurrentLimit(torqueCurrentLimit); - configReverseTorqueCurrentLimit(torqueCurrentLimit); - configLowerSupplyCurrentLimit(lowerCurrentLimit); - configLowerSupplyCurrentTime(timeUntilLowerCurrent); - configNeutralBrakeMode(true); - configClockwise_Positive(); - setFollowerConfigs( - new FollowerConfig( - "IndexerTower Follower", - 52, - Rio.CANIVORE, - MotorAlignmentValue.Aligned)); - } - } - - // private IndexerTowerConfig config; - // private IndexerSim sim; - - public IndexerTower(IndexerTowerConfig config) { - super(config); - this.config = config; - - // simulationInit(); - Telemetry.print(getName() + " Subsystem Initialized"); - } - - @Override - public void periodic() { - logBatteryUsage(); - Telemetry.log("IndexerTower/CurrentCommand", getCurrentCommandName()); - Telemetry.log("IndexerTower/Voltage", getVoltage(), "volts"); - Telemetry.log("IndexerTower/StatorCurrent", getStatorCurrent(), "amps"); - Telemetry.log("IndexerTower/SupplyCurrent", getSupplyCurrent(), "amps"); - Telemetry.log("IndexerTower/RPM", getVelocityRPM(), "RPM"); - Telemetry.log("IndexerTower/Temp", getTemp(), "deg_C"); - } - - @Override - public void setupStates() {} - - @Override - public void setupDefaultCommand() { - IndexerTowerStates.setupDefaultCommand(); - } - - // -------------------------------------------------------------------------------- - // Custom Commands - // -------------------------------------------------------------------------------- - - public Command runTorqueFOC(DoubleSupplier torque) { - return run(() -> setTorqueCurrentFoc(torque)); - } - - public void setVoltageAndCurrentLimits( - DoubleSupplier voltage, DoubleSupplier supply, DoubleSupplier torque) { - setVoltageOutput(voltage); - setCurrentLimits(supply, torque); - } - - public Command runVoltageCurrentLimits( - DoubleSupplier voltage, DoubleSupplier supplyCurrent, DoubleSupplier torqueCurrent) { - return runVoltage(voltage).alongWith(runCurrentLimits(supplyCurrent, torqueCurrent)); - } - - public Command runTCcurrentLimits(DoubleSupplier torqueCurrent, DoubleSupplier supplyCurrent) { - return runTorqueCurrentFoc(torqueCurrent) - .alongWith(runCurrentLimits(supplyCurrent, torqueCurrent)); - } - - public Command stopMotor() { - return run(() -> stop()); - } - - // -------------------------------------------------------------------------------- - // Simulation - // -------------------------------------------------------------------------------- - // public void simulationInit() { - // if (isAttached()) { - // // Create a new RollerSim with the left view, the motor's sim state, and a 6 in - // diameter - // sim = new IndexerSim(RobotSim.topView, motor.getSimState()); - // } - // } - - // // Must be called to enable the simulation - // // if roller position changes configure x and y to set position. - // @Override - // public void simulationPeriodic() { - // if (isAttached()) { - // sim.simulationPeriodic(); - // } - // } - - // class IndexerSim extends RollerSim { - // public IndexerSim(Mechanism2d mech, TalonFXSimState rollerMotorSim) { - // super( - // new RollerConfig(config.getWheelDiameter()) - // .setPosition(config.getIntakeX(), config.getIntakeY()), - // mech, - // rollerMotorSim, - // config.getName()); - // } - // } -} diff --git a/src/main/java/frc/robot/indexerTower/IndexerTowerStates.java b/src/main/java/frc/robot/indexerTower/IndexerTowerStates.java deleted file mode 100644 index 78df9f67..00000000 --- a/src/main/java/frc/robot/indexerTower/IndexerTowerStates.java +++ /dev/null @@ -1,83 +0,0 @@ -package frc.robot.indexerTower; - -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.CommandScheduler; -import edu.wpi.first.wpilibj2.command.Commands; -import frc.robot.Robot; -import frc.spectrumLib.Telemetry; - -public class IndexerTowerStates { - private static IndexerTower indexerTower = Robot.getIndexerTower(); - private static IndexerTower.IndexerTowerConfig config = Robot.getConfig().indexerTower; - - public static void setupDefaultCommand() { - indexerTower.setDefaultCommand( - indexerTower.stopMotor().ignoringDisable(true).withName("IndexerTower.default")); - } - - public static void neutral() { - scheduleIfNotRunning(indexerTower.runVoltage(() -> 0).withName("IndexerTower.neutral")); - } - - public static void indexMax() { - scheduleIfNotRunning( - indexerTower - .runVelocityTcFocRPM(config.getIndexerTowerFeedRPM()) - .withName("IndexerTower.feedMax")); - } - - public static void slowIndex() { - scheduleIfNotRunning( - indexerTower - .runVelocityTcFocRPM(config::getIndexerSlowVelocityRPM) - .withName("IndexerTower.slowFeed")); - } - - public static void quickReverseThenIndex() { - scheduleIfNotRunning( - Commands.sequence( - indexerTower.runVelocityTcFocRPM(config::getIndexerUnjamRPM).withTimeout(1), - indexerTower.runVelocityTcFocRPM(config.getIndexerTowerFeedRPM()))); - } - - public static void unjam() { - scheduleIfNotRunning( - indexerTower - .runVelocityTcFocRPM(config::getIndexerUnjamRPM) - .withName("IndexerTower.unjam")); - } - - public static void coastMode() { - scheduleIfNotRunning(indexerTower.coastMode()); - } - - public static void ensureBrakeMode() { - scheduleIfNotRunning(indexerTower.ensureBrakeMode()); - } - - public static Command unjamCommand() { - return indexerTower.runVelocityTcFocRPM(config::getIndexerUnjamRPM); - } - - // Log Command - protected static Command log(Command cmd) { - return Telemetry.log(cmd); - } - - /** - * Schedules a command for the indexer subsystem only if it's not already the running command - * - * @param command the command to schedule - */ - public static void scheduleIfNotRunning(Command command) { - CommandScheduler commandScheduler = CommandScheduler.getInstance(); - - // Check what command is currently requiring this subsystem - Command current = commandScheduler.requiring(indexerTower); - - // Only schedule if it's not already the same same command - if (current != command) { - commandScheduler.schedule(command); - } - } -} diff --git a/src/main/java/frc/robot/intakeExtension/IntakeExtensionStates.java b/src/main/java/frc/robot/intakeExtension/IntakeExtensionStates.java deleted file mode 100644 index cc30365f..00000000 --- a/src/main/java/frc/robot/intakeExtension/IntakeExtensionStates.java +++ /dev/null @@ -1,143 +0,0 @@ -package frc.robot.intakeExtension; - -import edu.wpi.first.wpilibj2.command.*; -import edu.wpi.first.wpilibj2.command.button.Trigger; -import frc.robot.Robot; -import frc.spectrumLib.Telemetry; - -public class IntakeExtensionStates { - private static IntakeExtension intakeExtension = Robot.getIntakeExtension(); - private static IntakeExtension.IntakeExtensionConfig config = Robot.getConfig().intakeExtension; - - public static final Trigger fullOut = - intakeExtension.atPercentage(config::getFullOut, config::getAtPoseTolerance); - public static final Trigger home = - intakeExtension.atPercentage(config::getHome, config::getAtPoseTolerance); - - private static boolean sentOutByIntakeState = false; - - public static void setupDefaultCommand() { - intakeExtension.setDefaultCommand( - log(intakeExtension.runHoldIntakeExtension().withName("IntakeExtension.default"))); - } - - public static Command operatorResetIntakeExtension() { - return Commands.runOnce(() -> intakeExtension.resetCurrentPositionToMax()) - .ignoringDisable(true); - } - - // -------------------- State Commands -------------------- - - public static void fullExtend() { - scheduleIfNotRunning( - intakeExtension - .motionMagicPercentMove(config::getFullOut) - .withName("IntakeExtension.fullExtend")); - sentOutByIntakeState = true; - } - - public static void fullRetract() { - scheduleIfNotRunning( - intakeExtension - .motionMagicPercentMove(config::getHome) - .withName("IntakeExtension.fullRetract")); - } - - public static void fullExtendConditional() { - if (sentOutByIntakeState) { - scheduleIfNotRunning( - intakeExtension - .motionMagicPercentMove(config::getFullOut) - .withName("IntakeExtension.fullExtendConditional")); - } else { - neutral(); - } - } - - public static void slowIntakeCloseWithDelay() { - scheduleIfNotRunning( - Commands.sequence( - Commands.waitSeconds( - config.getTimeUntilIntakeSqueeze().getAsDouble()), - intakeExtension.slowMoveToPercent(config::getSqueeze)) - .withName("IntakeExtension.slowIntakeCloseWithDelay")); - } - - public static void slowIntakeCloseWithoutDelay() { - scheduleIfNotRunning( - intakeExtension - .slowMoveToPercent(config::getSqueeze) - .withName("IntakeExtension.slowIntakeCloseWithoutDelay")); - } - - public static Command fullExtendCommand() { - return log( - intakeExtension - .motionMagicPercentMove(config::getFullOut) - .withName("IntakeExtension.fullExtendCommand")); - } - - public static Command fullRetractCommand() { - return log( - intakeExtension - .motionMagicPercentMove(config::getHome) - .withName("IntakeExtension.fullRetractCommand")); - } - - public static Command slowIntakeCloseCommand() { - return log(intakeExtension.slowMoveToPercent(config::getSqueeze)) - .withName("IntakeExtension.slowIntakeClose"); - } - - public static Command positiveVoltageOut() { - return log( - intakeExtension - .runVoltageNoSoftLimit(config::getPositiveVoltageOut) - .withName("IntakeExtension.positiveVoltageOut")); - } - - public static Command negativeVoltageOut() { - return log( - intakeExtension - .runVoltageNoSoftLimit(config::getNegativeVoltageOut) - .withName("IntakeExtension.negativeVoltageOut")); - } - - public static Command coastMode() { - return log(intakeExtension.coastMode().withName("IntakeExtension.coastMode")); - } - - public static Command brakeMode() { - return log(intakeExtension.ensureBrakeMode().withName("IntakeExtension.brakeMode")); - } - - public static void neutral() { - scheduleIfNotRunning( - intakeExtension.runVoltage(() -> 0).withName("IntakeExtension.neutral")); - } - - // -------------------------------------------------------- - - // Log Command - protected static Command log(Command cmd) { - return Telemetry.log(cmd); - } - - /** - * Schedules a command for the intake extension subsystem only if it's not already the running - * command - * - * @param command the command to schedule - */ - public static void scheduleIfNotRunning(Command command) { - CommandScheduler commandScheduler = CommandScheduler.getInstance(); - - // Check what command is currently requiring this subsystem - Command current = commandScheduler.requiring(intakeExtension); - - // Only schedule if it's not already the same same command - if (current != command) { - commandScheduler.schedule(command); - } - } -} diff --git a/src/main/java/frc/robot/launcher/LauncherStates.java b/src/main/java/frc/robot/launcher/LauncherStates.java deleted file mode 100644 index eb1c8846..00000000 --- a/src/main/java/frc/robot/launcher/LauncherStates.java +++ /dev/null @@ -1,93 +0,0 @@ -package frc.robot.launcher; - -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.CommandScheduler; -import edu.wpi.first.wpilibj2.command.button.Trigger; -import frc.robot.Robot; -import frc.spectrumLib.Telemetry; - -public class LauncherStates { - private static Launcher launcher = Robot.getLauncher(); - private static Launcher.LauncherConfig config = Robot.getConfig().launcher; - - public static void setupDefaultCommand() { - launcher.setDefaultCommand( - launcher.stopMotor().ignoringDisable(true).withName("Launcher.default")); - } - - public static Trigger aimingAtTarget() { - return launcher.aimingAtTarget(); - } - - // -------------------- State Commands -------------------- - - public static void neutral() { - scheduleIfNotRunning(launcher.runVoltage(() -> 0).withName("Launcher.neutral")); - } - - public static void coastMode() { - scheduleIfNotRunning(launcher.coastMode()); - } - - public static void ensureBrakeMode() { - scheduleIfNotRunning(launcher.ensureBrakeMode()); - } - - public static void slowLaunch() { - scheduleIfNotRunning( - launcher.runVelocityTcFocRPM(config::getSlowLaunchSpeed) - .withName("Launcher.slowLaunch")); - } - - public static void aimAtTarget() { - scheduleIfNotRunning(launcher.trackTargetCommand().withName("Launcher.aimAtHub")); - } - - public static void autonAimAtTarget() { - scheduleIfNotRunning( - launcher.runVelocityTcFocRPM(config::getAutoTrenchLaunch) - .withName("Launcher.autonAimAtTarget")); - } - - public static void customLaunchSpeed() { - scheduleIfNotRunning(launcher.onTheFlyLaunch().withName("Launcher.onTheFlyLaunch")); - } - - public static void idlePrep() { - scheduleIfNotRunning( - launcher.runVelocityTcFocRPM(config::getIdlingRPM).withName("Launcher.idlePrep")); - } - - // -------------------------------------------------------- - - public static Command launchFuel() { - return launcher.runTorqueFOC(config::getLauncherTorqueCurrent) - .withName("Launcher.launchFuelCommand"); - } - - public static Command aimAtTargetCommand() { - return log(launcher.trackTargetCommand().withName("Launcher.aimAtHubCommand")); - } - - // Log Command - protected static Command log(Command cmd) { - return Telemetry.log(cmd); - } - - /** - * Schedules a command for the launcher subsystem only if it's not already the running command - * - * @param command the command to schedule - */ - public static void scheduleIfNotRunning(Command command) { - CommandScheduler commandScheduler = CommandScheduler.getInstance(); - - // Check what command is currently requiring this subsystem - Command current = commandScheduler.requiring(launcher); - - // Only schedule if it's not already the same same command - if (current != command) { - commandScheduler.schedule(command); - } - } -} diff --git a/src/main/java/frc/robot/leds/CANdleLeds.java b/src/main/java/frc/robot/leds/CANdleLeds.java deleted file mode 100644 index 725234d0..00000000 --- a/src/main/java/frc/robot/leds/CANdleLeds.java +++ /dev/null @@ -1,56 +0,0 @@ -// /* Generated by Phoenix Tuner X */ -// package frc.robot.leds; - -// import static edu.wpi.first.units.Units.*; - -// import com.ctre.phoenix6.CANBus; -// import com.ctre.phoenix6.configs.CANdleConfiguration; -// import com.ctre.phoenix6.configs.LEDConfigs; -// import com.ctre.phoenix6.controls.SingleFadeAnimation; -// import com.ctre.phoenix6.hardware.CANdle; -// import com.ctre.phoenix6.signals.LossOfSignalBehaviorValue; -// import com.ctre.phoenix6.signals.RGBWColor; -// import com.ctre.phoenix6.signals.StripTypeValue; -// import edu.wpi.first.wpilibj2.command.SubsystemBase; -// import frc.robot.Robot; -// import frc.spectrumLib.Rio; -// import frc.spectrumLib.SpectrumSubsystem; -// import frc.spectrumLib.Telemetry; -// import lombok.Getter; - -// /** Subsystem that controls an addressable LED strip using a CANdle. */ -// public class CANdleLeds extends SubsystemBase implements SpectrumSubsystem { -// @Getter private final CANdle CANdle; -// @Getter private final CANdleConfiguration CANdleConfig; - -// @Getter private final int numLeds = 20; - -// public CANdleLeds() { -// CANdle = new CANdle(1, new CANBus(Rio.CANIVORE)); -// CANdleConfig = -// new CANdleConfiguration() -// .withLED( -// new LEDConfigs() -// .withStripType(StripTypeValue.RGB) -// .withBrightnessScalar(0.5) -// .withLossOfSignalBehavior( -// LossOfSignalBehaviorValue.DisableLEDs)); -// CANdle.getConfigurator().apply(CANdleConfig); -// CANdle.setControl( -// new SingleFadeAnimation(0, 20) -// .withColor(new RGBWColor(156, 11, 255, 0)) -// .withFrameRate(Hertz.of(100))); -// Robot.add(this); -// Telemetry.print("LED Subsystem Initialized: "); -// } - -// @Override -// public void setupDefaultCommand() { -// LedStates.setDefaultCommand(); -// } - -// @Override -// public void setupStates() { -// LedStates.bindTriggers(); -// } -// } diff --git a/src/main/java/frc/robot/leds/LedStates.java b/src/main/java/frc/robot/leds/LedStates.java deleted file mode 100644 index 14add09a..00000000 --- a/src/main/java/frc/robot/leds/LedStates.java +++ /dev/null @@ -1,142 +0,0 @@ -// package frc.robot.leds; - -// import com.ctre.phoenix6.controls.SingleFadeAnimation; -// import com.ctre.phoenix6.controls.StrobeAnimation; -// import com.ctre.phoenix6.hardware.CANdle; -// import com.ctre.phoenix6.signals.RGBWColor; -// import edu.wpi.first.wpilibj.DriverStation; -// import edu.wpi.first.wpilibj2.command.InstantCommand; -// import edu.wpi.first.wpilibj2.command.button.Trigger; -// import frc.rebuilt.Field; -// import frc.rebuilt.ShiftHelpers; -// import frc.robot.Robot; -// import frc.spectrumLib.util.Util; -// import edu.wpi.first.wpilibj2.command.Commands; - -// public class LedStates { -// private static CANdle leds = Robot.getLeds(); -// private static CANdle candle = leds.getCANdle(); - -// public static final Trigger auto = Util.autoMode; - -// // period between end of auto and first alliance shift -// public static final Trigger transitionShift = -// new Trigger( -// () -> { -// double t = DriverStation.getMatchTime(); -// return (t <= 140 && t > 133); -// }) -// .and(Util.teleop); - -// public static final Trigger endgame = -// new Trigger(() -> DriverStation.getMatchTime() <= 30).and(Util.teleop); - -// // 3 seconds before the end of each shift -// public static final Trigger aboutToChangeShift = -// new Trigger( -// () -> { -// double t = DriverStation.getMatchTime(); -// return (t <= 108 && t >= 105) -// || (t <= 83 && t >= 80) -// || (t <= 58 && t >= 55) -// || (t <= 33 && t >= 30); -// }) -// .and(Util.teleop); - -// public static final Trigger transitionAboutToEnd = -// new Trigger( -// () -> { -// double t = DriverStation.getMatchTime(); -// return (t <= 133 && t > 130); -// }) -// .and(Util.teleop); - -// public static final Trigger blueShift = -// new Trigger(() -> ShiftHelpers.isCurrentShiftBlue(DriverStation.getMatchTime())) -// .and(Util.teleop); -// public static final Trigger redShift = -// new Trigger(() -> ShiftHelpers.isCurrentShiftRed(DriverStation.getMatchTime())) -// .and(Util.teleop); - -// public static Trigger bothInShift = auto.or(transitionShift, endgame); - -// public static void setDefaultCommand() {} - -// static void bindTriggers() { -// // Match time related patterns -// autoShift(auto, 15); -// afterAutoTransition(transitionShift, 15); -// transitionAboutToEnd(transitionAboutToEnd, 25); -// redAlliance(redShift.and(bothInShift.not()), 10); -// shiftAboutToEnd(aboutToChangeShift, 25); -// blueAlliance(blueShift.and(bothInShift.not()), 10); -// endgame(endgame, 20); -// } - -// /** -// * -// * @param trigger -// * @param priority -// */ -// static void autoShift(Trigger trigger, int priority) { -// SingleFadeAnimation shiftAnimation = -// new SingleFadeAnimation(0, 20) -// .withSlot(0) -// .withColor( -// Field.isBlue() -// ? new RGBWColor(0, 0, 255) -// : new RGBWColor(255, 0, 0)); -// trigger.onTrue(Commands.runOnce(() -> candle.setControl(shiftAnimation))); -// } - -// static void afterAutoTransition(Trigger trigger, int priority) { -// SingleFadeAnimation shiftAnimation = -// new SingleFadeAnimation(0, 20) -// .withSlot(0) -// .withColor( -// Field.isBlue() -// ? new RGBWColor(0, 0, 255) -// : new RGBWColor(255, 0, 0)); -// trigger.onTrue(Commands.runOnce(() -> candle.setControl(shiftAnimation))); -// } - -// static void redAlliance(Trigger trigger, int priority) { -// SingleFadeAnimation redAllianceShift = -// new SingleFadeAnimation(0, 20).withSlot(0).withColor(new RGBWColor(255, 0, 0)); -// trigger.onTrue(Commands.runOnce(() -> candle.setControl(redAllianceShift))); -// } - -// static void blueAlliance(Trigger trigger, int priority) { -// SingleFadeAnimation blueAllianceShift = -// new SingleFadeAnimation(0, 20).withSlot(0).withColor(new RGBWColor(0, 0, 255)); -// trigger.onTrue(Commands.runOnce(() -> candle.setControl(blueAllianceShift))); -// } - -// static void shiftAboutToEnd(Trigger trigger, int priority) { -// StrobeAnimation aboutToShift = -// new StrobeAnimation(0, 20) -// .withSlot(0) -// .withColor( -// ShiftHelpers.isCurrentShiftBlue(DriverStation.getMatchTime()) -// ? new RGBWColor(0, 0, 255) -// : new RGBWColor(255, 0, 0)); -// trigger.onTrue(Commands.runOnce(() -> candle.setControl(aboutToShift))); -// } - -// static void transitionAboutToEnd(Trigger trigger, int priority) { -// StrobeAnimation aboutToShift = -// new StrobeAnimation(0, 20) -// .withSlot(0) -// .withColor( -// Field.isBlue() -// ? new RGBWColor(0, 0, 255) -// : new RGBWColor(255, 0, 0)); -// trigger.onTrue(Commands.runOnce(() -> candle.setControl(aboutToShift))); -// } - -// static void endgame(Trigger trigger, int priority) { -// SingleFadeAnimation endgameAnimation = -// new SingleFadeAnimation(0, 20).withSlot(0).withColor(new RGBWColor(207, 255, 4)); -// trigger.onTrue(Commands.runOnce(() -> candle.setControl(endgameAnimation))); -// } -// } diff --git a/src/main/java/frc/robot/operator/Operator.java b/src/main/java/frc/robot/operator/Operator.java index 1974101a..0817e600 100644 --- a/src/main/java/frc/robot/operator/Operator.java +++ b/src/main/java/frc/robot/operator/Operator.java @@ -1,62 +1,31 @@ package frc.robot.operator; import edu.wpi.first.wpilibj2.command.button.Trigger; -import frc.robot.Robot; -import frc.spectrumLib.Telemetry; import frc.spectrumLib.gamepads.Gamepad; +import frc.spectrumLib.telemetry.Telemetry; +/* A, B, X, Y, Left Bumper, Right Bumper = Buttons 1 to 6 in simulation */ public class Operator extends Gamepad { - - private static double climberScalerDown = 0.5; - private static double climberScalerUp = 0.5; - - // Triggers, these would be robot states such as intake, visionAim, etc. - // If triggers need any of the config values set them in the constructor - /* A, B, X, Y, Left Bumper, Right Bumper = Buttons 1 to 6 in simulation */ - - public final Trigger enabled = teleop.or(testMode); // works for both teleop and testMode - public final Trigger fn = leftBumper; - public final Trigger noFn = fn.not(); - public final Trigger home_select = select.or(leftStickClick); - public final Trigger startButton = start; - - public final Trigger RT = rightTrigger; + public final Trigger LB = leftBumper; + public final Trigger RB = rightBumper; public final Trigger LT = leftTrigger; + public final Trigger RT = rightTrigger; - public final Trigger manualOverride = enabled.and(rightStickX.or(rightStickY)); - - public final Trigger AButton = A.and(teleop); - public final Trigger BButton = B.and(teleop); - public final Trigger XButton = X.and(teleop); - public final Trigger YButton = Y.and(teleop); - - public final Trigger testA = A.and(testMode); - public final Trigger testB = B.and(testMode); - public final Trigger testX = X.and(testMode); - public final Trigger testY = Y.and(testMode); - - public final Trigger driving = testMode.and(leftStickX.or(leftStickY)); - public final Trigger steer = testMode.and(rightStickX.or(rightStickY)); - - public final Trigger coastA = A.and(disabled); - public final Trigger brakeB = B.and(disabled); - - public final Trigger resetIntakeExtensionPos = Y.and(fn); - public final Trigger resetTurretPos = start.and(fn); - - public final Trigger moveTurretLeft = LT.and(fn); - public final Trigger moveTurretRight = RT.and(fn); + public final Trigger AButton = A; + public final Trigger BButton = B; + public final Trigger XButton = X; + public final Trigger YButton = Y; - public final Trigger dpadUp = upDpad.and(teleop); - public final Trigger dpadDown = downDpad.and(teleop); - public final Trigger dpadLeft = leftDpad.and(teleop); - public final Trigger dpadRight = rightDpad.and(teleop); + public final Trigger startButton = start; + public final Trigger selectButton = select; - public final Trigger rightStickTrigger = rightStickX.or(rightStickY); + public final Trigger leftStickPress = leftStickClick; + public final Trigger rightStickPress = rightStickClick; - // DISABLED TRIGGERS - public final Trigger coastOn_dB = disabled.and(B); - public final Trigger coastOff_dA = disabled.and(A); + public final Trigger dPadUp = upDpad; + public final Trigger dPadDown = downDpad; + public final Trigger dPadLeft = leftDpad; + public final Trigger dPadRight = rightDpad; public static class OperatorConfig extends Config { @@ -72,23 +41,7 @@ public OperatorConfig() { public Operator(OperatorConfig config) { super(config); this.config = config; - Robot.add(this); - Telemetry.print("Operator Subsystem Initialized"); - } - - @Override - public void setupStates() { - // Left Blank so we can bind when the controller is connected - OperatorStates.setStates(); - } - - @Override - public void setupDefaultCommand() { - OperatorStates.setupDefaultCommand(); - } - public double getTriggerAxis() { - return ((getRightTriggerAxis() * climberScalerUp) - - (getLeftTriggerAxis() * climberScalerDown)); + Telemetry.print("Operator Subsystem Initialized: "); } } diff --git a/src/main/java/frc/robot/operator/OperatorStates.java b/src/main/java/frc/robot/operator/OperatorStates.java deleted file mode 100644 index eea4b114..00000000 --- a/src/main/java/frc/robot/operator/OperatorStates.java +++ /dev/null @@ -1,60 +0,0 @@ -package frc.robot.operator; - -import edu.wpi.first.wpilibj2.command.Command; -import frc.rebuilt.ShotCalculator; -import frc.robot.Robot; -import frc.robot.indexerBed.IndexerBedStates; -import frc.robot.indexerTower.IndexerTowerStates; -import frc.robot.intakeExtension.IntakeExtensionStates; -import frc.robot.launcher.LauncherStates; -import frc.spectrumLib.Telemetry; - -/** This class should have any command calls that directly call the Operator */ -public class OperatorStates { - private static Operator operator = Robot.getOperator(); - - /** Set default command to turn off the rumble */ - public static void setupDefaultCommand() { - operator.setDefaultCommand( - log( - rumble(0, 1) - .withName( - "Operator.noRumble"))); // .repeatedly().withName("Operator.default")); - } - - /** Set the states for the operator controller */ - public static void setStates() { - operator.resetIntakeExtensionPos.onTrue( - IntakeExtensionStates.operatorResetIntakeExtension(), rumble(1, 0.5)); - - operator.BButton.whileTrue(IndexerTowerStates.unjamCommand()); - operator.XButton.whileTrue(IndexerBedStates.unjamCommand()); - operator.AButton.onTrue(LauncherStates.aimAtTargetCommand()); - - operator.rightBumperOnly.whileTrue(IntakeExtensionStates.fullRetractCommand()); - - operator.RT.whileTrue(IntakeExtensionStates.positiveVoltageOut()); - operator.LT.whileTrue(IntakeExtensionStates.negativeVoltageOut()); - - operator.testA.whileTrue(IntakeExtensionStates.fullExtendCommand()); - operator.testB.whileTrue(IntakeExtensionStates.fullRetractCommand()); - - operator.coastA.onTrue(IntakeExtensionStates.coastMode()); - operator.brakeB.onTrue(IntakeExtensionStates.brakeMode()); - - operator.dpadDown.onTrue(log(ShotCalculator.decreaseHoodAngleOffset())); - operator.dpadUp.onTrue(log(ShotCalculator.increaseHoodAngleOffset())); - operator.dpadRight.onTrue(log(ShotCalculator.decreaseDriveAngleOffset())); - operator.dpadLeft.onTrue(log(ShotCalculator.increaseDriveAngleOffset())); - } - - /** Command that can be used to rumble the operator controller */ - public static Command rumble(double intensity, double durationSeconds) { - return operator.rumbleCommand(intensity, durationSeconds).withName("Operator.rumble"); - } - - // Log Command - protected static Command log(Command cmd) { - return Telemetry.log(cmd); - } -} diff --git a/src/main/java/frc/robot/pilot/Pilot.java b/src/main/java/frc/robot/pilot/Pilot.java index 05eec704..daa05408 100644 --- a/src/main/java/frc/robot/pilot/Pilot.java +++ b/src/main/java/frc/robot/pilot/Pilot.java @@ -1,94 +1,47 @@ package frc.robot.pilot; import static edu.wpi.first.units.Units.MetersPerSecond; +import static edu.wpi.first.units.Units.RadiansPerSecond; import edu.wpi.first.wpilibj2.command.button.Trigger; import frc.robot.Robot; -import frc.spectrumLib.SpectrumState; -import frc.spectrumLib.Telemetry; import frc.spectrumLib.gamepads.Gamepad; -import lombok.Getter; -import lombok.Setter; +import frc.spectrumLib.telemetry.Telemetry; +/* A, B, X, Y, Left Bumper, Right Bumper, Left Trigger, Right Trigger = Buttons 1 to 8 in simulation */ public class Pilot extends Gamepad { + public final Trigger LB = leftBumper; + public final Trigger RB = rightBumper; + public final Trigger LT = leftTrigger; + public final Trigger RT = rightTrigger; - // Triggers, these would be robot states such as ampReady, intake, visionAim, etc. - // If triggers need any of the config values set them in the constructor - /* A, B, X, Y, Left Bumper, Right Bumper = Buttons 1 to 6 in simulation */ - public final Trigger enabled = teleop.or(testMode); // works for both teleop and testMode - public final Trigger fn = leftBumper; - public final Trigger noFn = fn.not(); - public final Trigger home_select = select; - - public final Trigger LT = leftTrigger.and(noFn, teleop); - public final Trigger RT = rightTrigger.and(noFn, teleop); - public final Trigger LB_LT = leftTrigger.and(fn, teleop); - - public final Trigger AButton = A.and(teleop); - public final Trigger BButton = B.and(teleop); - public final Trigger XButton = X.and(teleop); - public final Trigger YButton = Y.and(teleop); - - public final Trigger coastA = A.and(disabled); - public final Trigger brakeB = B.and(disabled); - - public final Trigger startButton = start.and(noFn, teleop); - - public final Trigger RB = rightBumper.and(teleop); - - public final Trigger dpadUp = upDpad.and(teleop); - public final Trigger dpadDown = downDpad.and(teleop); - public final Trigger dpadLeft = leftDpad.and(teleop); - public final Trigger dpadRight = rightDpad.and(teleop); - - // Drive Triggers - public final Trigger upReorient = upDpad.and(fn, teleop); - public final Trigger leftReorient = leftDpad.and(fn, teleop); - public final Trigger downReorient = downDpad.and(fn, teleop); - public final Trigger rightReorient = rightDpad.and(fn, teleop); - - /* Use the right stick to set a cardinal direction to aim at */ - public final Trigger driving = enabled.and(leftStickX.or(leftStickY)); - public final Trigger steer = enabled.and(rightStickX.or(rightStickY)); - - public final Trigger fpv_LS = leftStickClick.and(enabled); // Remapped to Left back button - public final Trigger toggleReverse = - rightStickClick.and(enabled); // Remapped to Right back button - - // DISABLED TRIGGERS - public final Trigger coastOn_dB = disabled.and(B); - public final Trigger coastOff_dA = disabled.and(A); - public final Trigger reZero_start = disabled.and(leftBumper, rightBumper, start); - public final Trigger visionPoseReset_LB_Select = disabled.and(leftBumper, select); - - // TEST TRIGGERS - public final Trigger testTune_tB = testMode.and(B); - public final Trigger testTune_tA = testMode.and(A); - public final Trigger testTune_tX = testMode.and(X); - public final Trigger testTune_tY = testMode.and(Y); - public final Trigger testTune_RB = testMode.and(rightBumper); - public final Trigger testTune_LB = testMode.and(leftBumper); - public final Trigger testTriggersTrigger = testMode.and(leftTrigger.or(rightTrigger)); - - public final Trigger testActionReady = rightBumper.and(testMode); + public final Trigger AButton = A; + public final Trigger BButton = B; + public final Trigger XButton = X; + public final Trigger YButton = Y; - public static class PilotConfig extends Config { + public final Trigger startButton = start; + public final Trigger selectButton = select; + + public final Trigger leftStickPress = leftStickClick; + public final Trigger rightStickPress = rightStickClick; - @Getter @Setter private double slowModeScalor = 0.45; - @Getter @Setter private double defaultTurnScalor = 0.6; - @Getter @Setter private double turboModeScalor = 1; - private double deadzone = 0.05; + public final Trigger dPadUp = upDpad; + public final Trigger dPadDown = downDpad; + public final Trigger dPadLeft = leftDpad; + public final Trigger dPadRight = rightDpad; + + public static class PilotConfig extends Config { + private double deadzone = 0.10; public PilotConfig() { super("Pilot", 0); setLeftStickDeadzone(deadzone); - setLeftStickExp(3); - // Set Scalar in Constructor from Swerve Config + setLeftStickExp(3.0); setRightStickDeadzone(deadzone); - setRightStickExp(3); - // Set Scalar in Constructor from Swerve Config + setRightStickExp(3.0); setTriggersDeadzone(deadzone); setTriggersExp(1); @@ -96,38 +49,24 @@ public PilotConfig() { } } + @SuppressWarnings("unused") private PilotConfig config; - private @Getter @Setter SpectrumState slowMode = new SpectrumState("SlowMode"); - private @Getter @Setter SpectrumState turboMode = new SpectrumState("TurboMode"); - /** Create a new Pilot with the default name and port. */ public Pilot(PilotConfig config) { super(config); this.config = config; - config.setLeftStickScalar(Robot.getConfig().swerve.getSpeedAt12Volts().in(MetersPerSecond)); + config.setLeftStickScalar( + Robot.getConfig().swerve.getLinearSpeedAt12Volts().in(MetersPerSecond)); + config.setRightStickScalar( + Robot.getConfig().swerve.getAngularSpeedAt12Volts().in(RadiansPerSecond)); leftStickCurve.setScalar(config.getLeftStickScalar()); - - config.setRightStickScalar(Robot.getConfig().swerve.getMaxAngularVelocity()); rightStickCurve.setScalar(config.getRightStickScalar()); - Robot.add(this); - Telemetry.print("Pilot Subsystem Initialized"); + Telemetry.print("Pilot Subsystem Initialized: "); } - @Override - public void setupStates() { - // Used for setting rumble and control mode states only - PilotStates.setStates(); - } - - @Override - public void setupDefaultCommand() { - PilotStates.setupDefaultCommand(); - } - - // DRIVE METHODS public void setMaxVelocity(double maxVelocity) { leftStickCurve.setScalar(maxVelocity); } @@ -137,36 +76,20 @@ public void setMaxRotationalVelocity(double maxRotationalVelocity) { } // Positive is forward, up on the left stick is positive - // Applies Exponential Curve, Deadzone, and Slow Mode toggle public double getDriveFwdPositive() { double fwdPositive = leftStickCurve.calculate(-1 * getLeftY()); - if (slowMode.getAsBoolean()) { - fwdPositive *= Math.abs(config.getSlowModeScalor()); - } return fwdPositive; } // Positive is left, left on the left stick is positive - // Applies Exponential Curve, Deadzone, and Slow Mode toggle public double getDriveLeftPositive() { double leftPositive = -1 * leftStickCurve.calculate(getLeftX()); - if (slowMode.getAsBoolean()) { - leftPositive *= Math.abs(config.getSlowModeScalor()); - } return leftPositive; } // Positive is counter-clockwise, left Trigger is positive - // Applies Exponential Curve, Deadzone, and Slow Mode toggle public double getDriveCCWPositive() { double ccwPositive = rightStickCurve.calculate(getRightX()); - if (slowMode.getAsBoolean()) { - ccwPositive *= Math.abs(config.getSlowModeScalor()); - } else if (turboMode.getAsBoolean()) { - ccwPositive *= Math.abs(config.getTurboModeScalor()); - } else { - ccwPositive *= Math.abs(config.getDefaultTurnScalor()); - } return -1 * ccwPositive; // invert the value } diff --git a/src/main/java/frc/robot/pilot/PilotStates.java b/src/main/java/frc/robot/pilot/PilotStates.java deleted file mode 100644 index 2590f088..00000000 --- a/src/main/java/frc/robot/pilot/PilotStates.java +++ /dev/null @@ -1,97 +0,0 @@ -package frc.robot.pilot; - -import com.ctre.phoenix6.Utils; -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.Commands; -import edu.wpi.first.wpilibj2.command.button.Trigger; -import frc.rebuilt.ShotCalculator; -import frc.robot.Robot; -import frc.robot.RobotSim; -import frc.robot.RobotStates; -import frc.robot.State; -import frc.robot.fuelIntake.FuelIntakeStates; -import frc.robot.intakeExtension.IntakeExtensionStates; -import frc.robot.vision.VisionStates; -import frc.spectrumLib.Telemetry; -import frc.spectrumLib.util.Util; - -/** This class should have any command calls that directly call the Pilot */ -public class PilotStates { - private static Pilot pilot = Robot.getPilot(); - - /** Set default command to turn off the rumble */ - public static void setupDefaultCommand() { - pilot.setDefaultCommand(log(rumble(0, 1).withName("Pilot.noRumble"))); - } - - private static Trigger reorientButton = - pilot.upReorient.or(pilot.downReorient, pilot.leftReorient, pilot.rightReorient); - - private static final Trigger launching = - new Trigger(() -> RobotStates.getAppliedState() == State.LAUNCH_WITH_SQUEEZE); - - /** Set the states for the pilot controller */ - public static void setStates() { - // Reset vision pose with Left Bumper and Select - pilot.visionPoseReset_LB_Select.onTrue(VisionStates.resetVisionPose()); - - pilot.BButton.whileTrue(IntakeExtensionStates.slowIntakeCloseCommand()); - pilot.YButton.whileTrue(Robot.getAuton().launch()); - - // Simulation Only: Map RT and LT to intake and launch fuel for testing - pilot.RT.and(Utils::isSimulation).whileTrue(RobotSim.mapleSimIntakeFuel()); - pilot.LT.and(Utils::isSimulation).whileTrue(RobotSim.mapleSimLaunchFuel()); - - pilot.rightTriggerOnly.and(pilot.fn).whileTrue(FuelIntakeStates.ejectCommand()); - - pilot.coastA.onTrue(IntakeExtensionStates.coastMode()); - pilot.brakeB.onTrue(IntakeExtensionStates.brakeMode()); - - pilot.dpadDown.onTrue(log(ShotCalculator.decreaseHoodAngleOffset())); - pilot.dpadUp.onTrue(log(ShotCalculator.increaseHoodAngleOffset())); - pilot.dpadRight.onTrue(log(ShotCalculator.decreaseDriveAngleOffset())); - pilot.dpadLeft.onTrue(log(ShotCalculator.increaseDriveAngleOffset())); - - // Slow mode when driver is launching fuel - launching.whileTrue(slowMode()); - - // Rumble whenever we reorient - reorientButton.onTrue(log(rumble(1, 0.5).withName("Pilot.reorientRumble"))); - } - - public static final Trigger buttonAPress = pilot.AButton; - - /** Command that can be used to rumble the pilot controller */ - public static Command rumble(double intensity, double durationSeconds) { - return pilot.rumbleCommand(intensity, durationSeconds) - .withName("Pilot.rumble") - .onlyIf(Util.autoMode.not()); - } - - /** - * Command that can be used to turn on the slow mode. Slow mode modifies the fwd, left, and CCW - * methods, we don't want these to require the pilot subsystem arm - */ - public static Command slowMode() { - return Commands.startEnd( - () -> pilot.getSlowMode().setState(true), - () -> pilot.getSlowMode().setState(false)) - .withName("Pilot.setSlowMode"); - } - - /** - * Command that can be used to turn on the turbo mode. Turbo mode modifies CCW methods, we don't - * want these to require the pilot subsystem - */ - public static Command turboMode() { - return Commands.startEnd( - () -> pilot.getTurboMode().setState(true), - () -> pilot.getTurboMode().setState(false)) - .withName("Pilot.setTurboMode"); - } - - // Log Command - protected static Command log(Command cmd) { - return Telemetry.log(cmd); - } -} diff --git a/src/main/java/frc/robot/subsystems/SuperStructure.java b/src/main/java/frc/robot/subsystems/SuperStructure.java new file mode 100644 index 00000000..65767a02 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/SuperStructure.java @@ -0,0 +1,330 @@ +package frc.robot.subsystems; + +import edu.wpi.first.wpilibj.Timer; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.InstantCommand; +import edu.wpi.first.wpilibj2.command.SubsystemBase; +import edu.wpi.first.wpilibj2.command.button.Trigger; +import frc.robot.subsystems.fuelIntake.FuelIntake; +import frc.robot.subsystems.indexerTower.IndexerTower; +import frc.robot.subsystems.intakeExtension.IntakeExtension; +import frc.robot.subsystems.launcher.Launcher; +import frc.robot.subsystems.spindexer.Spindexer; +import frc.robot.subsystems.swerve.Swerve; +import frc.robot.subsystems.turret.Turret; +import frc.spectrumLib.telemetry.Telemetry; +import frc.spectrumLib.util.Util; +import lombok.Getter; + +public class SuperStructure extends SubsystemBase { + + @Getter private final Swerve swerve; + @Getter private final FuelIntake fuelIntake; + @Getter private final IntakeExtension intakeExtension; + @Getter private final IndexerTower indexerTower; + @Getter private final Spindexer spindexer; + @Getter private final Launcher launcher; + @Getter private final Turret turret; + + private static final double REGULAR_TELEOP_TRANSLATION_COEFFICIENT = 1.0; + private static final double SHOOTING_TELEOP_TRANSLATION_COEFFICIENT = 0.1; + + public enum WantedSuperState { + IDLE, + INTAKE_FUEL, + TRACK_TARGET, + LAUNCH_WITH_SQUEEZE, + LAUNCH_WITH_SQUEEZE_WITH_NO_DELAY, + LAUNCH_WITHOUT_SQUEEZE, + AUTON_TRACK_TARGET, + AUTON_INTAKE_FUEL, + UNJAM, + FORCE_HOME, + } + + public enum CurrentSuperState { + IDLE, + INTAKE_FUEL, + TRACK_TARGET, + LAUNCH_WITH_SQUEEZE, + LAUNCH_WITH_SQUEEZE_WITH_NO_DELAY, + LAUNCH_WITHOUT_SQUEEZE, + AUTON_IDLE, + AUTON_TRACK_TARGET, + AUTON_INTAKE_FUEL, + UNJAM, + FORCE_HOME, + } + + @Getter private WantedSuperState wantedSuperState = WantedSuperState.IDLE; + @Getter private CurrentSuperState currentSuperState = CurrentSuperState.IDLE; + private CurrentSuperState previousSuperState = CurrentSuperState.IDLE; + + public SuperStructure( + Swerve swerve, + FuelIntake fuelIntake, + IntakeExtension intakeExtension, + IndexerTower indexerTower, + Spindexer spindexer, + Launcher launcher, + Turret turret) { + this.swerve = swerve; + this.fuelIntake = fuelIntake; + this.intakeExtension = intakeExtension; + this.indexerTower = indexerTower; + this.spindexer = spindexer; + this.launcher = launcher; + this.turret = turret; + } + + private final Timer intakeSqueezeTimer = new Timer(); + private final double secondsToSqueeze = 1.0; + + private static boolean isSqueezeState(CurrentSuperState state) { + return state == CurrentSuperState.LAUNCH_WITH_SQUEEZE; + } + + @Override + public void periodic() { + currentSuperState = handleStateTransitions(); + + // Restart the squeeze timer exactly once when first entering a squeeze state + if (isSqueezeState(currentSuperState) && !isSqueezeState(previousSuperState)) { + intakeSqueezeTimer.restart(); + } + + applyStates(); + + previousSuperState = currentSuperState; + + Telemetry.log("SuperStructure/WantedSuperState", wantedSuperState.toString()); + Telemetry.log("SuperStructure/CurrentSuperState", currentSuperState.toString()); + Telemetry.log( + "SuperStructure/IntakeSqueezeTimerElapsed", intakeSqueezeTimer.get(), "seconds"); + } + + private CurrentSuperState handleStateTransitions() { + return switch (wantedSuperState) { + case IDLE -> Util.autoMode.getAsBoolean() || Util.disabled.getAsBoolean() + ? CurrentSuperState.AUTON_IDLE + : CurrentSuperState.IDLE; + case INTAKE_FUEL -> CurrentSuperState.INTAKE_FUEL; + case TRACK_TARGET -> CurrentSuperState.TRACK_TARGET; + case LAUNCH_WITH_SQUEEZE -> CurrentSuperState.LAUNCH_WITH_SQUEEZE; + case LAUNCH_WITH_SQUEEZE_WITH_NO_DELAY -> CurrentSuperState + .LAUNCH_WITH_SQUEEZE_WITH_NO_DELAY; + case LAUNCH_WITHOUT_SQUEEZE -> CurrentSuperState.LAUNCH_WITHOUT_SQUEEZE; + case AUTON_TRACK_TARGET -> CurrentSuperState.AUTON_TRACK_TARGET; + case AUTON_INTAKE_FUEL -> CurrentSuperState.AUTON_INTAKE_FUEL; + case UNJAM -> CurrentSuperState.UNJAM; + case FORCE_HOME -> CurrentSuperState.FORCE_HOME; + }; + } + + private void applyStates() { + switch (currentSuperState) { + case IDLE: + applyIdle(); + break; + case INTAKE_FUEL: + intakeFuel(); + break; + case TRACK_TARGET: + trackTarget(); + break; + case LAUNCH_WITH_SQUEEZE: + launchWithSqueeze(); + break; + case LAUNCH_WITH_SQUEEZE_WITH_NO_DELAY: + launchWithSqueezeWithNoDelay(); + break; + case LAUNCH_WITHOUT_SQUEEZE: + launchWithoutSqueeze(); + break; + case AUTON_IDLE: + applyAutonIdle(); + break; + case AUTON_INTAKE_FUEL: + autonIntakeFuel(); + break; + case AUTON_TRACK_TARGET: + autonTrackTarget(); + break; + case UNJAM: + unjam(); + break; + case FORCE_HOME: + forceHome(); + break; + } + } + + // ── State methods ────────────────────────────────────────────────────────── + + private void applyIdle() { + swerve.setWantedState(Swerve.WantedState.TELEOP_DRIVE); + swerve.setTeleopVelocityCoefficient(REGULAR_TELEOP_TRANSLATION_COEFFICIENT); + fuelIntake.setWantedState(FuelIntake.WantedState.NEUTRAL); + indexerTower.setWantedState(IndexerTower.WantedState.OFF); + spindexer.setWantedState(Spindexer.WantedState.OFF); + intakeExtension.setWantedState(IntakeExtension.WantedState.STOPPED); + launcher.setWantedState(Launcher.WantedState.IDLE_PREP); + turret.setWantedState(Turret.WantedState.HOME); + } + + private void intakeFuel() { + swerve.setWantedState(Swerve.WantedState.TELEOP_DRIVE); + swerve.setTeleopVelocityCoefficient(REGULAR_TELEOP_TRANSLATION_COEFFICIENT); + fuelIntake.setWantedState(FuelIntake.WantedState.INTAKE); + indexerTower.setWantedState(IndexerTower.WantedState.OFF); + spindexer.setWantedState(Spindexer.WantedState.SLOW_INDEX); + intakeExtension.setWantedState(IntakeExtension.WantedState.FULL_EXTEND); + launcher.setWantedState(Launcher.WantedState.IDLE_PREP); + turret.setWantedState(Turret.WantedState.HOME); + } + + private void trackTarget() { + swerve.setWantedState(Swerve.WantedState.PILOT_AIM_AT_TARGET); + swerve.setTeleopVelocityCoefficient(REGULAR_TELEOP_TRANSLATION_COEFFICIENT); + fuelIntake.setWantedState(FuelIntake.WantedState.NEUTRAL); + indexerTower.setWantedState(IndexerTower.WantedState.OFF); + spindexer.setWantedState(Spindexer.WantedState.OFF); + intakeExtension.setWantedState(IntakeExtension.WantedState.CONDITIONAL_EXTEND); + launcher.setWantedState(Launcher.WantedState.IDLE_PREP); + turret.setWantedState(Turret.WantedState.AIM_AT_TARGET); + } + + private void launchWithSqueeze() { + swerve.setWantedState(Swerve.WantedState.PILOT_AIM_AT_TARGET); + swerve.setTeleopVelocityCoefficient(SHOOTING_TELEOP_TRANSLATION_COEFFICIENT); + fuelIntake.setWantedState(FuelIntake.WantedState.INTAKE); + indexerTower.setWantedState(IndexerTower.WantedState.INDEX_MAX); + spindexer.setWantedState(Spindexer.WantedState.INDEX_MAX); + launcher.setWantedState(Launcher.WantedState.LAUNCH); + turret.setWantedState(Turret.WantedState.AIM_AT_TARGET); + + if (intakeSqueezeTimer.hasElapsed(secondsToSqueeze)) { + intakeExtension.setWantedState(IntakeExtension.WantedState.SLOW_CLOSE); + intakeSqueezeTimer.stop(); + } else { + intakeExtension.setWantedState(IntakeExtension.WantedState.FULL_EXTEND); + } + } + + private void launchWithSqueezeWithNoDelay() { + swerve.setWantedState(Swerve.WantedState.PILOT_AIM_AT_TARGET); + swerve.setTeleopVelocityCoefficient(SHOOTING_TELEOP_TRANSLATION_COEFFICIENT); + fuelIntake.setWantedState(FuelIntake.WantedState.INTAKE); + indexerTower.setWantedState(IndexerTower.WantedState.INDEX_MAX); + spindexer.setWantedState(Spindexer.WantedState.INDEX_MAX); + intakeExtension.setWantedState(IntakeExtension.WantedState.SLOW_CLOSE); + launcher.setWantedState(Launcher.WantedState.LAUNCH); + turret.setWantedState(Turret.WantedState.AIM_AT_TARGET); + } + + private void launchWithoutSqueeze() { + swerve.setWantedState(Swerve.WantedState.PILOT_AIM_AT_TARGET); + swerve.setTeleopVelocityCoefficient(SHOOTING_TELEOP_TRANSLATION_COEFFICIENT); + fuelIntake.setWantedState(FuelIntake.WantedState.INTAKE); + indexerTower.setWantedState(IndexerTower.WantedState.INDEX_MAX); + spindexer.setWantedState(Spindexer.WantedState.INDEX_MAX); + intakeExtension.setWantedState(IntakeExtension.WantedState.CONDITIONAL_EXTEND); + launcher.setWantedState(Launcher.WantedState.LAUNCH); + turret.setWantedState(Turret.WantedState.AIM_AT_TARGET); + } + + private void applyAutonIdle() { + swerve.setWantedState(Swerve.WantedState.IDLE); + fuelIntake.setWantedState(FuelIntake.WantedState.NEUTRAL); + indexerTower.setWantedState(IndexerTower.WantedState.OFF); + spindexer.setWantedState(Spindexer.WantedState.OFF); + intakeExtension.setWantedState(IntakeExtension.WantedState.STOPPED); + launcher.setWantedState(Launcher.WantedState.IDLE_PREP); + turret.setWantedState(Turret.WantedState.HOME); + } + + private void autonIntakeFuel() { + fuelIntake.setWantedState(FuelIntake.WantedState.INTAKE); + indexerTower.setWantedState(IndexerTower.WantedState.OFF); + spindexer.setWantedState(Spindexer.WantedState.SLOW_INDEX); + intakeExtension.setWantedState(IntakeExtension.WantedState.FULL_EXTEND); + launcher.setWantedState(Launcher.WantedState.IDLE_PREP); + turret.setWantedState(Turret.WantedState.HOME); + } + + private void autonTrackTarget() { + fuelIntake.setWantedState(FuelIntake.WantedState.NEUTRAL); + indexerTower.setWantedState(IndexerTower.WantedState.OFF); + spindexer.setWantedState(Spindexer.WantedState.OFF); + intakeExtension.setWantedState(IntakeExtension.WantedState.CONDITIONAL_EXTEND); + launcher.setWantedState(Launcher.WantedState.LAUNCH); + turret.setWantedState(Turret.WantedState.AIM_AT_TARGET); + } + + private void unjam() { + swerve.setWantedState(Swerve.WantedState.TELEOP_DRIVE); + swerve.setTeleopVelocityCoefficient(REGULAR_TELEOP_TRANSLATION_COEFFICIENT); + fuelIntake.setWantedState(FuelIntake.WantedState.NEUTRAL); + indexerTower.setWantedState(IndexerTower.WantedState.UNJAM); + spindexer.setWantedState(Spindexer.WantedState.UNJAM); + intakeExtension.setWantedState(IntakeExtension.WantedState.CONDITIONAL_EXTEND); + launcher.setWantedState(Launcher.WantedState.OFF); + turret.setWantedState(Turret.WantedState.HOME); + } + + private void forceHome() { + swerve.setWantedState(Swerve.WantedState.TELEOP_DRIVE); + swerve.setTeleopVelocityCoefficient(REGULAR_TELEOP_TRANSLATION_COEFFICIENT); + fuelIntake.setWantedState(FuelIntake.WantedState.NEUTRAL); + indexerTower.setWantedState(IndexerTower.WantedState.OFF); + spindexer.setWantedState(Spindexer.WantedState.OFF); + spindexer.setWantedState(Spindexer.WantedState.OFF); + intakeExtension.setWantedState(IntakeExtension.WantedState.FULL_RETRACT); + launcher.setWantedState(Launcher.WantedState.OFF); + turret.setWantedState(Turret.WantedState.HOME); + } + + // ── Public API ───────────────────────────────────────────────────────────── + + // Allocation-free boolean checks — use these in per-loop code (e.g. ShotCalculator). + public boolean isRobotInNeutralZone() { + return swerve.isInNeutralZone(); + } + + public boolean isRobotInEnemyZone() { + return swerve.isInEnemyAllianceZone(); + } + + public boolean isRobotInFeedZone() { + return isRobotInEnemyZone() || isRobotInNeutralZone(); + } + + public boolean isRobotInScoreZone() { + return !isRobotInFeedZone(); + } + + // Trigger factories — use these for binding-time composition only. + public Trigger robotInNeutralZone() { + return new Trigger(this::isRobotInNeutralZone); + } + + public Trigger robotInEnemyZone() { + return new Trigger(this::isRobotInEnemyZone); + } + + public Trigger robotInFeedZone() { + return new Trigger(this::isRobotInFeedZone); + } + + public Trigger robotInScoreZone() { + return new Trigger(this::isRobotInScoreZone); + } + + public void setWantedSuperState(WantedSuperState state) { + this.wantedSuperState = state; + } + + public Command setStateCommand(WantedSuperState state) { + return new InstantCommand(() -> setWantedSuperState(state)); + } +} diff --git a/src/main/java/frc/robot/fuelIntake/FuelIntake.java b/src/main/java/frc/robot/subsystems/fuelIntake/FuelIntake.java similarity index 51% rename from src/main/java/frc/robot/fuelIntake/FuelIntake.java rename to src/main/java/frc/robot/subsystems/fuelIntake/FuelIntake.java index 01907eb6..0cdeacb9 100644 --- a/src/main/java/frc/robot/fuelIntake/FuelIntake.java +++ b/src/main/java/frc/robot/subsystems/fuelIntake/FuelIntake.java @@ -1,60 +1,44 @@ -package frc.robot.fuelIntake; +package frc.robot.subsystems.fuelIntake; import com.ctre.phoenix6.signals.MotorAlignmentValue; import com.ctre.phoenix6.sim.TalonFXSimState; import edu.wpi.first.math.util.Units; -import edu.wpi.first.networktables.DoubleSubscriber; import edu.wpi.first.wpilibj.smartdashboard.Mechanism2d; -import edu.wpi.first.wpilibj2.command.Command; import frc.robot.Robot; import frc.robot.RobotSim; -import frc.spectrumLib.Rio; -import frc.spectrumLib.Telemetry; +import frc.spectrumLib.hardware.Rio; import frc.spectrumLib.mechanism.Mechanism; import frc.spectrumLib.sim.RollerConfig; import frc.spectrumLib.sim.RollerSim; -import java.util.function.DoubleSupplier; +import frc.spectrumLib.telemetry.Telemetry; import lombok.Getter; -import lombok.Setter; /** The Fuel Intake subsystem. Responsible for intake and handling of fuel elements. */ public class FuelIntake extends Mechanism { public static class FuelIntakeConfig extends Config { - // Intake Voltages and Current - @Getter @Setter private double fuelIntakeVoltage = 9.0; - - @Getter @Setter private double fuelAgitationTorqueCurrent = 45.0; - @Getter @Setter private double fuelSlowIntakeTorqueCurrent = 45.0; - @Getter @Setter private double fuelIntakeTorqueCurrent = 130.0; - @Getter @Setter private double ejectTorqueCurrent = -50; - - @Getter - private final DoubleSubscriber intakeTorqueCurrent = - Telemetry.tunable("Tunable/IntakeTorqueCurrent", fuelIntakeTorqueCurrent); - /* Intake config values */ - @Getter private double currentLimit = 70; - @Getter private double torqueCurrentLimit = 180; - @Getter private double velocityKp = 5; - @Getter private double velocityKv = 0; - @Getter private double velocityKs = 4; + @Getter private final double supplyCurrentLimit = 70; + @Getter private final double statorCurrentLimit = 180; + @Getter private final double velocityKp = 5; + @Getter private final double velocityKv = 0; + @Getter private final double velocityKs = 4; /* Sim Configs */ - @Getter private double intakeX = Units.inchesToMeters(15); - @Getter private double intakeY = Units.inchesToMeters(23); - @Getter private double wheelDiameter = 6; + @Getter private final double intakeX = Units.inchesToMeters(15); + @Getter private final double intakeY = Units.inchesToMeters(23); + @Getter private final double wheelDiameter = 6; public FuelIntakeConfig() { - super("Intake", 5, Rio.RIO_CANBUS); + super("Intake Left", 5, Rio.RIO_CANBUS); configPIDGains(0, velocityKp, 0, 0); configFeedForwardGains(velocityKs, velocityKv, 0, 0); configGearRatio(1); - configSupplyCurrentLimit(currentLimit, true); - configStatorCurrentLimit(torqueCurrentLimit, true); - configForwardTorqueCurrentLimit(torqueCurrentLimit); - configReverseTorqueCurrentLimit(torqueCurrentLimit); + configSupplyCurrentLimit(supplyCurrentLimit, true); + configStatorCurrentLimit(statorCurrentLimit, true); + configForwardTorqueCurrentLimit(statorCurrentLimit); + configReverseTorqueCurrentLimit(statorCurrentLimit); configNeutralBrakeMode(false); configCounterClockwise_Positive(); setFollowerConfigs( @@ -63,8 +47,60 @@ public FuelIntakeConfig() { } } - private FuelIntakeConfig config; - private FuelIntakeSim sim; + // ---- State Machine ---- + + public enum WantedState { + NEUTRAL, + OFF, + INTAKE, + SLOW_INTAKE, + } + + public enum SystemState { + NEUTRAL, + OFF, + INTAKE, + SLOW_INTAKE, + } + + private WantedState wantedState = WantedState.NEUTRAL; + private SystemState systemState = SystemState.NEUTRAL; + + public void setWantedState(WantedState state) { + this.wantedState = state; + } + + private SystemState handleStateTransition() { + return switch (wantedState) { + case NEUTRAL -> SystemState.NEUTRAL; + case INTAKE -> SystemState.INTAKE; + case SLOW_INTAKE -> SystemState.SLOW_INTAKE; + case OFF -> SystemState.OFF; + }; + } + + private void applyStates() { + double wantedTorqueCurrent = 0; + switch (systemState) { + case NEUTRAL: + wantedTorqueCurrent = 0; + break; + case INTAKE: + wantedTorqueCurrent = 130; + break; + case SLOW_INTAKE: + wantedTorqueCurrent = 45; + break; + case OFF: + stop(); + return; + } + final double finalWantedTorqueCurrent = wantedTorqueCurrent; + setTorqueCurrentFoc(() -> finalWantedTorqueCurrent); + } + + @Getter private final FuelIntakeConfig config; + @Getter private FuelIntakeSim sim; public FuelIntake(FuelIntakeConfig config) { super(config); @@ -76,7 +112,11 @@ public FuelIntake(FuelIntakeConfig config) { @Override public void periodic() { + systemState = handleStateTransition(); + applyStates(); logBatteryUsage(); + Telemetry.log("FuelIntake/WantedState", wantedState.toString()); + Telemetry.log("FuelIntake/SystemState", systemState.toString()); Telemetry.log("FuelIntake/CurrentCommand", getCurrentCommandName()); Telemetry.log("FuelIntake/Voltage", getVoltage(), "volts"); Telemetry.log("FuelIntake/StatorCurrent", getStatorCurrent(), "amps"); @@ -85,48 +125,13 @@ public void periodic() { Telemetry.log("FuelIntake/Temp", getTemp(), "deg_C"); } - @Override - public void setupStates() {} - - @Override - public void setupDefaultCommand() { - FuelIntakeStates.setupDefaultCommand(); - } - - // -------------------------------------------------------------------------------- - // Custom Commands - // -------------------------------------------------------------------------------- - - public Command runTorqueFOC(DoubleSupplier torque) { - return run(() -> setTorqueCurrentFoc(torque)); - } - - public void setVoltageAndCurrentLimits( - DoubleSupplier voltage, DoubleSupplier supply, DoubleSupplier torque) { - setVoltageOutput(voltage); - setCurrentLimits(supply, torque); - } - - public Command runVoltageCurrentLimits( - DoubleSupplier voltage, DoubleSupplier supplyCurrent, DoubleSupplier torqueCurrent) { - return runVoltage(voltage).alongWith(runCurrentLimits(supplyCurrent, torqueCurrent)); - } - - public Command runTCcurrentLimits(DoubleSupplier torqueCurrent, DoubleSupplier supplyCurrent) { - return runTorqueCurrentFoc(torqueCurrent) - .alongWith(runCurrentLimits(supplyCurrent, torqueCurrent)); - } - - public Command stopMotor() { - return run(() -> stop()); - } - // -------------------------------------------------------------------------------- // Simulation // -------------------------------------------------------------------------------- public void simulationInit() { if (isAttached()) { - // Create a new RollerSim with the left view, the motor's sim state, and a 6 in diameter + // Create a new RollerSim with the left view, the motor's sim state, and a 6 in + // diameter sim = new FuelIntakeSim(RobotSim.leftView, motor.getSimState()); } } diff --git a/src/main/java/frc/robot/subsystems/indexerTower/IndexerTower.java b/src/main/java/frc/robot/subsystems/indexerTower/IndexerTower.java new file mode 100644 index 00000000..5c9dbf0c --- /dev/null +++ b/src/main/java/frc/robot/subsystems/indexerTower/IndexerTower.java @@ -0,0 +1,162 @@ +package frc.robot.subsystems.indexerTower; + +import com.ctre.phoenix6.signals.MotorAlignmentValue; +import com.ctre.phoenix6.sim.TalonFXSimState; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.wpilibj.smartdashboard.Mechanism2d; +import frc.robot.RobotSim; +import frc.spectrumLib.hardware.Rio; +import frc.spectrumLib.mechanism.Mechanism; +import frc.spectrumLib.sim.RollerConfig; +import frc.spectrumLib.sim.RollerSim; +import frc.spectrumLib.telemetry.Telemetry; +import lombok.Getter; + +/** The Indexer Tower subsystem. Lifts fuel from the bed up to the launcher. */ +public class IndexerTower extends Mechanism { + + public static class IndexerTowerConfig extends Config { + /* Indexer config values */ + @Getter private final double supplyCurrentLimit = 40; + @Getter private final double statorCurrentLimit = 180; + @Getter private final double lowerSupplyCurrentLimit = 40; + @Getter private final double lowerSupplyCurrentTime = 0; + @Getter private final double velocityKp = 50; + @Getter private final double velocityKv = 0; + @Getter private final double velocityKs = 40; + + /* Sim Configs */ + @Getter private final double intakeX = Units.inchesToMeters(60); + @Getter private final double intakeY = Units.inchesToMeters(75); + @Getter private final double wheelDiameter = 12; + + public IndexerTowerConfig() { + super("IndexerTower", 51, Rio.CANIVORE); + configPIDGains(0, velocityKp, 0, 0); + configFeedForwardGains(velocityKs, velocityKv, 0, 0); + configGearRatio(1); + configSupplyCurrentLimit(supplyCurrentLimit, true); + configStatorCurrentLimit(statorCurrentLimit, true); + configForwardTorqueCurrentLimit(statorCurrentLimit); + configReverseTorqueCurrentLimit(statorCurrentLimit); + configLowerSupplyCurrentLimit(lowerSupplyCurrentLimit); + configLowerSupplyCurrentTime(lowerSupplyCurrentTime); + configNeutralBrakeMode(true); + configClockwise_Positive(); + setFollowerConfigs( + new FollowerConfig( + "IndexerTower Follower", + 52, + Rio.CANIVORE, + MotorAlignmentValue.Aligned)); + } + } + + // ---- State Machine ---- + + public enum WantedState { + OFF, + INDEX_MAX, + SLOW_INDEX, + UNJAM, + } + + public enum SystemState { + OFF, + INDEX_MAX, + SLOW_INDEX, + UNJAM, + } + + private WantedState wantedState = WantedState.OFF; + private SystemState systemState = SystemState.OFF; + + public void setWantedState(WantedState state) { + this.wantedState = state; + } + + private SystemState handleStateTransition() { + return switch (wantedState) { + case OFF -> SystemState.OFF; + case INDEX_MAX -> SystemState.INDEX_MAX; + case SLOW_INDEX -> SystemState.SLOW_INDEX; + case UNJAM -> SystemState.UNJAM; + }; + } + + private void applyStates() { + double wantedRPM = 0; + switch (systemState) { + case OFF: + stop(); + return; + case INDEX_MAX: + wantedRPM = 4000; + break; + case SLOW_INDEX: + wantedRPM = 1000; + break; + case UNJAM: + wantedRPM = -1500; + break; + } + final double finalWantedRPM = wantedRPM; + setVelocityTCFOCrpm(() -> finalWantedRPM); + } + + @Getter private final IndexerTowerConfig config; + @Getter private IndexerSim sim; + + public IndexerTower(IndexerTowerConfig config) { + super(config); + this.config = config; + + simulationInit(); + Telemetry.print(getName() + " Subsystem Initialized"); + } + + @Override + public void periodic() { + systemState = handleStateTransition(); + applyStates(); + logBatteryUsage(); + Telemetry.log("IndexerTower/WantedState", wantedState.toString()); + Telemetry.log("IndexerTower/SystemState", systemState.toString()); + Telemetry.log("IndexerTower/CurrentCommand", getCurrentCommandName()); + Telemetry.log("IndexerTower/Voltage", getVoltage(), "volts"); + Telemetry.log("IndexerTower/StatorCurrent", getStatorCurrent(), "amps"); + Telemetry.log("IndexerTower/SupplyCurrent", getSupplyCurrent(), "amps"); + Telemetry.log("IndexerTower/RPM", getVelocityRPM(), "RPM"); + Telemetry.log("IndexerTower/Temp", getTemp(), "deg_C"); + } + + // -------------------------------------------------------------------------------- + // Simulation + // -------------------------------------------------------------------------------- + public void simulationInit() { + if (isAttached()) { + // Create a new RollerSim with the left view, the motor's sim state, and a 6 in diameter + sim = new IndexerSim(RobotSim.topView, motor.getSimState()); + } + } + + // Must be called to enable the simulation + // if roller position changes configure x and y to set position. + @Override + public void simulationPeriodic() { + if (isAttached()) { + sim.simulationPeriodic(); + } + } + + class IndexerSim extends RollerSim { + public IndexerSim(Mechanism2d mech, TalonFXSimState rollerMotorSim) { + super( + new RollerConfig(config.getWheelDiameter()) + .setPosition(config.getIntakeX(), config.getIntakeY()), + mech, + rollerMotorSim, + config.getName()); + } + } +} diff --git a/src/main/java/frc/robot/intakeExtension/IntakeExtension.java b/src/main/java/frc/robot/subsystems/intakeExtension/IntakeExtension.java similarity index 50% rename from src/main/java/frc/robot/intakeExtension/IntakeExtension.java rename to src/main/java/frc/robot/subsystems/intakeExtension/IntakeExtension.java index 43470d5d..9ad274f8 100644 --- a/src/main/java/frc/robot/intakeExtension/IntakeExtension.java +++ b/src/main/java/frc/robot/subsystems/intakeExtension/IntakeExtension.java @@ -1,58 +1,50 @@ -package frc.robot.intakeExtension; +package frc.robot.subsystems.intakeExtension; import com.ctre.phoenix6.configs.TalonFXConfiguration; import com.ctre.phoenix6.configs.TalonFXConfigurator; import com.ctre.phoenix6.hardware.TalonFX; import com.ctre.phoenix6.sim.TalonFXSimState; import edu.wpi.first.math.util.Units; -import edu.wpi.first.networktables.DoubleSubscriber; import edu.wpi.first.wpilibj.smartdashboard.Mechanism2d; import edu.wpi.first.wpilibj.util.Color; import edu.wpi.first.wpilibj.util.Color8Bit; import edu.wpi.first.wpilibj2.command.Command; import frc.robot.RobotSim; -import frc.robot.RobotStates; -import frc.robot.State; -import frc.spectrumLib.Rio; -import frc.spectrumLib.Telemetry; +import frc.spectrumLib.hardware.Rio; import frc.spectrumLib.mechanism.Mechanism; import frc.spectrumLib.sim.LinearConfig; import frc.spectrumLib.sim.LinearSim; -import frc.spectrumLib.util.Util; -import java.util.function.DoubleSupplier; +import frc.spectrumLib.telemetry.Telemetry; import lombok.Getter; import lombok.Setter; +/** The Intake Extension subsystem. Extends and retracts the fuel intake. */ public class IntakeExtension extends Mechanism { public static class IntakeExtensionConfig extends Config { @Getter private final double initPosition = 0; - @Getter private double triggerTolerance = 5; + @Getter private final double triggerTolerance = 5; /* Intake Extension config settings */ @Getter private final double zeroSpeed = -0.1; @Getter private final double holdMaxSpeedRPM = 18; - @Getter @Setter private double maxRotations = 2.779053; - @Getter @Setter private double minRotations = 0.0; + @Getter private final double maxRotations = 2.779053; + @Getter private final double minRotations = 0.0; /* Positions are in percent of max rotations (0% -> 0 rotations | 100% -> max rotation) */ - @Getter private double home = 0; - @Getter private double squeeze = 25; - @Getter private double fullOut = 100; - @Getter private double atPoseTolerance = 10; - @Getter private double springyPoseTolerance = 20; + @Getter private final double home = 0; + @Getter private final double squeeze = 25; + @Getter private final double fullOut = 100; + @Getter private final double atPoseTolerance = 10; + @Getter private final double springyPoseTolerance = 20; - @Getter private double positiveVoltageOut = 10; - @Getter private double negativeVoltageOut = -10; + @Getter private final double positiveVoltageOut = 10; + @Getter private final double negativeVoltageOut = -10; - @Getter - private final DoubleSubscriber timeUntilIntakeSqueeze = - Telemetry.tunable("Tunable/TimeUntilIntakeSqueeze", 1.0); - - @Getter private final double normalCurrentLimit = 40; - @Getter private final double normalTorqueCurrentLimit = 80; + @Getter private final double normalSupplyCurrentLimit = 40; + @Getter private final double normalStatorCurrentLimit = 80; @Getter private final double springyModeSupplyCurrentLimit = 5; @Getter private final double springyModeStatorCurrentLimit = 20; @@ -68,29 +60,29 @@ public static class IntakeExtensionConfig extends Config { @Getter private final double mmAcceleration = 300; @Getter private final double mmJerk = 1000; - @Getter @Setter private double sensorToMechanismRatio = 11.25; - @Getter @Setter private double rotorToSensorRatio = 1; + @Getter private final double sensorToMechanismRatio = 11.25; + @Getter private final double rotorToSensorRatio = 1; /* Cancoder config settings */ - @Getter @Setter private double CANcoderRotorToSensorRatio = 1.7; + @Getter private final double CANcoderRotorToSensorRatio = 1.7; // CANcoderRotorToSensorRatio / sensorToMechanismRatio; - @Getter @Setter private double CANcoderSensorToMechanismRatio = 1; + @Getter private final double CANcoderSensorToMechanismRatio = 1; - @Getter @Setter private double CANcoderOffset = 0; - @Getter @Setter private boolean CANcoderAttached = false; + @Getter private final double CANcoderOffset = 0; + @Getter private final boolean CANcoderAttached = false; /* Sim Configs */ - @Getter private double intakeX = Units.inchesToMeters(70); - @Getter private double intakeY = Units.inchesToMeters(23); - @Getter private double extensionMass = 10.0; - @Getter private double drumRadiusMeters = Units.inchesToMeters(0.955 / 2); - @Getter private double extensionGearing = 1.7; - @Getter private double angle = 180; - @Getter private double staticLength = 10; - @Getter private double movingLength = 55; - @Getter private double lineWidth = 20; - @Getter private double maxExtensionHeight = 40; + @Getter private final double intakeX = Units.inchesToMeters(70); + @Getter private final double intakeY = Units.inchesToMeters(23); + @Getter private final double extensionMass = 10.0; + @Getter private final double drumRadiusMeters = Units.inchesToMeters(0.955 / 2); + @Getter private final double extensionGearing = 11.25; + @Getter private final double angle = 180; + @Getter private final double staticLength = 10; + @Getter private final double movingLength = 55; + @Getter private final double lineWidth = 20; + @Getter private final double maxExtensionHeight = 40; public IntakeExtensionConfig() { super("IntakeExtension", 7, Rio.CANIVORE); // Rio.CANIVORE); @@ -98,11 +90,11 @@ public IntakeExtensionConfig() { configPIDGains(0, positionKp, positionKi, positionKd); configFeedForwardGains(positionKs, positionKv, positionKa, positionKg); configMotionMagic(mmCruiseVelocity, mmAcceleration, mmJerk); - configSupplyCurrentLimit(normalCurrentLimit, true); - configStatorCurrentLimit(normalTorqueCurrentLimit, true); + configSupplyCurrentLimit(normalSupplyCurrentLimit, true); + configStatorCurrentLimit(normalStatorCurrentLimit, true); configGearRatio(gearRatio); - configForwardTorqueCurrentLimit(normalTorqueCurrentLimit); - configReverseTorqueCurrentLimit(-1 * normalTorqueCurrentLimit); + configForwardTorqueCurrentLimit(normalStatorCurrentLimit); + configReverseTorqueCurrentLimit(normalStatorCurrentLimit); configForwardSoftLimit(maxRotations, true); configReverseSoftLimit(minRotations, true); configNeutralBrakeMode(true); @@ -119,150 +111,130 @@ public IntakeExtensionConfig modifyMotorConfig(TalonFX motor) { } } - @Getter private IntakeExtensionConfig config; - @Getter private IntakeExtensionSim sim; - - @Getter @Setter private boolean inSpringyMode = false; - - public IntakeExtension(IntakeExtensionConfig config) { - super(config); - this.config = config; - - if (isAttached()) { - setInitialPosition(); - } + // ---- State Machine ---- - simulationInit(); - Telemetry.print(getName() + " Subsystem Initialized"); + public enum WantedState { + STOPPED, + FULL_EXTEND, + CONDITIONAL_EXTEND, + FULL_RETRACT, + SLOW_CLOSE, } - @Override - public void periodic() { - // updateSpringyMode(); - - logBatteryUsage(); - Telemetry.log("IntakeExtension/CurrentCommand", getCurrentCommandName()); - Telemetry.log("IntakeExtension/Voltage", getVoltage(), "volts"); - Telemetry.log("IntakeExtension/StatorCurrent", getStatorCurrent(), "amps"); - Telemetry.log("IntakeExtension/SupplyCurrent", getSupplyCurrent(), "amps"); - Telemetry.log("IntakeExtension/Position", getPositionRotations(), "rotations"); - Telemetry.log("IntakeExtension/RPM", getVelocityRPM(), "RPM"); - Telemetry.log("IntakeExtension/Temp", getTemp(), "deg_C"); - Telemetry.log("IntakeExtension/InSpringyMode", inSpringyMode); - } - - @Override - public void setupStates() {} - - @Override - public void setupDefaultCommand() { - IntakeExtensionStates.setupDefaultCommand(); + public enum SystemState { + STOPPED, + FULL_EXTEND, + FULL_RETRACT, + SLOW_CLOSE, } - private void setInitialPosition() { - motor.setPosition(degreesToRotations(() -> config.getInitPosition())); - } + private WantedState wantedState = WantedState.STOPPED; + private SystemState systemState = SystemState.STOPPED; + private boolean sentOutByIntakeState = false; - public void resetCurrentPositionToMax() { - motor.setPosition(config.getMaxRotations()); + public void setWantedState(WantedState state) { + this.wantedState = state; } - public Command resetToInitialPos() { - return run(this::setInitialPosition); + private SystemState handleStateTransition() { + return switch (wantedState) { + case STOPPED -> SystemState.STOPPED; + case FULL_EXTEND -> { + sentOutByIntakeState = true; + yield SystemState.FULL_EXTEND; + } + case CONDITIONAL_EXTEND -> sentOutByIntakeState + ? SystemState.FULL_EXTEND + : SystemState.STOPPED; + case FULL_RETRACT -> { + sentOutByIntakeState = false; + yield SystemState.FULL_RETRACT; + } + case SLOW_CLOSE -> SystemState.SLOW_CLOSE; + }; } - public void setSpringyMode(boolean enabled) { - if (enabled && !inSpringyMode) { - setCurrentLimits( - config::getSpringyModeSupplyCurrentLimit, - config::getSpringyModeStatorCurrentLimit); - inSpringyMode = true; - } else if (!enabled && inSpringyMode) { - setCurrentLimits(config::getNormalCurrentLimit, config::getNormalTorqueCurrentLimit); - inSpringyMode = false; + private void applyStates() { + boolean slowMove = false; + double wantedPercent = 0; + switch (systemState) { + case FULL_EXTEND: + wantedPercent = 100; + break; + case FULL_RETRACT: + wantedPercent = 0; + break; + case SLOW_CLOSE: + slowMove = true; + wantedPercent = 25; + break; + case STOPPED: + stop(); + return; + } + final double finalWantedPercent = wantedPercent; + final double finalRotation = percentToRotations(() -> finalWantedPercent); + if (slowMove) { + setDynMMPositionVoltage(() -> finalRotation, () -> 4.0, () -> 20.0, () -> 1000.0); + } else { + setMMPosition(() -> finalRotation); } } - public void updateSpringyMode() { - State currentState = RobotStates.getAppliedState(); - boolean isLaunching = - currentState == State.AUTON_LAUNCH_WITH_SQUEEZE - || currentState == State.LAUNCH_WITH_SQUEEZE - || currentState == State.LAUNCH_WITH_SQUEEZE_WITH_NO_DELAY; - boolean atFullOut = - atPercentage(config::getFullOut, config::getSpringyPoseTolerance).getAsBoolean(); - - boolean inAuto = Util.autoMode.getAsBoolean(); - - setSpringyMode(!isLaunching && atFullOut && !inAuto); - } - - // -------------------------------------------------------------------------------- - // Custom Commands - // -------------------------------------------------------------------------------- + @Getter private final IntakeExtensionConfig config; + @Getter private IntakeExtensionSim sim; - /** Holds the position of the Intake Extension. */ - public Command runHoldIntakeExtension() { - return new Command() { - double holdPosition = 0; // rotations + @Getter @Setter private boolean inSpringyMode = false; - // constructor - { - setName("IntakeExtension.holdPosition"); - addRequirements(IntakeExtension.this); - } + public IntakeExtension(IntakeExtensionConfig config) { + super(config); + this.config = config; - @Override - public boolean runsWhenDisabled() { - return true; - } + setInitialPosition(); - @Override - public void initialize() { - holdPosition = getPositionRotations(); - stop(); - } + simulationInit(); + Telemetry.print(getName() + " Subsystem Initialized"); + } - @Override - public void execute() { - if (Math.abs(getVelocityRPM()) > config.holdMaxSpeedRPM) { - stop(); - holdPosition = getPositionRotations(); - } else { - setDynMMPositionFoc( - () -> holdPosition, - () -> config.getMmCruiseVelocity(), - () -> config.getMmAcceleration(), - () -> 20); - } - } + private void setInitialPosition() { + if (isAttached()) { + motor.setPosition(degreesToRotations(() -> config.getInitPosition())); + } + } - @Override - public void end(boolean interrupted) { - stop(); - } - }; + public void resetCurrentPositionToMax() { + motor.setPosition(config.getMaxRotations()); } - public Command move(DoubleSupplier rotations) { - return run(() -> setVoltageOutput(rotations)); + public Command resetCurrentPositionToMaxCommand() { + return run(this::resetCurrentPositionToMax); } - public Command motionMagicPercentMove(DoubleSupplier percent) { - return run(() -> setMMPosition(() -> percentToRotations(percent))); + public Command resetToInitialPos() { + return run(this::setInitialPosition); } - public Command slowMoveToPercent(DoubleSupplier percent) { - return run( - () -> - setDynMMPositionVoltage( - () -> percentToRotations(percent), () -> 4, () -> 20, () -> 1000)); + @Override + public void periodic() { + systemState = handleStateTransition(); + applyStates(); + logBatteryUsage(); + Telemetry.log("IntakeExtension/WantedState", wantedState.toString()); + Telemetry.log("IntakeExtension/SystemState", systemState.toString()); + Telemetry.log("IntakeExtension/CurrentCommand", getCurrentCommandName()); + Telemetry.log("IntakeExtension/Voltage", getVoltage(), "volts"); + Telemetry.log("IntakeExtension/StatorCurrent", getStatorCurrent(), "amps"); + Telemetry.log("IntakeExtension/SupplyCurrent", getSupplyCurrent(), "amps"); + Telemetry.log("IntakeExtension/Position", getPositionRotations(), "rotations"); + Telemetry.log("IntakeExtension/RPM", getVelocityRPM(), "RPM"); + Telemetry.log("IntakeExtension/Temp", getTemp(), "deg_C"); + Telemetry.log("IntakeExtension/InSpringyMode", inSpringyMode); } // -------------------------------------------------------------------------------- // Simulation // -------------------------------------------------------------------------------- - private void simulationInit() { + public void simulationInit() { if (isAttached()) { sim = new IntakeExtensionSim(RobotSim.leftView, motor.getSimState()); } diff --git a/src/main/java/frc/robot/launcher/Launcher.java b/src/main/java/frc/robot/subsystems/launcher/Launcher.java similarity index 56% rename from src/main/java/frc/robot/launcher/Launcher.java rename to src/main/java/frc/robot/subsystems/launcher/Launcher.java index 2feb5569..93bd20c5 100644 --- a/src/main/java/frc/robot/launcher/Launcher.java +++ b/src/main/java/frc/robot/subsystems/launcher/Launcher.java @@ -1,21 +1,17 @@ -package frc.robot.launcher; +package frc.robot.subsystems.launcher; import com.ctre.phoenix6.signals.MotorAlignmentValue; import com.ctre.phoenix6.sim.TalonFXSimState; import edu.wpi.first.math.util.Units; import edu.wpi.first.networktables.DoubleSubscriber; import edu.wpi.first.wpilibj.smartdashboard.Mechanism2d; -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.button.Trigger; import frc.rebuilt.ShotCalculator; -import frc.robot.Robot; import frc.robot.RobotSim; -import frc.spectrumLib.Rio; -import frc.spectrumLib.Telemetry; +import frc.spectrumLib.hardware.Rio; import frc.spectrumLib.mechanism.Mechanism; import frc.spectrumLib.sim.RollerConfig; import frc.spectrumLib.sim.RollerSim; -import java.util.function.DoubleSupplier; +import frc.spectrumLib.telemetry.*; import lombok.Getter; import lombok.Setter; @@ -26,7 +22,7 @@ public static class LauncherConfig extends Config { // Intake Voltages and Current @Getter @Setter private double LauncherVoltage = 9.0; @Getter @Setter private double LauncherSupplyCurrent = 30.0; - @Getter @Setter private double LauncherTorqueCurrent = 85.0; + @Getter @Setter private double LauncherStatorCurrent = 85.0; @Getter @Setter private double idlingRPM = 700; @Getter @Setter private double slowLaunchSpeed = 400; @@ -37,11 +33,11 @@ public static class LauncherConfig extends Config { Telemetry.tunable("Launcher/OnTheFlySpeed", 0.0); /* Launcher config values */ - @Getter private double currentLimit = 80; - @Getter private double torqueCurrentLimit = 100; - @Getter private double forwardTorqueCurrentLimit = torqueCurrentLimit; - @Getter private double reverseTorqueCurrentLimit = -10; - @Getter private double lowerCurrentLimit = 60; + @Getter private double supplyCurrentLimit = 80; + @Getter private double statorCurrentLimit = 100; + @Getter private double forwardStatorCurrentLimit = statorCurrentLimit; + @Getter private double reverseStatorCurrentLimit = -10; + @Getter private double lowerSupplyCurrentLimit = 60; @Getter private double timeUntilLowerCurrent = 1; @Getter private double nominalVoltage = 16; @Getter private double velocityKp = 10; @@ -51,8 +47,8 @@ public static class LauncherConfig extends Config { @Getter private double onTargetToleranceRPM = 100; /* Sim Configs */ - @Getter private double launcherX = Units.inchesToMeters(62.5); - @Getter private double launcherY = Units.inchesToMeters(60); + @Getter private double launcherX = Units.inchesToMeters(50); + @Getter private double launcherY = Units.inchesToMeters(63); @Getter private double wheelDiameter = 4; public LauncherConfig() { @@ -60,12 +56,12 @@ public LauncherConfig() { configPIDGains(0, velocityKp, 0, 0); configFeedForwardGains(velocityKs, velocityKv, 0, 0); configGearRatio(1); - configLowerSupplyCurrentLimit(lowerCurrentLimit); + configLowerSupplyCurrentLimit(lowerSupplyCurrentLimit); configLowerSupplyCurrentTime(timeUntilLowerCurrent); - configSupplyCurrentLimit(currentLimit, true); - configStatorCurrentLimit(torqueCurrentLimit, true); - configForwardTorqueCurrentLimit(forwardTorqueCurrentLimit); - configReverseTorqueCurrentLimit(reverseTorqueCurrentLimit); + configSupplyCurrentLimit(supplyCurrentLimit, true); + configStatorCurrentLimit(statorCurrentLimit, true); + configForwardTorqueCurrentLimit(forwardStatorCurrentLimit); + configReverseTorqueCurrentLimit(reverseStatorCurrentLimit); configNeutralBrakeMode(false); configForwardVoltageLimit(nominalVoltage); configReverseVoltageLimit(nominalVoltage); @@ -83,8 +79,58 @@ public LauncherConfig() { } } - private LauncherConfig config; - private LauncherSim sim; + // ---- State Machine ---- + + public enum WantedState { + OFF, + IDLE_PREP, + LAUNCH, + } + + public enum SystemState { + OFF, + IDLE_PREP, + LAUNCH, + } + + private WantedState wantedState = WantedState.OFF; + private SystemState systemState = SystemState.OFF; + + public void setWantedState(WantedState state) { + this.wantedState = state; + } + + @SuppressWarnings("unused") + private SystemState handleStateTransition() { + return switch (wantedState) { + case OFF -> SystemState.OFF; + case IDLE_PREP -> SystemState.IDLE_PREP; + case LAUNCH -> SystemState.LAUNCH; + }; + } + + // TODO: add actual values when robot is built + @SuppressWarnings("unused") + private void applyStates() { + double wantedRPM = 0; + switch (systemState) { + case OFF: + stop(); + return; + case IDLE_PREP: + wantedRPM = 700; + break; + case LAUNCH: + var params = ShotCalculator.getInstance().getParameters(); + wantedRPM = params.flywheelSpeed() + ShotCalculator.FLYWHEEL_SPEED_OFFSET; + break; + } + final double finalWantedRPM = wantedRPM; + setVelocityTCFOCrpm(() -> finalWantedRPM); + } + + @Getter private LauncherConfig config; + @Getter private LauncherSim sim; public Launcher(LauncherConfig config) { super(config); @@ -96,7 +142,9 @@ public Launcher(LauncherConfig config) { @Override public void periodic() { + systemState = handleStateTransition(); logBatteryUsage(); + applyStates(); Telemetry.log("Launcher/CurrentCommand", getCurrentCommandName()); Telemetry.log("Launcher/Voltage", getVoltage(), "volts"); Telemetry.log("Launcher/StatorCurrent", getStatorCurrent(), "amps"); @@ -105,74 +153,9 @@ public void periodic() { Telemetry.log("Launcher/Temp", getTemp(), "deg_C"); } - @Override - public void setupStates() {} - - @Override - public void setupDefaultCommand() { - LauncherStates.setupDefaultCommand(); - } - - // -------------------------------------------------------------------------------- - // Custom Commands - // -------------------------------------------------------------------------------- - - public Command runTorqueFOC(DoubleSupplier torque) { - return run(() -> setTorqueCurrentFoc(torque)); - } - - public void setVoltageAndCurrentLimits( - DoubleSupplier voltage, DoubleSupplier supply, DoubleSupplier torque) { - setVoltageOutput(voltage); - setCurrentLimits(supply, torque); - } - - public Command runVoltageCurrentLimits( - DoubleSupplier voltage, DoubleSupplier supplyCurrent, DoubleSupplier torqueCurrent) { - return runVoltage(voltage).alongWith(runCurrentLimits(supplyCurrent, torqueCurrent)); - } - - public Command runTCcurrentLimits(DoubleSupplier torqueCurrent, DoubleSupplier supplyCurrent) { - return runTorqueCurrentFoc(torqueCurrent) - .alongWith(runCurrentLimits(supplyCurrent, torqueCurrent)); - } - - public Command stopMotor() { - return run(() -> stop()); - } - - public Command trackTargetCommand() { - return run(() -> { - var params = ShotCalculator.getInstance().getParameters(); - setVelocityTCFOCrpm(() -> params.flywheelSpeed()); - }) - .withName("Launcher.trackTargetCommand"); - } - - public Command onTheFlyLaunch() { - return run(() -> { - setVelocityTCFOCrpm(() -> config.getOnTheFlySpeed().get()); - }) - .withName("Launcher.onTheFlyLaunch"); - } - - public Trigger aimingAtTarget() { - return new Trigger( - () -> { - var params = ShotCalculator.getInstance().getParameters(); - - double targetRPM = params.flywheelSpeed(); - double currentRPM = getVelocityRPM(); - - double errorRPM = currentRPM - targetRPM; - - return Math.abs(errorRPM) < config.getOnTargetToleranceRPM(); - }); - } - // -------------------------------------------------------------------------------- // Simulation - // -------------------------------------------------------------------------------- + // // -------------------------------------------------------------------------------- public void simulationInit() { if (isAttached()) { sim = new LauncherSim(RobotSim.leftView, motor.getSimState()); @@ -192,8 +175,7 @@ class LauncherSim extends RollerSim { public LauncherSim(Mechanism2d mech, TalonFXSimState rollerMotorSim) { super( new RollerConfig(config.getWheelDiameter()) - .setPosition(config.getLauncherX(), config.getLauncherY()) - .setMount(Robot.getHood().getSim()), + .setPosition(config.getLauncherX(), config.getLauncherY()), mech, rollerMotorSim, config.getName()); diff --git a/src/main/java/frc/robot/subsystems/leds/Leds.java b/src/main/java/frc/robot/subsystems/leds/Leds.java new file mode 100644 index 00000000..62f88ad4 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/leds/Leds.java @@ -0,0 +1,62 @@ +package frc.robot.subsystems.leds; + +import com.ctre.phoenix6.CANBus; +import com.ctre.phoenix6.signals.LossOfSignalBehaviorValue; +import com.ctre.phoenix6.signals.StripTypeValue; +import frc.spectrumLib.hardware.Rio; +import frc.spectrumLib.leds.SpectrumLEDs; +import frc.spectrumLib.telemetry.Telemetry; + +/** + * Robot LED subsystem for the 2026 REBUILT season. + * + *

Extends {@link SpectrumLEDs} to inherit the full pattern library (solid, stripe, blink, + * breathe, rainbow, chase, bounce, gradient, ombre, wave, countdown, etc.) and the CANdle hardware + * abstraction. Robot-specific convenience command methods are defined below; bind them via triggers + * in {@code Robot.java} or {@code SuperStructure}. + * + *

Hardware: CANdle device ID 1 on the CANivore bus, 20-LED RGB external strip at brightness 0.5. + * LEDs are disabled on signal loss. + */ +public class Leds extends SpectrumLEDs { + + // ------------------------------------------------------------------------- + // Hardware configuration + // ------------------------------------------------------------------------- + + /** Number of external LEDs attached to the CANdle output (indices 8–27 on the device). */ + public static final int NUM_LEDS = 20; + + /** + * Static hardware config. Set {@code startIdx = 8} to address only the external strip (skipping + * the 8 onboard CANdle LEDs); keep at {@code 0} to address all 20 LEDs starting from the first + * onboard LED. + */ + public static final Config ledsConfig; + + static { + ledsConfig = new Config("Leds", 1, NUM_LEDS, new CANBus(Rio.CANIVORE)); + ledsConfig.setStripType(StripTypeValue.RGB); + ledsConfig.setBrightness(0.5); + ledsConfig.setLossOfSignalBehavior(LossOfSignalBehaviorValue.DisableLEDs); + } + + // ------------------------------------------------------------------------- + // Constructor + // ------------------------------------------------------------------------- + + public Leds() { + super(ledsConfig); + + setDefaultCommand(setPattern(breathe(purple, 2.0), -1).withName("Leds.idle")); + + Telemetry.print(getName() + " Subsystem Initialized"); + } + + @Override + public void periodic() { + Telemetry.log("Leds/CurrentCommand", getCurrentCommandName()); + Telemetry.log("Leds/CommandPriority", getCommandPriority()); + Telemetry.log("Leds/IsAnimating", isAnimating()); + } +} diff --git a/src/main/java/frc/robot/subsystems/spindexer/Spindexer.java b/src/main/java/frc/robot/subsystems/spindexer/Spindexer.java new file mode 100644 index 00000000..702ec3f8 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/spindexer/Spindexer.java @@ -0,0 +1,153 @@ +package frc.robot.subsystems.spindexer; + +import com.ctre.phoenix6.sim.TalonFXSimState; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.wpilibj.smartdashboard.Mechanism2d; +import frc.robot.RobotSim; +import frc.spectrumLib.hardware.Rio; +import frc.spectrumLib.mechanism.Mechanism; +import frc.spectrumLib.sim.RollerConfig; +import frc.spectrumLib.sim.RollerSim; +import frc.spectrumLib.telemetry.Telemetry; +import lombok.Getter; +import lombok.Setter; + +public class Spindexer extends Mechanism { + + public static class SpindexerConfig extends Config { + + @Getter @Setter private double supplyCurrentLimit = 40; + @Getter @Setter private double statorCurrentLimit = 80; + @Getter @Setter private double velocityKp = 5; + @Getter @Setter private double velocityKv = 10; + @Getter @Setter private double velocityKs = 15; + + /* Sim Configs */ + @Getter @Setter + private double spindexerX = Units.inchesToMeters(RobotSim.leftViewWidth / 2.0); + + @Getter @Setter + private double spindexerY = Units.inchesToMeters(RobotSim.leftViewHeight / 2.0); + + @Getter @Setter private double spindexerDiameter = 12; + + public SpindexerConfig() { + super("Spindexer", 8, Rio.CANIVORE); + configPIDGains(velocityKp, 0, 0); + configFeedForwardGains(velocityKs, velocityKv, 0, 0); + configSupplyCurrentLimit(supplyCurrentLimit, true); + configStatorCurrentLimit(statorCurrentLimit, true); + configClockwise_Positive(); + } + } + + // ---- State Machine ---- + + public enum WantedState { + OFF, + INDEX_MAX, + SLOW_INDEX, + UNJAM, + } + + public enum SystemState { + OFF, + INDEX_MAX, + SLOW_INDEX, + UNJAM, + } + + private WantedState wantedState = WantedState.OFF; + private SystemState systemState = SystemState.OFF; + + public void setWantedState(WantedState state) { + this.wantedState = state; + } + + private SystemState handleStateTransition() { + return switch (wantedState) { + case OFF -> SystemState.OFF; + case INDEX_MAX -> SystemState.INDEX_MAX; + case SLOW_INDEX -> SystemState.SLOW_INDEX; + case UNJAM -> SystemState.UNJAM; + }; + } + + // TODO: get actual values when robot is built + private void applyStates() { + double wantedRPM = 0; + switch (systemState) { + case OFF: + stop(); + return; + case INDEX_MAX: + wantedRPM = 3000; + break; + case SLOW_INDEX: + wantedRPM = 1000; + break; + case UNJAM: + wantedRPM = -2000; + break; + } + final double finalWantedRPM = wantedRPM; + setVelocityTCFOCrpm(() -> finalWantedRPM); + } + + @Getter private final SpindexerConfig config; + @Getter private SpindexerSim sim; + + public Spindexer(SpindexerConfig config) { + super(config); + this.config = config; + + simulationInit(); + Telemetry.print(getName() + " Subsystem Initialized"); + } + + @Override + public void periodic() { + systemState = handleStateTransition(); + applyStates(); + logBatteryUsage(); + Telemetry.log("Spindexer/WantedState", wantedState.toString()); + Telemetry.log("Spindexer/SystemState", systemState.toString()); + Telemetry.log("Spindexer/CurrentCommand", getCurrentCommandName()); + Telemetry.log("Spindexer/Voltage", getVoltage(), "volts"); + Telemetry.log("Spindexer/StatorCurrent", getStatorCurrent(), "amps"); + Telemetry.log("Spindexer/SupplyCurrent", getSupplyCurrent(), "amps"); + Telemetry.log("Spindexer/RPM", getVelocityRPM(), "RPM"); + Telemetry.log("Spindexer/Temp", getTemp(), "deg_C"); + } + + // -------------------------------------------------------------------------------- + // Simulation + // -------------------------------------------------------------------------------- + public void simulationInit() { + if (isAttached()) { + // Create a new RollerSim with the top view, the motor's sim state, and a 12 in + // diameter + sim = new SpindexerSim(RobotSim.leftView, motor.getSimState()); + } + } + + // Must be called to enable the simulation + // if roller position changes configure x and y to set position. + @Override + public void simulationPeriodic() { + if (isAttached()) { + sim.simulationPeriodic(); + } + } + + class SpindexerSim extends RollerSim { + public SpindexerSim(Mechanism2d mech, TalonFXSimState rollerMotorSim) { + super( + new RollerConfig(config.getSpindexerDiameter()) + .setPosition(config.getSpindexerX(), config.getSpindexerY()), + mech, + rollerMotorSim, + config.getName()); + } + } +} diff --git a/src/main/java/frc/robot/swerve/Swerve.java b/src/main/java/frc/robot/subsystems/swerve/Swerve.java similarity index 61% rename from src/main/java/frc/robot/swerve/Swerve.java rename to src/main/java/frc/robot/subsystems/swerve/Swerve.java index 9236d35c..a654416f 100644 --- a/src/main/java/frc/robot/swerve/Swerve.java +++ b/src/main/java/frc/robot/subsystems/swerve/Swerve.java @@ -1,6 +1,6 @@ // Based on // https://github.com/CrossTheRoadElec/Phoenix6-Examples/blob/main/java/SwerveWithPathPlanner/src/main/java/frc/robot/subsystems/CommandSwerveDrivetrain.java -package frc.robot.swerve; +package frc.robot.subsystems.swerve; import static edu.wpi.first.units.Units.Inches; import static edu.wpi.first.units.Units.Pounds; @@ -10,14 +10,17 @@ import com.ctre.phoenix6.hardware.CANcoder; import com.ctre.phoenix6.hardware.TalonFX; import com.ctre.phoenix6.swerve.SwerveDrivetrain; +import com.ctre.phoenix6.swerve.SwerveModule; import com.ctre.phoenix6.swerve.SwerveModule.DriveRequestType; import com.ctre.phoenix6.swerve.SwerveModule.SteerRequestType; import com.ctre.phoenix6.swerve.SwerveRequest; +import com.ctre.phoenix6.swerve.utility.PhoenixPIDController; import com.pathplanner.lib.auto.AutoBuilder; import com.pathplanner.lib.config.PIDConstants; import com.pathplanner.lib.config.RobotConfig; import com.pathplanner.lib.controllers.PPHolonomicDriveController; import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Pose3d; import edu.wpi.first.math.geometry.Rectangle2d; import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.math.geometry.Translation2d; @@ -30,46 +33,72 @@ import edu.wpi.first.wpilibj.Notifier; import edu.wpi.first.wpilibj.Timer; import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Subsystem; import edu.wpi.first.wpilibj2.command.button.Trigger; import frc.rebuilt.Field; import frc.rebuilt.FieldHelpers; +import frc.rebuilt.RobotBumpSim; +import frc.rebuilt.ShotCalculator; import frc.robot.Robot; -import frc.robot.swerve.controllers.RotationController; -import frc.robot.swerve.controllers.TranslationXController; -import frc.robot.swerve.controllers.TranslationYController; -import frc.spectrumLib.SpectrumSubsystem; -import frc.spectrumLib.Telemetry; import frc.spectrumLib.swerve.MapleSimSwerveDrivetrain; +import frc.spectrumLib.telemetry.Telemetry; import frc.spectrumLib.util.Util; import java.util.Arrays; import java.util.Optional; -import java.util.function.DoubleSupplier; import java.util.function.Supplier; import lombok.Getter; +import lombok.Setter; /** * Class that extends the Phoenix SwerveDrivetrain class and implements subsystem so it can be used * in command-based projects easily. */ -public class Swerve extends SwerveDrivetrain - implements SpectrumSubsystem { +public class Swerve extends SwerveDrivetrain implements Subsystem { + + // ── State machine ────────────────────────────────────────────────────────────────── + public enum WantedState { + TELEOP_DRIVE, + PILOT_AIM_AT_TARGET, + IDLE + } + + public enum SystemState { + TELEOP_DRIVE, + PILOT_AIM_AT_TARGET, + IDLE + } + + private WantedState wantedState = WantedState.IDLE; + private SystemState systemState = SystemState.IDLE; + + public static final double TRANSLATION_ERROR_MARGIN_METERS = Units.inchesToMeters(1.0); + public static final double DRIVE_TO_POINT_STATIC_FRICTION_CONSTANT = 0.02; + private static final double SKEW_COMPENSATION_SCALAR = -0.03; + + @Getter @Setter private double teleopVelocityCoefficient = 1.0; + @Getter @Setter private double rotationVelocityCoefficient = 1.0; + @Getter private SwerveConfig config; private Notifier simNotifier = null; - private RotationController rotationController; - private TranslationXController xController; - private TranslationYController yController; private Alert pigeonAlert = new Alert("Pigeon IMU Disconnected", Alert.AlertType.kError); - /* Keep track if we've ever applied the operator perspective before or not */ - private boolean hasAppliedPilotPerspective = false; - private final SwerveRequest.ApplyRobotSpeeds AutoRequest = new SwerveRequest.ApplyRobotSpeeds() .withDriveRequestType(DriveRequestType.Velocity) .withSteerRequestType(SteerRequestType.Position) .withDesaturateWheelSpeeds(true); + private static final SwerveRequest.ApplyFieldSpeeds FIELD_CENTRIC_DRIVE = + new SwerveRequest.ApplyFieldSpeeds() + .withDriveRequestType(DriveRequestType.Velocity) + .withSteerRequestType(SteerRequestType.Position); + + private final SwerveRequest.FieldCentricFacingAngle DRIVE_AT_ANGLE_REQUEST = + new SwerveRequest.FieldCentricFacingAngle() + .withDriveRequestType(SwerveModule.DriveRequestType.Velocity) + .withSteerRequestType(SwerveModule.SteerRequestType.Position); + /** * Constructs a new Swerve drive subsystem. * @@ -87,19 +116,31 @@ public Swerve(SwerveConfig config) { this.config = config; - rotationController = new RotationController(config); - xController = new TranslationXController(config); - yController = new TranslationYController(config); - if (Utils.isSimulation()) { startSimThread(); } configurePathPlanner(); - Robot.add(this); + // Configure heading PID on the shared drive-at-angle request + DRIVE_AT_ANGLE_REQUEST.HeadingController = + new PhoenixPIDController( + config.getKPRotationController(), + config.getKIRotationController(), + config.getKDRotationController()); + DRIVE_AT_ANGLE_REQUEST.HeadingController.enableContinuousInput(-Math.PI, Math.PI); + DRIVE_AT_ANGLE_REQUEST + .withDeadband( + config.getLinearSpeedAt12Volts().baseUnitMagnitude() + * config.getAimDeadband()) + .withRotationalDeadband( + config.getAngularSpeedAt12Volts().baseUnitMagnitude() + * config.getAimDeadband()) + .withMaxAbsRotationalRate(config.getAngularSpeedAt12Volts()); + this.register(); + optimizeBusUtilization(); registerTelemetry(this::log); Telemetry.print(getName() + " Subsystem Initialized"); @@ -110,10 +151,10 @@ public Swerve(SwerveConfig config) { // -------------------------------------------------------------------------------- protected void log(SwerveDriveState state) { - Telemetry.log("Swerve/Pose", state.Pose); - Telemetry.log("Swerve/TargetStates", state.ModuleTargets); - Telemetry.log("Swerve/MeasuredStates", state.ModuleStates); - Telemetry.log("Swerve/MeasuredSpeeds", state.Speeds); + Telemetry.log("Swerve/State/Pose", state.Pose); + Telemetry.log("Swerve/State/TargetStates", state.ModuleTargets); + Telemetry.log("Swerve/State/MeasuredStates", state.ModuleStates); + Telemetry.log("Swerve/State/MeasuredSpeeds", state.Speeds); } protected void logBatteryUsage() { @@ -122,10 +163,10 @@ protected void logBatteryUsage() { Robot.getBatteryLogger().reportCurrentUsage("Mechanisms/SwerveSteer", steerMotorCurrent); Robot.getBatteryLogger().reportCurrentUsage("Mechanisms/SwerveDrive", driveMotorCurrent); - Telemetry.log("Swerve/DriveStatorCurrent", getDriveMotorStatorCurrents()); - Telemetry.log("Swerve/SteerStatorCurrent", getSteerMotorStatorCurrents()); - Telemetry.log("Swerve/DriveSupplyCurrent", getDriveMotorSupplyCurrents()); - Telemetry.log("Swerve/SteerSupplyCurrent", getSteerMotorSupplyCurrents()); + Telemetry.log("Swerve/Currents/DriveStatorCurrent", getDriveMotorStatorCurrents()); + Telemetry.log("Swerve/Currents/SteerStatorCurrent", getSteerMotorStatorCurrents()); + Telemetry.log("Swerve/Currents/DriveSupplyCurrent", getDriveMotorSupplyCurrents()); + Telemetry.log("Swerve/Currents/SteerSupplyCurrent", getSteerMotorSupplyCurrents()); } protected double getDriveMotorStatorCurrents() { @@ -158,22 +199,44 @@ protected double getSteerMotorSupplyCurrents() { */ @Override public void periodic() { + systemState = handleStateTransition(); + applyStates(); + Telemetry.log("Swerve/CurrentCommand", getCurrentCommandName()); + Telemetry.log("Swerve/TeleopVelocityCoefficient", getTeleopVelocityCoefficient()); + Telemetry.log("Swerve/RotationVelocityCoefficient", getRotationVelocityCoefficient()); logBatteryUsage(); - setPilotPerspective(); checkPigeonConnection(); - } - @Override - public void setupStates() { - SwerveStates.setStates(); - } + if (Utils.isSimulation()) { + Telemetry.log("Sim/SimPose", getRobotPose()); + if (robotBumpSim != null) { + Pose2d simPose = + mapleSimSwerveDrivetrain.mapleSimDrive.getSimulatedDriveTrainPose(); + ChassisSpeeds robotRelSpeeds = + mapleSimSwerveDrivetrain.mapleSimDrive + .getDriveTrainSimulatedChassisSpeedsRobotRelative(); + ChassisSpeeds fieldRelSpeeds = + ChassisSpeeds.fromRobotRelativeSpeeds( + robotRelSpeeds, simPose.getRotation()); + // subticks=5 -> dt = 20ms/5 = 4ms sub-steps (matches MapleSim's 5ms period closely) + simRobotPose3d = robotBumpSim.update(simPose, fieldRelSpeeds, 5); + if (robotBumpSim.isOnRamp()) { + mapleSimSwerveDrivetrain.mapleSimDrive.setSimulationWorldPose( + robotBumpSim.getSimWorldPose(simPose)); + } + Telemetry.log("Sim/RobotPose3d", simRobotPose3d); + } + } - @Override - public void setupDefaultCommand() { - SwerveStates.setupDefaultCommand(); + Telemetry.log("Swerve/WantedState", wantedState.toString()); + Telemetry.log("Swerve/SystemState", systemState.toString()); } + // ----------------------------------------------------------------------- + // Subsystem Setup + // ----------------------------------------------------------------------- + protected String getCurrentCommandName() { Command currentCommand = this.getCurrentCommand(); if (currentCommand != null) { @@ -183,6 +246,71 @@ protected String getCurrentCommandName() { return "none"; } + private SystemState handleStateTransition() { + return switch (wantedState) { + case TELEOP_DRIVE -> SystemState.TELEOP_DRIVE; + case PILOT_AIM_AT_TARGET -> SystemState.PILOT_AIM_AT_TARGET; + case IDLE -> SystemState.IDLE; + default -> SystemState.IDLE; + }; + } + + private void applyStates() { + switch (systemState) { + default: + case IDLE: + break; + case PILOT_AIM_AT_TARGET: + var params = ShotCalculator.getInstance().getParameters(); + ChassisSpeeds joystickSpeeds = calculateSpeedsBasedOnJoystickInputs(); + setControl( + DRIVE_AT_ANGLE_REQUEST + .withVelocityX(joystickSpeeds.vxMetersPerSecond) + .withVelocityY(joystickSpeeds.vyMetersPerSecond) + .withTargetDirection(params.turretAngle())); + + break; + case TELEOP_DRIVE: + setControl(FIELD_CENTRIC_DRIVE.withSpeeds(calculateSpeedsBasedOnJoystickInputs())); + break; + } + } + + private ChassisSpeeds calculateSpeedsBasedOnJoystickInputs() { + if (DriverStation.getAlliance().isEmpty()) { + return new ChassisSpeeds(0, 0, 0); + } + + double xMagnitude = Robot.getPilot().getDriveFwdPositive(); + double yMagnitude = Robot.getPilot().getDriveLeftPositive(); + double angularMagnitude = Robot.getPilot().getDriveCCWPositive(); + + double xVelocity = + (DriverStation.getAlliance().orElse(DriverStation.Alliance.Blue) + == DriverStation.Alliance.Blue + ? xMagnitude + : -xMagnitude) + * teleopVelocityCoefficient; + double yVelocity = + (DriverStation.getAlliance().orElse(DriverStation.Alliance.Blue) + == DriverStation.Alliance.Blue + ? yMagnitude + : -yMagnitude) + * teleopVelocityCoefficient; + double angularVelocity = angularMagnitude * rotationVelocityCoefficient; + + Rotation2d skewCompensationFactor = + Rotation2d.fromRadians( + getCurrentRobotChassisSpeeds().omegaRadiansPerSecond + * SKEW_COMPENSATION_SCALAR); + + return ChassisSpeeds.fromRobotRelativeSpeeds( + ChassisSpeeds.fromFieldRelativeSpeeds( + new ChassisSpeeds(xVelocity, yVelocity, angularVelocity), + getRobotPose().getRotation()), + getRobotPose().getRotation().plus(skewCompensationFactor)); + } + // -------------------------------------------------------------------------------- // Pose Methods // -------------------------------------------------------------------------------- @@ -282,56 +410,47 @@ public Trigger inYzoneAlliance(double minYmeter, double maxYmeter) { maxYmeter)); } - public Trigger inNeutralZone() { - final double fieldLengthMeters = Units.feetToMeters(54.0); - final double fieldWidthMeters = Units.feetToMeters(27.0); + private static final double FIELD_LENGTH_METERS = Units.feetToMeters(54.0); + private static final double FIELD_WIDTH_METERS = Units.feetToMeters(27.0); + private static final double NEUTRAL_DEPTH_METERS = Units.inchesToMeters(283.0); + private static final double NEUTRAL_LENGTH_METERS = Units.inchesToMeters(317.7); + private static final double ENEMY_ALLIANCE_DEPTH_METERS = Units.inchesToMeters(180.0); - final double neutralDepthMeters = Units.inchesToMeters(283.0); - final double neutralLengthMeters = Units.inchesToMeters(317.7); + private static final Rectangle2d NEUTRAL_ZONE = + new Rectangle2d( + new Translation2d( + FIELD_LENGTH_METERS / 2.0 - NEUTRAL_DEPTH_METERS / 2.0, + FIELD_WIDTH_METERS / 2.0 - NEUTRAL_LENGTH_METERS / 2.0), + new Translation2d( + FIELD_LENGTH_METERS / 2.0 + NEUTRAL_DEPTH_METERS / 2.0, + FIELD_WIDTH_METERS / 2.0 + NEUTRAL_LENGTH_METERS / 2.0)); - final double centerX = fieldLengthMeters / 2.0; - final double centerY = fieldWidthMeters / 2.0; + private static final Rectangle2d ENEMY_ALLIANCE_ZONE = + new Rectangle2d( + new Translation2d(FIELD_LENGTH_METERS - ENEMY_ALLIANCE_DEPTH_METERS, 0), + new Translation2d(FIELD_LENGTH_METERS, FIELD_WIDTH_METERS)); - Rectangle2d neutralZone = - new Rectangle2d( - new Translation2d( - (centerX - neutralDepthMeters / 2.0), - centerY - neutralLengthMeters / 2.0), - new Translation2d( - (centerX + neutralDepthMeters / 2.0), - centerY + neutralLengthMeters / 2.0)); + /** Returns {@code true} when the robot is inside the neutral zone. Allocation-free. */ + public boolean isInNeutralZone() { + return NEUTRAL_ZONE.contains(getRobotPose().getTranslation()); + } - return new Trigger( - () -> { - double x = getRobotPose().getX(); - double y = getRobotPose().getY(); + /** + * Returns {@code true} when the robot is inside the opposing alliance's zone (pose X is flipped + * for red so the same rectangle works for both alliances). + */ + public boolean isInEnemyAllianceZone() { + return ENEMY_ALLIANCE_ZONE.contains( + new Translation2d( + FieldHelpers.flipXifRed(getRobotPose().getX()), getRobotPose().getY())); + } - return neutralZone.contains(new Translation2d(x, y)); - }); + public Trigger inNeutralZone() { + return new Trigger(this::isInNeutralZone); } public Trigger inEnemyAllianceZone() { - final double fieldLengthMeters = Units.feetToMeters(54.0); - final double fieldWidthMeters = Units.feetToMeters(27.0); - - final double allianceDepthMeters = Units.inchesToMeters(180.0); // X depth - final double allianceSpanMeters = fieldWidthMeters; // Y span - - final double minX = fieldLengthMeters - allianceDepthMeters; - final double minY = 0; - - Rectangle2d enemyAllianceZone = - new Rectangle2d( - new Translation2d(minX, minY), - new Translation2d(fieldLengthMeters, allianceSpanMeters)); - - return new Trigger( - () -> { - double x = FieldHelpers.flipXifRed(getRobotPose().getX()); - double y = getRobotPose().getY(); - - return enemyAllianceZone.contains(new Translation2d(x, y)); - }); + return new Trigger(this::isInEnemyAllianceZone); } public Trigger inFieldRight() { @@ -366,29 +485,6 @@ public ChassisSpeeds getCurrentRobotChassisSpeeds() { return getKinematics().toChassisSpeeds(getState().ModuleStates); } - // -------------------------------------------------------------------------------- - // Pilot Perspective - // -------------------------------------------------------------------------------- - - private void setPilotPerspective() { - /* Periodically try to apply the operator perspective */ - /* If we haven't applied the operator perspective before, then we should apply it regardless of DS state */ - /* This allows us to correct the perspective in case the robot code restarts mid-match */ - /* Otherwise, only check and apply the operator perspective if the DS is disabled */ - /* This ensures driving behavior doesn't change until an explicit disable event occurs during testing*/ - if (!hasAppliedPilotPerspective || DriverStation.isDisabled()) { - DriverStation.getAlliance() - .ifPresent( - allianceColor -> { - this.setOperatorPerspectiveForward( - allianceColor == Alliance.Red - ? config.getRedAlliancePerspectiveRotation() - : config.getBlueAlliancePerspectiveRotation()); - hasAppliedPilotPerspective = true; - }); - } - } - // -------------------------------------------------------------------------------- // Reorientation Methods // -------------------------------------------------------------------------------- @@ -422,49 +518,6 @@ protected double getClosestCardinal() { } } - protected double getClosest45() { - double angleRadians = getRotation().getRadians(); - double angleDegrees = Math.toDegrees(angleRadians); - - // Normalize the angle to be within 0 to 360 degrees - angleDegrees = angleDegrees % 360; - if (angleDegrees < 0) { - angleDegrees += 360; - } - - // Round to the nearest multiple of 45 degrees - double closest45Degrees = Math.round(angleDegrees / 45.0) * 45.0; - - // Convert back to radians and return as a Rotation2d - return Rotation2d.fromDegrees(closest45Degrees).getRadians(); - } - - protected double getClosestFieldAngle() { - // Step 1: Read the angle in radians - double angleRadians = getRotation().getRadians(); - - // Step 2: Convert the angle from radians to degrees - double angleDegrees = Math.toDegrees(angleRadians); - - // Step 3: Define a table of angles in degrees - double[] angleTable = {0, 180, 126, -126, 54, -54, 60, -60, 120, -120, 90, -90}; - - // Step 4: Find the nearest angle from the table - double closestAngle = angleTable[0]; - double minDifference = getRotationDifference(angleDegrees, closestAngle); - - for (double angle : angleTable) { - double difference = getRotationDifference(angleDegrees, angle); - if (difference < minDifference) { - minDifference = difference; - closestAngle = angle; - } - } - - // Step 5: Return the nearest angle in Radians - return Math.toRadians(closestAngle); - } - protected Command cardinalReorient() { return runOnce( () -> { @@ -497,14 +550,6 @@ public double getRotationDifference(double angle1, double angle2) { // Rotation Controller // -------------------------------------------------------------------------------- - double getRotationControl(double goalRadians) { - return rotationController.calculate(goalRadians, getRotationRadians()); - } - - void resetRotationController() { - rotationController.reset(getRotationRadians()); - } - Rotation2d getRotation() { return getRobotPose().getRotation(); } @@ -513,43 +558,29 @@ Rotation2d getRotation() { return getRobotPose().getRotation().getRadians(); } - double calculateRotationController(DoubleSupplier targetRadians, boolean useHold) { - return rotationController.calculate( - targetRadians.getAsDouble(), getRotationRadians(), useHold); - } - // -------------------------------------------------------------------------------- - // Translation X Controller + // Request Methods // -------------------------------------------------------------------------------- - void resetXController() { - xController.reset(getRobotPose().getX()); - } - - DoubleSupplier calculateXController(DoubleSupplier targetMeters) { - return () -> xController.calculate(targetMeters.getAsDouble(), getRobotPose().getX()); + // Used to set a control request to the swerve module, ignores disable so commands are + // continuous. + Command applyRequest(Supplier requestSupplier) { + return run(() -> this.setControl(requestSupplier.get())).ignoringDisable(true); } - // -------------------------------------------------------------------------------- - // Translation Y Controller - // -------------------------------------------------------------------------------- + // ── Public state setters ─────────────────────────────────────────────────────────── - void resetYController() { - yController.reset(getRobotPose().getY()); + public void setWantedState(WantedState state) { + this.wantedState = state; } - DoubleSupplier calculateYController(DoubleSupplier targetMeters) { - return () -> yController.calculate(targetMeters.getAsDouble(), getRobotPose().getY()); + public boolean isAtDesiredRotation() { + return isAtDesiredRotation(Units.degreesToRadians(10.0)); } - // -------------------------------------------------------------------------------- - // Request Methods - // -------------------------------------------------------------------------------- - - // Used to set a control request to the swerve module, ignores disable so commands are - // continuous. - Command applyRequest(Supplier requestSupplier) { - return run(() -> this.setControl(requestSupplier.get())).ignoringDisable(true); + public boolean isAtDesiredRotation(double toleranceRadians) { + return Math.abs(DRIVE_AT_ANGLE_REQUEST.HeadingController.getPositionError()) + < toleranceRadians; } // -------------------------------------------------------------------------------- @@ -557,8 +588,12 @@ Command applyRequest(Supplier requestSupplier) { // -------------------------------------------------------------------------------- private void configurePathPlanner() { - // Seed robot to mid field at start (Paths will change this starting position) - resetPose(Field.getCenterField()); + // Seed robot to in front of blue hub (Paths will change this starting position) + resetPose( + new Pose2d( + Field.getBlueHubCenter().getX() - 2, + Field.getBlueHubCenter().getY(), + Rotation2d.fromDegrees(0))); try { var config = RobotConfig.fromGUISettings(); @@ -598,6 +633,8 @@ private void configurePathPlanner() { // -------------------------------------------------------------------------------- @Getter private MapleSimSwerveDrivetrain mapleSimSwerveDrivetrain = null; + @Getter private RobotBumpSim robotBumpSim = null; + @Getter private Pose3d simRobotPose3d = Pose3d.kZero; @SuppressWarnings("unchecked") private void startSimThread() { @@ -617,6 +654,8 @@ private void startSimThread() { config.getFrontRight(), config.getBackLeft(), config.getBackRight()); + robotBumpSim = new RobotBumpSim(getModuleLocations()); + /* Run simulation at a faster rate so PID gains behave more reasonably */ simNotifier = new Notifier(mapleSimSwerveDrivetrain::update); simNotifier.startPeriodic(config.getSimLoopPeriod()); diff --git a/src/main/java/frc/robot/swerve/SwerveConfig.java b/src/main/java/frc/robot/subsystems/swerve/SwerveConfig.java similarity index 84% rename from src/main/java/frc/robot/swerve/SwerveConfig.java rename to src/main/java/frc/robot/subsystems/swerve/SwerveConfig.java index e8ea7a21..fafb1405 100644 --- a/src/main/java/frc/robot/swerve/SwerveConfig.java +++ b/src/main/java/frc/robot/subsystems/swerve/SwerveConfig.java @@ -1,6 +1,11 @@ -package frc.robot.swerve; +package frc.robot.subsystems.swerve; -import static edu.wpi.first.units.Units.*; +import static edu.wpi.first.units.Units.Amps; +import static edu.wpi.first.units.Units.DegreesPerSecond; +import static edu.wpi.first.units.Units.Inches; +import static edu.wpi.first.units.Units.MetersPerSecond; +import static edu.wpi.first.units.Units.Rotations; +import static edu.wpi.first.units.Units.Volts; import com.ctre.phoenix6.CANBus; import com.ctre.phoenix6.configs.CANcoderConfiguration; @@ -15,72 +20,42 @@ import com.ctre.phoenix6.swerve.SwerveModuleConstants.SteerFeedbackType; import com.ctre.phoenix6.swerve.SwerveModuleConstantsFactory; import edu.wpi.first.math.geometry.Rotation2d; -import edu.wpi.first.math.trajectory.TrapezoidProfile.Constraints; import edu.wpi.first.math.util.Units; -import edu.wpi.first.units.measure.*; -import frc.spectrumLib.Rio; +import edu.wpi.first.units.measure.Angle; +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.units.measure.Current; +import edu.wpi.first.units.measure.Distance; +import edu.wpi.first.units.measure.LinearVelocity; +import edu.wpi.first.units.measure.Voltage; +import frc.spectrumLib.hardware.Rio; import lombok.Getter; import lombok.Setter; public class SwerveConfig { @Getter private final double simLoopPeriod = 0.005; // 5 ms - @Getter @Setter private double robotWidth = Units.inchesToMeters(25); - @Getter @Setter private double robotLength = Units.inchesToMeters(29); - @Getter @Setter private double maxAngularRate = 3 * Math.PI; // rad/s @Getter @Setter private double deadband = 0.05; // 5% input deadband for the joysticks @Getter @Setter private double aimDeadband = 0.01; // 1% input deadband for aiming modes @Getter @Setter private double driveGearRatio = 6.03; @Getter @Setter private double steerGearRatio = 26.09; - @Getter @Setter // Estimated at first, then fudge-factored to make odom match record - private Distance wheelRadius = Inches.of(1.964); // 0.0499 m + @Getter @Setter private Distance wheelRadius = Inches.of(1.964); // 0.0499 m - // Theoretical free speed (ft/s) at 12v applied output; - @Getter @Setter private LinearVelocity speedAt12Volts = FeetPerSecond.of(16.8); + // Theoretical translational free speed (ft/s) at 12v applied output; + @Getter @Setter private LinearVelocity linearSpeedAt12Volts = MetersPerSecond.of(5.12); - @Getter private double kSdrive = 0.10; // 0.13 - @Getter private double kSsteer = 0.25; // 0.2 + // Theoretical rotational free speed (ft/s) at 12v applied output; + @Getter @Setter private AngularVelocity angularSpeedAt12Volts = DegreesPerSecond.of(540.00); // ----------------------------------------------------------------------- // PID Controller Constants // ----------------------------------------------------------------------- - @Getter private double maxAngularVelocity = 3.0 * Math.PI; // rad/s - @Getter private double maxAngularAcceleration = 3.0 * Math.PI; // rad/s^2 - @Getter private double kPRotationController = 5.0; @Getter private double kIRotationController = 0.0; @Getter private double kDRotationController = 0.0; - @Getter private double rotationTolerance = Units.degreesToRadians(1); // rads - @Getter private double rotationVelocityTolerance = Units.degreesToRadians(3); // rads/s - - @Getter private double kPHoldController = 0.15; - @Getter private double kIHoldController = 0.0; - @Getter private double kDHoldController = 0.0; - - @Getter private double kPTranslationController = 3.0; - @Getter private double kITranslationController = 0.0; - @Getter private double kDTranslationController = 0.0; - - @Getter private double translationTolerance = Units.inchesToMeters(0.6); // 0.4 - @Getter private double translationVelocityTolerance = Units.inchesToMeters(0.8); // 0.75 - - @Getter - private Constraints translationConstraints = - new Constraints(speedAt12Volts.baseUnitMagnitude(), 10); - - @Getter private double kPTagCenterController = 1.3; - @Getter private double kITagCenterController = 0.0; - @Getter private double kDTagCenterController = 0.00; - @Getter private double tagCenterTolerance = Units.inchesToMeters(0.5); // meters - - @Getter private double kPTagDistanceController = 0.1; // 0.15; - @Getter private double kITagDistanceController = 0.0; - @Getter private double kDTagDistanceController = 0.00; - @Getter private double tagDistanceTolerance = 0.3; // Area /* Blue alliance sees forward as 0 degrees (toward red alliance wall) */ @Getter private final Rotation2d blueAlliancePerspectiveRotation = Rotation2d.kZero; @@ -115,7 +90,7 @@ public class SwerveConfig { // The stator current at which the wheels start to slip; // This needs to be tuned to your individual robot - @Getter @Setter private Current slipCurrent = Amps.of(90); + @Getter @Setter private Current slipCurrent = Amps.of(80); // Initial configs for the drive and steer motors and the CANcoder; these cannot be null. // Some configs will be overwritten; check the `with*InitialConfigs()` API documentation. @@ -124,8 +99,11 @@ public class SwerveConfig { new TalonFXConfiguration() .withCurrentLimits( new CurrentLimitsConfigs() - .withSupplyCurrentLimit(Amps.of(65)) - .withSupplyCurrentLimitEnable(true)); + .withStatorCurrentLimit(Amps.of(80.0)) + .withStatorCurrentLimitEnable(true) + .withSupplyCurrentLimit(Amps.of(40.0)) + .withSupplyCurrentLimitEnable(true) + .withSupplyCurrentLowerLimit(Amps.of(40.0))); // Swerve azimuth does not require much torque output, so we can set a // relatively low stator current limit to help avoid @@ -145,7 +123,7 @@ public class SwerveConfig { // Every 1 rotation of the azimuth results in kCoupleRatio drive motor turns; // This may need to be tuned to your individual robot - @Getter private double coupleRatio = 3.375; + @Getter private double coupleRatio = 4.5; @Getter @Setter private boolean steerMotorReversed = false; @Getter @Setter private boolean invertLeftSide = false; @@ -274,7 +252,7 @@ public SwerveConfig updateConfig() { .withDriveMotorGains(driveGains) .withSteerMotorClosedLoopOutput(steerClosedLoopOutput) .withDriveMotorClosedLoopOutput(driveClosedLoopOutput) - .withSpeedAt12Volts(speedAt12Volts) + .withSpeedAt12Volts(linearSpeedAt12Volts) .withSteerInertia(steerInertia) .withDriveInertia(driveInertia) .withSteerFrictionVoltage(steerFrictionVoltage) diff --git a/src/main/java/frc/robot/subsystems/turret/Turret.java b/src/main/java/frc/robot/subsystems/turret/Turret.java new file mode 100644 index 00000000..bb5392a0 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/turret/Turret.java @@ -0,0 +1,248 @@ +package frc.robot.subsystems.turret; + +import com.ctre.phoenix6.configs.TalonFXConfiguration; +import com.ctre.phoenix6.configs.TalonFXConfigurator; +import com.ctre.phoenix6.controls.PositionTorqueCurrentFOC; +import com.ctre.phoenix6.hardware.TalonFX; +import com.ctre.phoenix6.sim.CANcoderSimState; +import com.ctre.phoenix6.sim.TalonFXSimState; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.trajectory.TrapezoidProfile; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.wpilibj.smartdashboard.Mechanism2d; +import frc.rebuilt.ShotCalculator; +import frc.robot.Robot; +import frc.robot.RobotSim; +import frc.spectrumLib.hardware.Rio; +import frc.spectrumLib.hardware.SpectrumCANcoder; +import frc.spectrumLib.hardware.SpectrumCANcoderConfig; +import frc.spectrumLib.mechanism.Mechanism; +import frc.spectrumLib.sim.ArmConfig; +import frc.spectrumLib.sim.ArmSim; +import frc.spectrumLib.telemetry.*; +import lombok.*; + +public class Turret extends Mechanism { + + public static class TurretConfig extends Config { + @Getter @Setter private boolean reversed = false; + + @Getter private final double initPosition = 0; + @Getter private final double presetPosition = 90; + @Getter private double triggerTolerance = 5; + @Getter private double unwrapTolerance = 10; + + @Getter private Rotation2d zeroOffsetFromRobotFront = Rotation2d.fromDegrees(180); + + /* Turret config settings */ + @Getter private final double zeroSpeed = -0.1; + @Getter private final double holdMaxSpeedRPM = 18; + + @Getter private final double currentLimit = 30; + @Getter private final double torqueCurrentLimit = 60; + @Getter private final double positionKp = 700; + @Getter private final double positionKd = 25; + @Getter private final double positionKv = 0; + @Getter private final double positionKs = 2; + @Getter private final double positionKa = 0; + @Getter private final double positionKg = 0; + @Getter private final double mmCruiseVelocity = 50; + @Getter private final double mmAcceleration = 300; + @Getter private final double mmJerk = 1000; + + // Trapezoidal profile constraints are in mechanism rotations and mechanism + // rotations per second + private final TrapezoidProfile.Constraints turretConstraints; + + @Getter @Setter private double sensorToMechanismRatio = 22.4; + @Getter @Setter private double rotorToSensorRatio = 1; + + /* Cancoder config settings */ + @Getter @Setter private double CANcoderRotorToSensorRatio = 5; + // CANcoderRotorToSensorRatio / sensorToMechanismRatio; + + @Getter @Setter private double CANcoderSensorToMechanismRatio = 9; + + @Getter @Setter private double CANcoderOffset = -0.196533203125; + @Getter @Setter private boolean CANcoderAttached = true; + @Getter @Setter private boolean isCANcoderInverted = false; + + /* Sim Configs */ + @Getter private double intakeX = Units.inchesToMeters(105); // Vertical Center + @Getter private double intakeY = Units.inchesToMeters(75); // Horizontal Center + @Getter private double simRatio = sensorToMechanismRatio; + @Getter private double length = 1; + + public TurretConfig() { + super("Turret", 44, Rio.CANIVORE); // Rio.CANIVORE); + configPIDGains(0, positionKp, 0, positionKd); + configFeedForwardGains(positionKs, positionKv, positionKa, positionKg); + configMotionMagic(mmCruiseVelocity, mmAcceleration, mmJerk); + configGearRatio(sensorToMechanismRatio); + configSupplyCurrentLimit(currentLimit, true); + configStatorCurrentLimit(torqueCurrentLimit, true); + configForwardTorqueCurrentLimit(torqueCurrentLimit); + configReverseTorqueCurrentLimit(torqueCurrentLimit); + configMinMaxRotations(-0.54, 0.50); + configReverseSoftLimit(getMinRotations(), true); + configForwardSoftLimit(getMaxRotations(), true); + configNeutralBrakeMode(true); + configContinuousWrap(false); + configGravityType(false); + configCounterClockwise_Positive(); + + turretConstraints = new TrapezoidProfile.Constraints(30, 40); + } + + public TurretConfig modifyMotorConfig(TalonFX motor) { + TalonFXConfigurator configurator = motor.getConfigurator(); + TalonFXConfiguration talonConfigMod = getTalonConfig(); + + configurator.apply(talonConfigMod); + talonConfig = talonConfigMod; + return this; + } + } + + public enum WantedState { + OFF, + HOME, + AIM_AT_TARGET, + } + + public enum SystemState { + OFF, + HOME, + AIM_AT_TARGET, + } + + private WantedState wantedState = WantedState.OFF; + private SystemState systemState = SystemState.OFF; + + public void setWantedState(WantedState state) { + this.wantedState = state; + } + + private SystemState handleStateTransition() { + return switch (wantedState) { + case OFF -> SystemState.OFF; + case HOME -> SystemState.HOME; + case AIM_AT_TARGET -> SystemState.AIM_AT_TARGET; + }; + } + + private void applyStates() { + double wantedDegrees = 0; + switch (systemState) { + case OFF: + stop(); + return; + case HOME: + wantedDegrees = 0; + break; + case AIM_AT_TARGET: + var params = ShotCalculator.getInstance().getParameters(); + wantedDegrees = + params.turretAngle().getDegrees() + + ShotCalculator.TURRET_ANGLE_OFFSET_DEGREES; + break; + } + final double finalWantedDegrees = wantedDegrees; + final double finalWantedPosition = degreesToRotations(() -> finalWantedDegrees); + setMMPositionFoc(() -> finalWantedPosition); + } + + @SuppressWarnings("unused") + private TrapezoidProfile profile; + + @SuppressWarnings("unused") + private TrapezoidProfile.State turretSetpoint; + + @SuppressWarnings("unused") + private PositionTorqueCurrentFOC turretRequest = new PositionTorqueCurrentFOC(0); + + @Getter private TurretConfig config; + @Getter private TurretSim sim; + + @SuppressWarnings("unused") + private SpectrumCANcoder canCoder; + + private SpectrumCANcoderConfig canCoderConfig; + CANcoderSimState canCoderSim; + + public Turret(TurretConfig config) { + super(config); + this.config = config; + + if (isAttached()) { + if (config.isCANcoderAttached() && !Robot.isSimulation()) { + canCoderConfig = + new SpectrumCANcoderConfig( + config.getCANcoderRotorToSensorRatio(), + config.getCANcoderSensorToMechanismRatio(), + config.getCANcoderOffset(), + config.isCANcoderAttached(), + config.isCANcoderInverted()); + canCoder = + new SpectrumCANcoder( + 44, + canCoderConfig, + motor, + config, + SpectrumCANcoder.CANCoderFeedbackType.FusedCANcoder); + } + } + + profile = new TrapezoidProfile(config.turretConstraints); + turretSetpoint = new TrapezoidProfile.State(); + + simulationInit(); + Telemetry.print(getName() + " Subsystem Initialized"); + } + + @Override + public void periodic() { + systemState = handleStateTransition(); + logBatteryUsage(); + applyStates(); + Telemetry.log("Turret/CurrentCommand", getCurrentCommandName()); + Telemetry.log("Turret/Voltage", getVoltage()); + Telemetry.log("Turret/Current", getStatorCurrent()); + Telemetry.log("Turret/PositionDegrees", getPositionDegrees()); + Telemetry.log("Turret/PositionRotations", getPositionRotations()); + Telemetry.log("Turret/VelocityRPM", getVelocityRPM()); + } + + // -------------------------------------------------------------------------------- + // Simulation + // -------------------------------------------------------------------------------- + private void simulationInit() { + if (isAttached()) { + sim = new TurretSim(RobotSim.topView, motor.getSimState()); + } + } + + @Override + public void simulationPeriodic() { + if (isAttached()) { + sim.simulationPeriodic(); + } + } + + class TurretSim extends ArmSim { + public TurretSim(Mechanism2d mech, TalonFXSimState turretMotorSim) { + super( + new ArmConfig( + config.intakeX, + config.intakeY, + config.simRatio, + config.length, + -720, + 720, + 0), + mech, + turretMotorSim, + config.getName()); + } + } +} diff --git a/src/main/java/frc/robot/subsystems/vision/Vision.java b/src/main/java/frc/robot/subsystems/vision/Vision.java new file mode 100644 index 00000000..2b3dde1b --- /dev/null +++ b/src/main/java/frc/robot/subsystems/vision/Vision.java @@ -0,0 +1,911 @@ +package frc.robot.subsystems.vision; + +import com.ctre.phoenix6.Utils; +import edu.wpi.first.apriltag.AprilTagFieldLayout; +import edu.wpi.first.apriltag.AprilTagFields; +import edu.wpi.first.math.Matrix; +import edu.wpi.first.math.VecBuilder; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.kinematics.ChassisSpeeds; +import edu.wpi.first.math.numbers.N1; +import edu.wpi.first.math.numbers.N3; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Subsystem; +import frc.rebuilt.FieldHelpers; +import frc.robot.Robot; +import frc.robot.auton.Auton; +import frc.spectrumLib.telemetry.Telemetry; +import frc.spectrumLib.telemetry.Telemetry.PrintPriority; +import frc.spectrumLib.util.Util; +import frc.spectrumLib.vision.Limelight; +import frc.spectrumLib.vision.Limelight.LimelightConfig; +import frc.spectrumLib.vision.LimelightHelpers; +import frc.spectrumLib.vision.LimelightHelpers.RawFiducial; +import frc.spectrumLib.vision.VisionLogger; +import java.util.Arrays; +import java.util.IdentityHashMap; +import lombok.Getter; + +/** + * Vision subsystem that manages three Limelights (back, left, right) and fuses their pose estimates + * into the swerve odometry via WPILib's {@code SwerveDrivePoseEstimator}. + * + *

Each robot loop iteration the subsystem: + * + *

    + *
  1. Pushes the robot's current heading to all Limelights so MegaTag2 IMU fusion remains + * accurate. + *
  2. Selects the Limelight with the best view ({@link #getBestLimelight()}). + *
  3. Runs the MT1 (and, while disabled, MT2) rejection pipeline and adds accepted estimates to + * the pose estimator with appropriate std-dev vectors. + *
  4. Logs per-camera status and pose data via {@link VisionLogger}. + *
+ */ +public class Vision implements Subsystem { + + // ========================================================================= + // Configuration + // ========================================================================= + + /** + * Static configuration for the Vision subsystem, including Limelight identifiers, + * camera-to-robot transforms, pipeline indices, and pose estimation covariance parameters. + */ + public static class VisionConfig { + + @Getter final String name = "Vision"; + + // -- Back Limelight --------------------------------------------------- + + /** NetworkTables hostname for the rear-facing Limelight. */ + @Getter final String backLL = "limelight-back"; + + /** + * Robot-relative pose of the rear Limelight. Translation in metres (x, y, z); rotation in + * degrees (roll, pitch, yaw). + */ + @Getter + final LimelightConfig backConfig = + new LimelightConfig(backLL) + .withTranslation(-0.3084987734, 0.2134100126, 0.6502249886) + .withRotation(0, 0, 180); + + // -- Left Limelight --------------------------------------------------- + + /** NetworkTables hostname for the left-facing Limelight. */ + @Getter final String leftLL = "limelight-left"; + + /** + * Robot-relative pose of the left Limelight. Translation in metres (x, y, z); rotation in + * degrees (roll, pitch, yaw). + */ + @Getter + final LimelightConfig leftConfig = + new LimelightConfig(leftLL).withTranslation(0, 0.215, 0.188).withRotation(0, 0, 90); + + // -- Right Limelight -------------------------------------------------- + + /** NetworkTables hostname for the right-facing Limelight. */ + @Getter final String rightLL = "limelight-right"; + + /** + * Robot-relative pose of the right Limelight. Translation in metres (x, y, z); rotation in + * degrees (roll, pitch, yaw). + */ + @Getter + final LimelightConfig rightConfig = + new LimelightConfig(rightLL) + .withTranslation(-0.04445, 0.3027487722, 0.7137249886) + .withRotation(0, 0, -90); + + // -- Turret geometry -------------------------------------------------- + + /** Robot-centre to turret pivot offset (metres). */ + @Getter + final Translation2d robotToTurretCenter = + new Translation2d(Units.inchesToMeters(-5.5), Units.inchesToMeters(4.7)); + + /** Turret pivot to camera offset (metres). */ + @Getter + final Translation2d turretCenterToCamera = + new Translation2d(Units.inchesToMeters(-5.641455), 0); + + // -- Pipeline indices ------------------------------------------------- + + @Getter final int backTagPipeline = 0; + @Getter final int leftTagPipeline = 0; + @Getter final int rightTagPipeline = 0; + + // -- Pose estimation covariance --------------------------------------- + + /** + * Default translational standard deviation for vision pose measurements (metres). Lower + * values trust vision more; higher values trust odometry more. + */ + @Getter double visionStdDevX = 0.5; + + /** + * @see #visionStdDevX + */ + @Getter double visionStdDevY = 0.5; + + /** + * Default rotational standard deviation for vision pose measurements (radians). Only used + * for MT1; MT2 heading is always discarded ({@link #kLargeVariance}). + */ + @Getter double visionStdDevTheta = 0.2; + + /** + * Variance used to effectively ignore a measurement dimension (e.g., heading from MT2 or + * single-tag MT1). + */ + @Getter final double kLargeVariance = 999999.0; + + /** Measurements older than this many seconds are not fused. */ + @Getter final double kMaxTimeDeltaSeconds = 0.1; + + /** Pre-built std-dev matrix using the default X/Y/theta values. */ + @Getter + final Matrix visionStdMatrix = + VecBuilder.fill(visionStdDevX, visionStdDevY, visionStdDevTheta); + } + + // ========================================================================= + // Fields + // ========================================================================= + + /** Rear-facing Limelight instance. */ + @Getter public final Limelight backLL; + + /** Left-facing Limelight instance. */ + @Getter public final Limelight leftLL; + + /** Right-facing Limelight instance. */ + @Getter public final Limelight rightLL; + + /** All three Limelights in one array for bulk operations. */ + public final Limelight[] allLimelights; + + /* Vision loggers — one per Limelight */ + private final VisionLogger backLogger; + private final VisionLogger leftLogger; + private final VisionLogger rightLogger; + + /** All three loggers in one array for bulk telemetry loops. */ + private final VisionLogger[] allLoggers; + + /** AprilTag IDs that are valid scoring targets for the blue alliance. */ + private final int[] blueTags = {18, 19, 20, 21, 24, 25, 26, 27}; + + /** AprilTag IDs that are valid scoring targets for the red alliance. */ + private final int[] redTags = {2, 3, 4, 5, 8, 9, 10, 11, 12}; + + /** Field layout loaded once at construction and shared across the robot. */ + @Getter private static AprilTagFieldLayout tagLayout; + + private final VisionConfig config; + + /** + * Tracks the last IMU mode written to each Limelight so we only issue a NetworkTables write + * when the desired mode actually changes. + */ + private final IdentityHashMap lastImuModeByLL = new IdentityHashMap<>(); + + // ========================================================================= + // Construction + // ========================================================================= + + /** + * Creates the Vision subsystem. + * + *

Instantiates all three Limelights, registers their loggers, applies initial camera + * settings (LEDs off, IMU mode 1), loads the AprilTag field layout, and registers this + * subsystem with the WPILib scheduler. + * + * @param config the static configuration object + */ + public Vision(VisionConfig config) { + this.config = config; + + backLL = new Limelight(config.backLL, config.backTagPipeline, config.backConfig); + leftLL = new Limelight(config.leftLL, config.leftTagPipeline, config.leftConfig); + rightLL = new Limelight(config.rightLL, config.rightTagPipeline, config.rightConfig); + + allLimelights = new Limelight[] {backLL, leftLL, rightLL}; + + backLogger = new VisionLogger("BackLL", backLL); + leftLogger = new VisionLogger("LeftLL", leftLL); + rightLogger = new VisionLogger("RightLL", rightLL); + allLoggers = new VisionLogger[] {backLogger, leftLogger, rightLogger}; + + for (Limelight limelight : allLimelights) { + limelight.setLEDMode(false); + setImuModeIfChanged(limelight, 1); + } + + tagLayout = AprilTagFieldLayout.loadField(AprilTagFields.k2026RebuiltWelded); + + this.register(); + Telemetry.print(getName() + " Subsystem Initialized"); + } + + /** + * @return the subsystem name defined in {@link VisionConfig}. + */ + @Override + public String getName() { + return config.getName(); + } + + // ========================================================================= + // Subsystem Periodic + // ========================================================================= + + /** + * Called every robot loop iteration by the WPILib scheduler. + * + *

    + *
  1. Pushes current heading to all Limelights (required for MegaTag2). + *
  2. Runs pose-estimation updates appropriate to the current robot mode. + *
  3. Logs telemetry for all cameras. + *
+ */ + @Override + public void periodic() { + setLimeLightOrientation(); + disabledLimelightUpdates(); + enabledLimelightUpdates(); + logTelemetry(); + } + + /** + * Logs connection status, integration status, tag status, MT1 poses, tag count, and target size + * for each Limelight via their {@link VisionLogger}. MT2 poses are only logged while disabled + * (they are unreliable when moving). Also updates the {@code Field2d} widget with the MT1 pose + * from each camera. + */ + public void logTelemetry() { + for (VisionLogger logger : allLoggers) { + logger.getCameraConnection(); + logger.getIntegratingStatus(); + logger.getLogStatus(); + logger.getTagStatus(); + logger.getPose(); + logger.getTagCount(); + logger.getTargetSize(); + } + + // MT2 poses are only reliable when the robot is stationary + if (Util.disabled.getAsBoolean()) { + backLogger.getMegaPose(); + leftLogger.getMegaPose(); + rightLogger.getMegaPose(); + } + + // Update Field2d visualization (null-safe; returns Pose2d.kZero when no data) + Robot.getField2d().getObject(backLL.getCameraName()).setPose(getBackMegaTag1Pose()); + Robot.getField2d().getObject(leftLL.getCameraName()).setPose(getLeftMegaTag1Pose()); + Robot.getField2d().getObject(rightLL.getCameraName()).setPose(getRightMegaTag1Pose()); + } + + // ========================================================================= + // Pose Estimation — Private Pipeline + // ========================================================================= + + /** + * Pushes the robot's current heading (from swerve odometry) to all Limelights each loop so + * MegaTag2 IMU fusion uses an up-to-date yaw. + */ + private void setLimeLightOrientation() { + double yaw = Robot.getSwerve().getRobotPose().getRotation().getDegrees(); + for (Limelight limelight : allLimelights) { + limelight.setRobotOrientation(yaw); + } + } + + /** + * While the robot is disabled, integrates both MegaTag1 and MegaTag2 estimates from the best + * Limelight to pre-seed the pose estimator before enable. + */ + private void disabledLimelightUpdates() { + if (Util.disabled.getAsBoolean()) { + Limelight bestLimelight = getBestLimelight(); + integrateSingleEstimate(getMT1VisionEstimate(bestLimelight, true)); + integrateSingleEstimate(getMT2VisionEstimate(bestLimelight)); + } + } + + /** + * While the robot is enabled (teleop, auto-update state, or auto-launching), integrates the + * MegaTag1 estimate from the best Limelight. + */ + private void enabledLimelightUpdates() { + if (Util.teleop.getAsBoolean() + || Auton.autonPoseUpdate.getAsBoolean() + || Auton.autonLaunching.getAsBoolean()) { + Limelight bestLimelight = getBestLimelight(); + integrateSingleEstimate(getMT1VisionEstimate(bestLimelight, false)); + } + } + + /** + * Builds a MegaTag1 (multi-tag, heading-fused) pose estimate for the given Limelight and + * decides whether it is trustworthy enough to add to the pose estimator. + * + *

Rejection criteria (any one triggers rejection): + * + *

    + *
  • No targets in view. + *
  • Any tag ambiguity > 0.9 (pose flip risk). + *
  • Pose outside the field boundary. + *
  • Robot spin rate ≥ 1.6 rad/s. + *
  • Target too small (≤ 0.025 %). + *
  • Roll or pitch > 5° (camera physically disturbed). + *
+ * + *

Accepted estimates are assigned std-dev vectors based on how many tags are visible and how + * large the target appears. {@code forceIntegrateXY} overrides the std-devs to near-zero, used + * during disabled pre-seeding. + * + * @param ll the Limelight to query + * @param forceIntegrateXY if {@code true}, bypass std-dev selection and use very tight + * covariance (disabled pre-seeding) + * @return a {@link VisionFieldPoseEstimate} ready to pass to the pose estimator, or {@code + * null} if rejected + */ + private VisionFieldPoseEstimate getMT1VisionEstimate(Limelight ll, boolean forceIntegrateXY) { + if (!ll.targetInView()) { + ll.setTagStatus("No Targets in View"); + ll.sendInvalidStatus("No Targets in View Rejection"); + return null; + } + + boolean multiTags = ll.multipleTagsInView(); + double targetSize = ll.getTargetSize(); + Pose3d megaTag1Pose3d = ll.getMegaTag1_Pose3d(); + Pose2d megaTag1Pose2d = megaTag1Pose3d.toPose2d(); + RawFiducial[] tags = ll.getRawFiducial(); + double highestAmbiguity = -1; + ChassisSpeeds robotSpeed = Robot.getSwerve().getCurrentRobotChassisSpeeds(); + double robotLinearSpeed = + Math.hypot(robotSpeed.vxMetersPerSecond, robotSpeed.vyMetersPerSecond); + + // Distance from current odometry pose to the MT1 estimate + double mt1PoseDifference = + Robot.getSwerve() + .getRobotPose() + .getTranslation() + .getDistance(megaTag1Pose2d.getTranslation()); + + // Ambiguity scan — reject immediately if any tag exceeds 0.9 + ll.setTagStatus(""); + if (tags != null) { + for (RawFiducial tag : tags) { + if (highestAmbiguity < 0 || tag.ambiguity > highestAmbiguity) { + highestAmbiguity = tag.ambiguity; + } + if (tag.ambiguity > 0.9) { + ll.sendInvalidStatus("High Ambiguity Rejection"); + return null; + } + } + } + + // Field boundary, spin rate, and target-size rejections + if (rejectionCheck(ll, megaTag1Pose2d, targetSize)) { + return null; + } + + // Reject if the camera pose shows significant roll or pitch (> 5°) + if (Math.abs(megaTag1Pose3d.getRotation().getX()) > Math.toRadians(5) + || Math.abs(megaTag1Pose3d.getRotation().getY()) > Math.toRadians(5)) { + ll.sendInvalidStatus("Roll/Pitch Rejection"); + return null; + } + + // Select std-devs based on confidence tier + double xyStds; + double degStds; + + if (robotLinearSpeed <= 0.2 && targetSize > 4) { + ll.sendValidStatus("Stationary close integration"); + xyStds = 0.1; + degStds = 0.1; + } else if (multiTags && targetSize > 2) { + ll.sendValidStatus("Strong Multi integration"); + xyStds = 0.1; + degStds = 0.1; + } else if (multiTags && targetSize > 0.2) { + ll.sendValidStatus("Multi integration"); + xyStds = 0.25; + degStds = 8; + } else if (targetSize > 2 && mt1PoseDifference < 0.5) { + ll.sendValidStatus("Close integration"); + xyStds = 0.5; + degStds = config.getKLargeVariance(); + } else if (targetSize > 1 && mt1PoseDifference < 0.25) { + ll.sendValidStatus("Proximity integration"); + xyStds = 1.0; + degStds = config.getKLargeVariance(); + } else if (highestAmbiguity < 0.25 && targetSize >= 0.03) { + ll.sendValidStatus("Stable integration"); + xyStds = 1.5; + degStds = config.getKLargeVariance(); + } else { + ll.sendInvalidStatus("Integration Criteria not Met"); + return null; + } + + // Widen heading std-dev when ambiguity is moderate + if (highestAmbiguity > 0.5) { + degStds = 15; + } + + // Discard heading during fast rotation (MegaTag1 heading unreliable while spinning) + if (robotSpeed.omegaRadiansPerSecond >= 0.5) { + degStds = 50; + } + + // Override covariance for disabled pre-seeding + if (forceIntegrateXY) { + xyStds = 0.01; + degStds = 0.01; + } + + Pose2d integratedPose = + new Pose2d(megaTag1Pose2d.getTranslation(), megaTag1Pose2d.getRotation()); + double timestamp = Utils.fpgaToCurrentTime(ll.getMegaTag1PoseTimestamp()); + // The pose estimator expects the heading std-dev in radians; degStds is in degrees. + Matrix stdDevs = VecBuilder.fill(xyStds, xyStds, Units.degreesToRadians(degStds)); + int numTags = tags == null ? 1 : tags.length; + + return new VisionFieldPoseEstimate(integratedPose, timestamp, stdDevs, numTags); + } + + /** + * Builds a MegaTag2 (IMU-fused, translation-only) pose estimate for the given Limelight. + * + *

MegaTag2 heading is always discarded ({@link VisionConfig#kLargeVariance}) because it + * relies on the IMU rather than tag geometry. This method is only called while the robot is + * disabled. + * + *

Rejection criteria: + * + *

    + *
  • No targets in view. + *
  • Pose outside field boundary. + *
  • Robot spin rate ≥ 1.6 rad/s. + *
  • Target too small (≤ 0.025 %). + *
+ * + * @param ll the Limelight to query + * @return a {@link VisionFieldPoseEstimate} ready to pass to the pose estimator, or {@code + * null} if rejected + */ + private VisionFieldPoseEstimate getMT2VisionEstimate(Limelight ll) { + if (!ll.targetInView()) { + ll.setTagStatus("No Targets in View"); + ll.sendInvalidStatus("No Targets in View Rejection"); + return null; + } + + boolean multiTags = ll.multipleTagsInView(); + double targetSize = ll.getTargetSize(); + Pose2d megaTag2Pose2d = ll.getMegaTag2_Pose2d(); + ChassisSpeeds robotSpeed = Robot.getSwerve().getCurrentRobotChassisSpeeds(); + double robotLinearSpeed = + Math.hypot(robotSpeed.vxMetersPerSecond, robotSpeed.vyMetersPerSecond); + + double mt2PoseDifference = + Robot.getSwerve() + .getRobotPose() + .getTranslation() + .getDistance(megaTag2Pose2d.getTranslation()); + + if (rejectionCheck(ll, megaTag2Pose2d, targetSize)) { + return null; + } + + // Select translational std-devs (heading is always discarded for MT2) + double xyStds; + + if (robotLinearSpeed <= 0.2 && targetSize > 4) { + ll.sendValidStatus("Stationary close integration"); + xyStds = 0.1; + } else if (multiTags && targetSize > 2) { + ll.sendValidStatus("Strong Multi integration"); + xyStds = 0.1; + } else if (multiTags && targetSize > 0.2) { + ll.sendValidStatus("Multi integration"); + xyStds = 0.25; + } else if (targetSize > 2 && (mt2PoseDifference < 0.5 || DriverStation.isDisabled())) { + ll.sendValidStatus("Close integration"); + xyStds = 0.5; + } else if (targetSize > 1 && (mt2PoseDifference < 0.25 || DriverStation.isDisabled())) { + ll.sendValidStatus("Proximity integration"); + xyStds = 1.0; + } else if (targetSize >= 0.03) { + ll.sendValidStatus("Stable integration"); + xyStds = 1.5; + } else { + ll.sendInvalidStatus("Integration Criteria not Met"); + return null; + } + + double degStds = config.getKLargeVariance(); + + Pose2d integratedPose = + new Pose2d(megaTag2Pose2d.getTranslation(), megaTag2Pose2d.getRotation()); + + return new VisionFieldPoseEstimate( + integratedPose, + Utils.fpgaToCurrentTime(ll.getMegaTag2PoseTimestamp()), + VecBuilder.fill(xyStds, xyStds, Units.degreesToRadians(degStds)), + (int) ll.getTagCountInView()); + } + + /** + * Adds a vision measurement to the swerve pose estimator if the estimate is non-null. + * + * @param estimate the estimate to integrate, or {@code null} to skip + */ + private void integrateSingleEstimate(VisionFieldPoseEstimate estimate) { + if (estimate != null) { + Robot.getSwerve() + .addVisionMeasurement( + estimate.getVisionRobotPoseMeters(), + estimate.getTimestampSeconds(), + estimate.getVisionMeasurementStdDevs()); + } + } + + /** + * Common rejection gate shared by both MT1 and MT2 pipelines. + * + *

Rejects on: + * + *

    + *
  • Pose outside field boundary. + *
  • Robot spin rate ≥ 1.6 rad/s. + *
  • Target size ≤ 0.025 % (too far / too small to trust). + *
+ * + * @param ll the Limelight (used for status reporting) + * @param pose the candidate pose to validate + * @param targetSize the Limelight target-size percentage + * @return {@code true} if the measurement should be rejected + */ + private boolean rejectionCheck(Limelight ll, Pose2d pose, double targetSize) { + if (FieldHelpers.poseOutOfField(pose)) { + ll.sendInvalidStatus("Out of Field Rejection"); + return true; + } + + if (Math.abs(Robot.getSwerve().getCurrentRobotChassisSpeeds().omegaRadiansPerSecond) + >= 1.6) { + ll.sendInvalidStatus("Rotation Speed Rejection"); + return true; + } + + if (targetSize <= 0.025) { + ll.sendInvalidStatus("Target Size Rejection"); + return true; + } + + return false; + } + + /** + * Updates the IMU mode on a Limelight only when the desired mode differs from the last written + * value, avoiding redundant NetworkTables writes. + * + * @param limelight the Limelight to configure + * @param desiredMode the IMU mode to apply (0 = external, 1 = internal, etc.) + */ + private void setImuModeIfChanged(Limelight limelight, int desiredMode) { + Integer lastMode = lastImuModeByLL.get(limelight); + if (lastMode == null || lastMode.intValue() != desiredMode) { + limelight.setIMUmode(desiredMode); + lastImuModeByLL.put(limelight, desiredMode); + } + } + + // ========================================================================= + // Pose Access & Queries + // ========================================================================= + + /** + * Returns the MegaTag1 (MT1) pose from the back Limelight, or {@link Pose2d#kZero} if no + * estimate is available. + */ + public Pose2d getBackMegaTag1Pose() { + Pose2d pose = backLL.getMegaTag1_Pose3d().toPose2d(); + return pose != null ? pose : Pose2d.kZero; + } + + /** + * Returns the MegaTag1 (MT1) pose from the left Limelight, or {@link Pose2d#kZero} if no + * estimate is available. + */ + public Pose2d getLeftMegaTag1Pose() { + Pose2d pose = leftLL.getMegaTag1_Pose3d().toPose2d(); + return pose != null ? pose : Pose2d.kZero; + } + + /** + * Returns the MegaTag1 (MT1) pose from the right Limelight, or {@link Pose2d#kZero} if no + * estimate is available. + */ + public Pose2d getRightMegaTag1Pose() { + Pose2d pose = rightLL.getMegaTag1_Pose3d().toPose2d(); + return pose != null ? pose : Pose2d.kZero; + } + + /** + * Returns the MegaTag2 (MT2) pose from the back Limelight, or {@link Pose2d#kZero} if no + * estimate is available. + */ + public Pose2d getBackMegaTag2Pose() { + Pose2d pose = backLL.getMegaTag2_Pose2d(); + return pose != null ? pose : Pose2d.kZero; + } + + /** + * Returns the MegaTag2 (MT2) pose from the left Limelight, or {@link Pose2d#kZero} if no + * estimate is available. + */ + public Pose2d getLeftMegaTag2Pose() { + Pose2d pose = leftLL.getMegaTag2_Pose2d(); + return pose != null ? pose : Pose2d.kZero; + } + + /** + * Returns the MegaTag2 (MT2) pose from the right Limelight, or {@link Pose2d#kZero} if no + * estimate is available. + */ + public Pose2d getRightMegaTag2Pose() { + Pose2d pose = rightLL.getMegaTag2_Pose2d(); + return pose != null ? pose : Pose2d.kZero; + } + + /** + * Selects and returns the Limelight with the highest combined score of visible tag count and + * target size. Ties fall back to {@link #backLL}. + * + * @return the Limelight currently offering the best view of AprilTags + */ + public Limelight getBestLimelight() { + Limelight bestLimelight = backLL; + double bestScore = 0; + for (Limelight limelight : allLimelights) { + double score = limelight.getTagCountInView() + limelight.getTargetSize(); + if (score > bestScore) { + bestScore = score; + bestLimelight = limelight; + } + } + return bestLimelight; + } + + /** + * Returns {@code true} if at least one Limelight reports an accurate pose (via {@link + * Limelight#hasAccuratePose()}). + */ + public boolean hasAccuratePose() { + for (Limelight limelight : allLimelights) { + if (limelight.hasAccuratePose()) return true; + } + return false; + } + + /** + * Returns {@code true} if any Limelight currently sees an AprilTag belonging to the current + * alliance's set of scoring targets. + * + *

Uses {@link DriverStation#getAlliance()} and falls back to Red if the alliance is unknown. + */ + public boolean tagsInView() { + DriverStation.Alliance alliance = + DriverStation.getAlliance().orElse(DriverStation.Alliance.Blue); + int[] allianceTags = (alliance == DriverStation.Alliance.Blue) ? blueTags : redTags; + return Arrays.stream(allLimelights) + .mapToInt(ll -> (int) ll.getClosestTagID()) + .anyMatch(id -> Arrays.stream(allianceTags).anyMatch(tag -> tag == id)); + } + + /** + * Triggers a rewind-capture snapshot on all Limelights (captures 165 seconds of history for + * post-match review). + */ + public void triggerRewindCaptureForAllCameras() { + for (Limelight limelight : allLimelights) { + LimelightHelpers.triggerRewindCapture(limelight.getName(), 165); + } + } + + // ========================================================================= + // Pose Reset + // ========================================================================= + + /** + * Resets the robot pose to the best Limelight's vision pose. Delegates to {@link + * #resetPoseToVision(boolean, Pose3d, Pose2d, double)}. + */ + public void resetPoseToVision() { + Limelight ll = getBestLimelight(); + resetPoseToVision( + ll.targetInView(), + ll.getMegaTag1_Pose3d(), + ll.getMegaTag2_Pose2d(), + ll.getMegaTag1PoseTimestamp()); + } + + /** + * Resets the robot pose to a specific vision pose if the data passes sanity checks. + * + *

Uses a very tight covariance ({@code 0.00001}) so the pose estimator snaps immediately to + * the vision measurement. The MT1 heading is preserved while MT2 provides the translation. + * + *

Rejection criteria: + * + *

    + *
  • No target in view. + *
  • Pose outside field boundary. + *
  • Camera height {@code |z| > 0.25 m} (robot floating / bad solve). + *
  • Roll or pitch > 5° (camera physically disturbed). + *
+ * + * @param targetInView whether the Limelight has a target + * @param botpose3D the MT1 Pose3d from the Limelight + * @param megaPose the MT2 Pose2d (provides translation for the reset) + * @param poseTimestamp the FPGA timestamp of the pose estimate + * @return {@code true} if the pose was accepted and the reset was applied + */ + public boolean resetPoseToVision( + boolean targetInView, Pose3d botpose3D, Pose2d megaPose, double poseTimestamp) { + + if (!targetInView) return false; + + Pose2d botpose = botpose3D.toPose2d(); + + if (FieldHelpers.poseOutOfField(botpose3D)) { + Telemetry.log("Vision/PoseReset/Rejection", "Out of field"); + return false; + } + if (Math.abs(botpose3D.getZ()) > 0.25) { + Telemetry.log("Vision/PoseReset/Rejection", "Pose in air"); + return false; + } + if (Math.abs(botpose3D.getRotation().getX()) > Math.toRadians(5) + || Math.abs(botpose3D.getRotation().getY()) > Math.toRadians(5)) { + Telemetry.log("Vision/PoseReset/Rejection", "Pose tilted"); + return false; + } + + double[] before = {botpose.getX(), botpose.getY(), botpose.getRotation().getDegrees()}; + Telemetry.log("Vision/PoseReset/Before", before); + + // Use MT2 translation + MT1 heading for best combined accuracy + Pose2d integratedPose = new Pose2d(megaPose.getTranslation(), botpose.getRotation()); + Robot.getSwerve() + .addVisionMeasurement( + integratedPose, poseTimestamp, VecBuilder.fill(0.00001, 0.00001, 0.00001)); + + Pose2d updated = Robot.getSwerve().getRobotPose(); + double[] after = {updated.getX(), updated.getY(), updated.getRotation().getDegrees()}; + Telemetry.log("Vision/PoseReset/After", after); + + return true; + } + + // ========================================================================= + // Camera Control + // ========================================================================= + + /** + * Sets all Limelights to the given pipeline index. + * + * @param pipeline the zero-indexed pipeline number to activate + */ + public void setLimelightPipelines(int pipeline) { + for (Limelight limelight : allLimelights) { + limelight.setLimelightPipeline(pipeline); + } + } + + // ========================================================================= + // Commands + // ========================================================================= + + /** + * Returns a command that blinks all Limelight LEDs while active and turns them off when the + * command ends. + * + * @return the blink command + */ + public Command blinkLimelights() { + Telemetry.print("Vision.blinkLimelights", PrintPriority.HIGH); + return startEnd( + () -> { + for (Limelight limelight : allLimelights) { + limelight.blinkLEDs(); + } + }, + () -> { + for (Limelight limelight : allLimelights) { + limelight.setLEDMode(false); + } + }) + .withName("Vision.blinkLimelights"); + } + + /** + * Returns a command that holds all Limelight LEDs solid-on while active and turns them off when + * the command ends. + * + * @return the solid-LED command + */ + public Command solidLimelight() { + return startEnd( + () -> { + for (Limelight limelight : allLimelights) { + limelight.setLEDMode(true); + } + }, + () -> { + for (Limelight limelight : allLimelights) { + limelight.setLEDMode(false); + } + }) + .withName("Vision.solidLimelight"); + } + + // ========================================================================= + // Inner Classes + // ========================================================================= + + /** + * Immutable data class that bundles a vision-derived field pose with its FPGA timestamp and + * covariance matrix, ready for use with {@code + * SwerveDrivePoseEstimator.addVisionMeasurement()}. + */ + @Getter + public class VisionFieldPoseEstimate { + + /** The estimated field-relative robot pose (metres, radians). */ + private final Pose2d visionRobotPoseMeters; + + /** The FPGA-converted timestamp of this measurement (seconds). */ + private final double timestampSeconds; + + /** + * The 3×1 standard-deviation vector {@code [x, y, theta]} passed to the pose estimator. + * Larger values indicate less trust in that dimension. + */ + private final Matrix visionMeasurementStdDevs; + + /** Number of AprilTags that contributed to this estimate. */ + private final int numTags; + + /** + * @param visionRobotPoseMeters field-relative robot pose + * @param timestampSeconds FPGA-converted capture timestamp + * @param visionMeasurementStdDevs 3×1 std-dev vector [x, y, theta] + * @param numTags number of tags used in the solve + */ + public VisionFieldPoseEstimate( + Pose2d visionRobotPoseMeters, + double timestampSeconds, + Matrix visionMeasurementStdDevs, + int numTags) { + this.visionRobotPoseMeters = visionRobotPoseMeters; + this.timestampSeconds = timestampSeconds; + this.visionMeasurementStdDevs = visionMeasurementStdDevs; + this.numTags = numTags; + } + } +} diff --git a/src/main/java/frc/robot/swerve/SwerveStates.java b/src/main/java/frc/robot/swerve/SwerveStates.java deleted file mode 100644 index 9e069b6f..00000000 --- a/src/main/java/frc/robot/swerve/SwerveStates.java +++ /dev/null @@ -1,564 +0,0 @@ -package frc.robot.swerve; - -import static edu.wpi.first.units.Units.Meters; -import static edu.wpi.first.units.Units.MetersPerSecond; - -import com.ctre.phoenix6.swerve.SwerveModule.DriveRequestType; -import com.ctre.phoenix6.swerve.SwerveModule.SteerRequestType; -import com.ctre.phoenix6.swerve.SwerveRequest; -import edu.wpi.first.math.filter.SlewRateLimiter; -import edu.wpi.first.math.geometry.Rotation2d; -import edu.wpi.first.math.util.Units; -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.Commands; -import edu.wpi.first.wpilibj2.command.button.Trigger; -import frc.rebuilt.Field; -import frc.rebuilt.ShotCalculator; -import frc.robot.Robot; -import frc.robot.RobotStates; -import frc.robot.State; -import frc.robot.operator.Operator; -import frc.robot.pilot.Pilot; -import frc.spectrumLib.Telemetry; -import frc.spectrumLib.util.Util; -import java.text.DecimalFormat; -import java.text.NumberFormat; -import java.util.Set; -import java.util.function.DoubleSupplier; - -public class SwerveStates { - // ------------------------------------------------------------------------ - // Dependencies / singletons - // ------------------------------------------------------------------------ - private static final Swerve swerve = Robot.getSwerve(); - private static final SwerveConfig config = Robot.getConfig().swerve; - private static final Pilot pilot = Robot.getPilot(); - private static final Operator operator = Robot.getOperator(); - - // ------------------------------------------------------------------------ - // Requests (pre-configured CTRE swerve requests) - // ------------------------------------------------------------------------ - private static final SwerveRequest.FieldCentric FIELD_CENTRIC_DRIVE = - new SwerveRequest.FieldCentric() - .withDeadband( - config.getSpeedAt12Volts().in(MetersPerSecond) * config.getDeadband()) - .withRotationalDeadband(config.getMaxAngularRate() * config.getDeadband()) - .withDriveRequestType(DriveRequestType.Velocity); - - private static final SwerveRequest.RobotCentric ROBOT_CENTRIC_DRIVE = - new SwerveRequest.RobotCentric() - .withDeadband( - config.getSpeedAt12Volts().in(MetersPerSecond) * config.getDeadband()) - .withRotationalDeadband(config.getMaxAngularRate() * config.getDeadband()) - .withDriveRequestType(DriveRequestType.Velocity); - - private static final SwerveRequest.SwerveDriveBrake X_BRAKE_REQUEST = - new SwerveRequest.SwerveDriveBrake(); - - private static final SwerveRequest.FieldCentricFacingAngle FIELD_CENTRIC_FACING_ANGLE = - new SwerveRequest.FieldCentricFacingAngle() - .withDeadband( - config.getSpeedAt12Volts().in(MetersPerSecond) - * config.getAimDeadband()) - .withRotationalDeadband(config.getMaxAngularRate() * config.getAimDeadband()) - .withDriveRequestType(DriveRequestType.Velocity) - .withSteerRequestType(SteerRequestType.Position) - .withMaxAbsRotationalRate(config.getMaxAngularRate()) - .withHeadingPID( - config.getKPRotationController(), - config.getKIRotationController(), - config.getKDRotationController()); - - private static final SwerveRequest.RobotCentricFacingAngle ROBOT_CENTRIC_FACING_ANGLE = - new SwerveRequest.RobotCentricFacingAngle() - .withDeadband( - config.getSpeedAt12Volts().in(MetersPerSecond) - * config.getAimDeadband()) - .withRotationalDeadband(config.getMaxAngularRate() * config.getAimDeadband()) - .withDriveRequestType(DriveRequestType.Velocity) - .withSteerRequestType(SteerRequestType.Position) - .withMaxAbsRotationalRate(config.getMaxAngularRate()) - .withHeadingPID( - config.getKPRotationController(), - config.getKIRotationController(), - config.getKDRotationController()); - - // ------------------------------------------------------------------------ - // Triggers - // ------------------------------------------------------------------------ - @SuppressWarnings("unused") - private static final Trigger snakeDrive = - new Trigger(() -> RobotStates.getAppliedState() == State.SNAKE_INTAKE); - - private static final Trigger launching = - new Trigger( - () -> - RobotStates.getAppliedState() == State.LAUNCH_WITH_SQUEEZE - || RobotStates.getAppliedState() - == State.LAUNCH_WITHOUT_SQUEEZE); - - private static final Trigger launchPreping = - new Trigger(() -> RobotStates.getAppliedState() == State.TRACK_TARGET); - - private static final Trigger isRed = new Trigger(() -> Field.isRed()); - - public static Trigger robotInNeutralZone() { - return swerve.inNeutralZone(); - } - - public static Trigger robotInEnemyZone() { - return swerve.inEnemyAllianceZone(); - } - - // ------------------------------------------------------------------------ - // Default command - // ------------------------------------------------------------------------ - private static final Command pilotSteerCommand = - log(pilotDrive().withName("SwerveCommands.pilotSteer").ignoringDisable(true)); - - protected static void setupDefaultCommand() { - swerve.setDefaultCommand(pilotSteerCommand); - } - - // ------------------------------------------------------------------------ - // Bindings / state setup - // ------------------------------------------------------------------------ - protected static void setStates() { - // Force back to manual steering when we steer - pilot.steer.whileTrue(swerve.getDefaultCommand()); - operator.steer.whileTrue(swerve.getDefaultCommand()); - - pilot.fpv_LS.whileTrue(log(fpvDrive())); - - (launching.or(launchPreping)) - .and(isRed, Util.autoMode.not()) - .whileTrue(log(pilotAimAtTargetRed())); - (launching.or(launchPreping)) - .and(isRed.not(), Util.autoMode.not()) - .whileTrue(log(pilotAimAtTargetBlue())); - // launching.and(Robot.getPilot().fn).whileTrue(log(tweakOut())); - launching.and(Robot.getPilot().RB).whileTrue(log(xBrake())); - - pilot.upReorient.onTrue(log(reorientForward())); - pilot.leftReorient.onTrue(log(reorientLeft())); - pilot.downReorient.onTrue(log(reorientBack())); - pilot.rightReorient.onTrue(log(reorientRight())); - } - - // ------------------------------------------------------------------------ - // Pilot commands (public-facing) - // ------------------------------------------------------------------------ - - /** - * Drive the robot using left stick and control orientation using the right stick. - * - * @return A command that drives the robot with translation control from the left stick and - * rotation control from the right stick - */ - protected static Command pilotDrive() { - return drive( - pilot::getDriveFwdPositive, - pilot::getDriveLeftPositive, - pilot::getDriveCCWPositive) - .withName("Swerve.PilotDrive"); - } - - /** - * Drive the robot with the front bumper trying to match the angle to the target. - * - * @return A command that drives the robot to match the angle to the target while allowing - * translation control with the left stick - */ - protected static Command pilotAimAtTargetRed() { - return aimDrive( - pilot::getDriveFwdPositive, - pilot::getDriveLeftPositive, - () -> - ShotCalculator.getInstance() - .getParameters() - .driveAngle() - .getRadians()) - .withName("Swerve.pilotAimAtTarget"); - } - - protected static Command pilotAimAtTargetBlue() { - return aimDrive( - pilot::getDriveFwdPositive, - pilot::getDriveLeftPositive, - () -> - ShotCalculator.getInstance() - .getParameters() - .driveAngle() - .plus(Rotation2d.k180deg) - .getRadians()) - .withName("Swerve.pilotAimAtTarget"); - } - - public static Command autonAimAtTarget() { - return aimDrive( - () -> 0, // No translation control in auton - () -> 0, - () -> { - if (Field.isBlue()) { - return ShotCalculator.getInstance() - .getParameters() - .driveAngle() - .plus(Rotation2d.k180deg) - .getRadians(); - } else { - return ShotCalculator.getInstance() - .getParameters() - .driveAngle() - .getRadians(); - } - }) - .withName("Swerve.autonAimAtTarget"); - } - - /** - * Drive the robot with its front bumper as the forward direction. - * - * @return A command that drives the robot with the front bumper as the forward direction, using - * the left stick for translation and the right stick for rotation - */ - protected static Command fpvDrive() { - return fpvDrive( - pilot::getDriveFwdPositive, - pilot::getDriveLeftPositive, - pilot::getDriveCCWPositive) - .withName("Swerve.PilotFPVDrive"); - } - - /** - * Drive the robot with the robot's orientation snapping to the closest cardinal direction. - * - * @return A command that drives the robot with the robot's orientation snapping to the closest - * cardinal direction, using the left stick for translation - */ - protected static Command snapSteerDrive() { - return drive( - pilot::getDriveFwdPositive, - pilot::getDriveLeftPositive, - pilot::chooseCardinalDirections) - .withName("Swerve.PilotStickSteer"); - } - - /** - * Drive the robot with the front bumper trying to match the robot's motion direction. - * - * @return A command that drives the robot with translation control from the left stick, while - * also trying to match the robot's motion direction with the robot angle - */ - protected static Command snakeDrive() { - return aimDrive( - pilot::getDriveFwdPositive, - pilot::getDriveLeftPositive, - pilot::getPilotStickAngle) - .withName("Swerve.SnakeDrive"); - } - - /** - * Tweak the robot's orientation by a small angle back and forth. - * - * @return A command that repeatedly tweaks the robot's orientation. - */ - protected static Command tweakOut() { - return Commands.defer( - () -> { - final double base = swerve.getRotation().getRadians(); - final double delta = Math.toRadians(10.0); - - Command toMinus = - aimDrive( - pilot::getDriveFwdPositive, - pilot::getDriveLeftPositive, - () -> base - delta) - .withTimeout(0.5); - - Command toPlus = - aimDrive( - pilot::getDriveFwdPositive, - pilot::getDriveLeftPositive, - () -> base + delta) - .withTimeout(0.5); - - return Commands.repeatingSequence(toMinus, toPlus); - }, - Set.of(swerve)) - .withName("Swerve.tweakOut"); - } - - /** Turn the swerve wheels to an X to prevent the robot from moving. */ - protected static Command xBrake() { - return swerve.applyRequest(() -> X_BRAKE_REQUEST).withName("Swerve.Xbrake"); - } - - /** - * Drive the robot with the front bumper trying to match a target angle. - * - * @param targetDegrees The target angle (expected to be in radians in current call sites) - * @return A command that drives the robot to match the target angle while allowing translation - * control with the left stick - */ - protected static Command pilotAimDrive(DoubleSupplier targetDegrees) { - return aimDrive(pilot::getDriveFwdPositive, pilot::getDriveLeftPositive, targetDegrees) - .withName("Swerve.PilotAimDrive"); - } - - /** - * Align the robot to the given x, y, and heading goals. - * - * @param xGoalMeters The x goal in meters - * @param yGoalMeters The y goal in meters - * @param headingRadians The heading goal in radians - * @return A command that aligns the robot to the specified x, y, and heading goals - */ - public static Command alignDrive( - DoubleSupplier xGoalMeters, DoubleSupplier yGoalMeters, DoubleSupplier headingRadians) { - - final DoubleSupplier x = getAlignToX(xGoalMeters); - final DoubleSupplier y = getAlignToY(yGoalMeters); - - final boolean invertForRed = Field.isRed(); - - return Commands.sequence( - resetXController(), - resetYController(), - aimDrive( - () -> invertForRed ? -x.getAsDouble() : x.getAsDouble(), - () -> invertForRed ? -y.getAsDouble() : y.getAsDouble(), - headingRadians)); - } - - protected static Command headingLockDrive() { - return headingLock(pilot::getDriveFwdPositive, pilot::getDriveLeftPositive) - .withName("Swerve.PilotHeadingLockDrive"); - } - - protected static Command lockToClosest45Drive() { - return lockToClosest45deg(pilot::getDriveFwdPositive, pilot::getDriveLeftPositive) - .withName("Swerve.PilotLockTo45degDrive"); - } - - // ------------------------------------------------------------------------ - // Controller/heading helpers - // ------------------------------------------------------------------------ - private static DoubleSupplier getAlignToX(DoubleSupplier xGoalMeters) { - return swerve.calculateXController(xGoalMeters); - } - - private static DoubleSupplier getAlignToY(DoubleSupplier yGoalMeters) { - return swerve.calculateYController(yGoalMeters); - } - - // ------------------------------------------------------------------------ - // Small helper commands (reset/set) - // ------------------------------------------------------------------------ - protected static Command resetXController() { - return swerve.runOnce(swerve::resetXController).withName("ResetXController"); - } - - protected static Command resetYController() { - return swerve.runOnce(swerve::resetYController).withName("ResetYController"); - } - - protected static Command resetTurnController() { - return swerve.runOnce(swerve::resetRotationController).withName("ResetTurnController"); - } - - protected static Command setTargetHeading(DoubleSupplier targetHeading) { - return Commands.runOnce(() -> config.setTargetHeading(targetHeading.getAsDouble())) - .withName("SetTargetHeading"); - } - - // ------------------------------------------------------------------------ - // Core drive primitives (compose these into higher-level behaviors) - // ------------------------------------------------------------------------ - private static Command drive( - DoubleSupplier fwdPositive, DoubleSupplier leftPositive, DoubleSupplier ccwPositive) { - return swerve.applyRequest( - () -> - FIELD_CENTRIC_DRIVE - .withVelocityX(fwdPositive.getAsDouble()) - .withVelocityY(leftPositive.getAsDouble()) - .withRotationalRate(ccwPositive.getAsDouble())) - .withName("Swerve.drive"); - } - - private static Command fpvDrive( - DoubleSupplier fwdPositive, DoubleSupplier leftPositive, DoubleSupplier ccwPositive) { - return swerve.applyRequest( - () -> - ROBOT_CENTRIC_DRIVE - .withVelocityX(fwdPositive.getAsDouble()) - .withVelocityY(leftPositive.getAsDouble()) - .withRotationalRate(ccwPositive.getAsDouble())) - .withName("Swerve.fpvDrive"); - } - - protected static Command fpvAimDrive( - DoubleSupplier velocityX, DoubleSupplier velocityY, DoubleSupplier targetRadians) { - return swerve.applyRequest( - () -> - ROBOT_CENTRIC_FACING_ANGLE - .withVelocityX(velocityX.getAsDouble()) - .withVelocityY(velocityY.getAsDouble()) - .withTargetDirection( - new Rotation2d(targetRadians.getAsDouble()))) - .withName("Swerve.fpvAimDrive"); - } - - protected static Command aimDrive( - DoubleSupplier velocityX, DoubleSupplier velocityY, DoubleSupplier targetRadians) { - return swerve.applyRequest( - () -> - FIELD_CENTRIC_FACING_ANGLE - .withVelocityX(velocityX.getAsDouble()) - .withVelocityY(velocityY.getAsDouble()) - .withTargetDirection( - new Rotation2d(targetRadians.getAsDouble()))) - .withName("Swerve.aimDrive"); - } - - protected static Command headingLock(DoubleSupplier velocityX, DoubleSupplier velocityY) { - return aimDrive(velocityX, velocityY, () -> swerve.getRotation().getRadians()) - .withName("Swerve.HeadingLock"); - } - - protected static Command lockToClosest45deg( - DoubleSupplier velocityX, DoubleSupplier velocityY) { - return aimDrive(velocityX, velocityY, swerve::getClosest45).withName("Swerve.LockTo45deg"); - } - - // ------------------------------------------------------------------------ - // Swerve characterization routines - // ------------------------------------------------------------------------ - private static final double WHEEL_RADIUS_MAX_VELOCITY = 1; // rad/s - private static final double WHEEL_RADIUS_RAMP_RATE = 0.5; // rad/s^2 - - public static double[] getWheelRadiusCharacterizationPositions() { - double[] positions = new double[4]; - double wheelRadiusGuess = config.getWheelRadius().in(Meters); // current config value - - for (int i = 0; i < 4; i++) { - positions[i] = - swerve.getModule(i).getCachedPosition().distanceMeters / wheelRadiusGuess; - } - return positions; - } - - /** - * Measures the robot's wheel radius by spinning in a circle. (Method from AdvantageKit). - * - *

This command ramps up the robot's rotation rate to a specified maximum while recording the - * change in gyro angle and wheel positions. When the command is cancelled, it calculates and - * prints the effective wheel radius based on the recorded data. - */ - public static Command wheelRadiusCharacterization() { - SlewRateLimiter limiter = new SlewRateLimiter(WHEEL_RADIUS_RAMP_RATE); - WheelRadiusCharacterizationState state = new WheelRadiusCharacterizationState(); - - return Commands.parallel( - // Drive control sequence - Commands.sequence( - Commands.runOnce(() -> limiter.reset(0.0)), - Commands.run( - () -> { - double speed = limiter.calculate(WHEEL_RADIUS_MAX_VELOCITY); - swerve.setControl( - FIELD_CENTRIC_DRIVE - .withVelocityX(0) - .withVelocityY(0) - .withRotationalRate(speed)); - }, - swerve)), - - // Measurement sequence - Commands.sequence( - Commands.waitSeconds(1.0), - Commands.runOnce( - () -> { - state.positions = getWheelRadiusCharacterizationPositions(); - state.lastAngle = swerve.getRotation(); - state.gyroDelta = 0.0; - }), - Commands.run( - () -> { - var rotation = swerve.getRotation(); - state.gyroDelta += - Math.abs( - rotation.minus(state.lastAngle) - .getRadians()); - state.lastAngle = rotation; - }) - .finallyDo( - () -> { - double[] positions = - getWheelRadiusCharacterizationPositions(); - double wheelDelta = 0.0; - for (int i = 0; i < 4; i++) { - wheelDelta += - Math.abs(positions[i] - state.positions[i]) - / 4.0; - } - double wheelRadius = - (state.gyroDelta - * config - .getDrivebaseRadiusMeters()) - / wheelDelta; - - NumberFormat formatter = new DecimalFormat("#0.000"); - Telemetry.log( - "WheelRadiusCharacterization/WheelDelta", - formatter.format(wheelDelta) + " radians"); - Telemetry.log( - "WheelRadiusCharacterization/GyroDelta", - formatter.format(state.gyroDelta) + " radians"); - Telemetry.log( - "WheelRadiusCharacterization/WheelRadiusMeters", - formatter.format(wheelRadius) + " meters"); - Telemetry.log( - "WheelRadiusCharacterization/WheelRadiusInches", - formatter.format( - Units.metersToInches( - wheelRadius)) - + " inches"); - }))); - } - - private static class WheelRadiusCharacterizationState { - double[] positions = new double[4]; - Rotation2d lastAngle = Rotation2d.kZero; - double gyroDelta = 0.0; - } - - // ------------------------------------------------------------------------ - // Reorient commands - // ------------------------------------------------------------------------ - protected static Command reorientForward() { - return swerve.reorientPilotAngle(0).withName("Swerve.reorientForward"); - } - - protected static Command reorientLeft() { - return swerve.reorientPilotAngle(90).withName("Swerve.reorientLeft"); - } - - protected static Command reorientBack() { - return swerve.reorientPilotAngle(180).withName("Swerve.reorientBack"); - } - - protected static Command reorientRight() { - return swerve.reorientPilotAngle(270).withName("Swerve.reorientRight"); - } - - protected static Command cardinalReorient() { - return swerve.cardinalReorient().withName("Swerve.cardinalReorient"); - } - - // ------------------------------------------------------------------------ - // Telemetry - // ------------------------------------------------------------------------ - protected static Command log(Command cmd) { - return Telemetry.log(cmd); - } -} diff --git a/src/main/java/frc/robot/swerve/controllers/RotationController.java b/src/main/java/frc/robot/swerve/controllers/RotationController.java deleted file mode 100644 index ef42add2..00000000 --- a/src/main/java/frc/robot/swerve/controllers/RotationController.java +++ /dev/null @@ -1,89 +0,0 @@ -package frc.robot.swerve.controllers; - -import edu.wpi.first.math.controller.PIDController; -import edu.wpi.first.math.controller.ProfiledPIDController; -import edu.wpi.first.math.trajectory.TrapezoidProfile.Constraints; -import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; -import frc.robot.swerve.SwerveConfig; - -public class RotationController { - private final ProfiledPIDController motionController; - private final PIDController holdController; - private final SwerveConfig config; - - @SuppressWarnings("unused") - private double lastOutput = 0.0; - - private final double deadband = 1e-3; - - public RotationController(SwerveConfig config) { - this.config = config; - - motionController = - new ProfiledPIDController( - config.getKPRotationController(), - config.getKIRotationController(), - config.getKDRotationController(), - new Constraints( - config.getMaxAngularVelocity(), - config.getMaxAngularAcceleration())); - - motionController.enableContinuousInput(-Math.PI, Math.PI); - motionController.setTolerance(config.getRotationTolerance()); - SmartDashboard.putData("PID Controllers/Rotation Controller", motionController); - - holdController = - new PIDController( - config.getKPHoldController(), - config.getKIHoldController(), - config.getKDHoldController()); - - holdController.enableContinuousInput(-Math.PI, Math.PI); - holdController.setTolerance(config.getRotationTolerance() / 2.0); - SmartDashboard.putData("PID Controllers/Hold Controller", holdController); - } - - public double calculate(double goalRadians, double currentRadians, boolean useHold) { - double output; - - if (useHold && atGoal()) { - output = holdController.calculate(currentRadians, goalRadians); - } else { - output = motionController.calculate(currentRadians, goalRadians); - } - - // SmartDashboard.putNumber("Rotation Output (raw)", output); - - if (Math.abs(output) > deadband) { - output += config.getKSsteer() * Math.signum(output); - } - - lastOutput = output; - // SmartDashboard.putNumber("Rotation Output (final)", output); - // SmartDashboard.putBoolean("Rotation At Goal", motionController.atGoal()); - - return output; - } - - public double calculate(double goalRadians, double currentRadians) { - return calculate(goalRadians, currentRadians, true); - } - - public boolean atGoal() { - return motionController.atGoal(); - } - - public boolean atSetpoint() { - return motionController.atSetpoint(); - } - - public void reset(double currentRadians) { - motionController.reset(currentRadians); - holdController.reset(); - lastOutput = 0.0; - } - - public void updatePID(double kP, double kI, double kD) { - motionController.setPID(kP, kI, kD); - } -} diff --git a/src/main/java/frc/robot/swerve/controllers/TagCenterAlignController.java b/src/main/java/frc/robot/swerve/controllers/TagCenterAlignController.java deleted file mode 100644 index a64c4061..00000000 --- a/src/main/java/frc/robot/swerve/controllers/TagCenterAlignController.java +++ /dev/null @@ -1,65 +0,0 @@ -package frc.robot.swerve.controllers; - -import edu.wpi.first.math.controller.PIDController; -import edu.wpi.first.math.trajectory.TrapezoidProfile.Constraints; -import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; -import frc.robot.Robot; -import frc.robot.swerve.Swerve; -import frc.robot.swerve.SwerveConfig; - -/** - * Uses a profiled PID Controller to quickly turn the robot to a specified angle. Once the robot is - * within a certain tolerance of the goal angle, a PID controller is used to hold the robot at that - * angle. - */ -public class TagCenterAlignController { - Swerve swerve; - SwerveConfig config; - PIDController controller; - Constraints constraints; - - double calculatedValue = 0; - double maxVelocity; - - public TagCenterAlignController(SwerveConfig config) { - this.config = config; - maxVelocity = Robot.getConfig().swerve.getSpeedAt12Volts().baseUnitMagnitude() * 0.5; - controller = - new PIDController( - config.getKPTagCenterController(), - config.getKITagCenterController(), - config.getKDTagCenterController()); - controller.setTolerance(0.0); - - SmartDashboard.putData("PID Controllers/tagCenterController", controller); - } - - public double calculate(double goalMeters, double currentMeters) { - calculatedValue = controller.calculate(currentMeters, goalMeters); - - if (atGoal(currentMeters)) { - calculatedValue = 0; - return calculatedValue; - } else { - if (Math.abs(calculatedValue) > maxVelocity) { - calculatedValue = maxVelocity * Math.signum(calculatedValue); - } - return calculatedValue + (config.getKSdrive() * Math.signum(calculatedValue)); - } - } - - public boolean atGoal(double current) { - double goal = controller.getSetpoint(); - boolean atGoal = Math.abs(current - goal) < config.getTagCenterTolerance(); - System.out.println("At Goal: " + atGoal + " Goal: " + goal + " Current: " + current); - return atGoal; - } - - public boolean atSetpoint() { - return controller.atSetpoint(); - } - - public void updatePID(double kP, double kI, double kD) { - controller.setPID(kP, kI, kD); - } -} diff --git a/src/main/java/frc/robot/swerve/controllers/TagDistanceAlignController.java b/src/main/java/frc/robot/swerve/controllers/TagDistanceAlignController.java deleted file mode 100644 index 45725e81..00000000 --- a/src/main/java/frc/robot/swerve/controllers/TagDistanceAlignController.java +++ /dev/null @@ -1,59 +0,0 @@ -package frc.robot.swerve.controllers; - -import edu.wpi.first.math.controller.PIDController; -import edu.wpi.first.math.trajectory.TrapezoidProfile.Constraints; -import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; -import frc.robot.swerve.Swerve; -import frc.robot.swerve.SwerveConfig; - -/** - * Uses a profiled PID Controller to quickly turn the robot to a specified angle. Once the robot is - * within a certain tolerance of the goal angle, a PID controller is used to hold the robot at that - * angle. - */ -public class TagDistanceAlignController { - Swerve swerve; - SwerveConfig config; - PIDController controller; - Constraints constraints; - - double calculatedValue = 0; - - public TagDistanceAlignController(SwerveConfig config) { - this.config = config; - controller = - new PIDController( - config.getKPTagDistanceController(), - config.getKITagDistanceController(), - config.getKDTagDistanceController()); - - controller.setTolerance(0.0); - SmartDashboard.putData("PID Controllers/tagDistanceController", controller); - } - - public double calculate(double goalArea, double currentArea) { - calculatedValue = controller.calculate(currentArea, goalArea); - - if (atGoal(currentArea)) { - calculatedValue = 0; - return calculatedValue; - } else { - return calculatedValue + (config.getKSdrive() * Math.signum(calculatedValue)); - } - } - - public boolean atGoal(double current) { - double goal = controller.getSetpoint(); - boolean atGoal = Math.abs(current - goal) < config.getTagDistanceTolerance(); - System.out.println("At Goal: " + atGoal + " Goal: " + goal + " Current: " + current); - return atGoal; - } - - public void reset(double current) { - // controller.reset(current); - } - - public void updatePID(double kP, double kI, double kD) { - controller.setPID(kP, kI, kD); - } -} diff --git a/src/main/java/frc/robot/swerve/controllers/TranslationXController.java b/src/main/java/frc/robot/swerve/controllers/TranslationXController.java deleted file mode 100644 index 900f1d72..00000000 --- a/src/main/java/frc/robot/swerve/controllers/TranslationXController.java +++ /dev/null @@ -1,66 +0,0 @@ -package frc.robot.swerve.controllers; - -import edu.wpi.first.math.MathUtil; -import edu.wpi.first.math.controller.ProfiledPIDController; -import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; -import frc.robot.swerve.SwerveConfig; - -/** - * Uses a profiled PID Controller to quickly turn the robot to a specified angle. Once the robot is - * within a certain tolerance of the goal angle, a PID controller is used to hold the robot at that - * angle. - */ -public class TranslationXController { - private final SwerveConfig config; - private final ProfiledPIDController controller; - private final double deadband = 1e-3; - - public TranslationXController(SwerveConfig config) { - this.config = config; - this.controller = - new ProfiledPIDController( - config.getKPTranslationController(), - config.getKITranslationController(), - config.getKDTranslationController(), - config.getTranslationConstraints()); - - controller.setTolerance(config.getTranslationTolerance()); - SmartDashboard.putData("PID Controllers/X Controller", controller); - } - - public double calculate(double goalMeters, double currentMeters) { - if (controller.atGoal()) { - return 0.0; - } - - double output = controller.calculate(currentMeters, goalMeters); - - if (Math.abs(output) > deadband) { - output += config.getKSdrive() * Math.signum(output); - } - - output = - MathUtil.clamp( - output, - -config.getTranslationConstraints().maxVelocity, - config.getTranslationConstraints().maxVelocity); - - // SmartDashboard.putNumber("X Controller Output", output); - // SmartDashboard.putBoolean("X At Goal", controller.atGoal()); - // SmartDashboard.putNumber("X Position Error", controller.getPositionError()); - // SmartDashboard.putNumber("X Tolerance", controller.getPositionTolerance()); - return output; - } - - public boolean atGoal() { - return controller.atGoal(); - } - - public void reset(double currentMeters) { - controller.reset(currentMeters); - } - - public void updatePID(double kP, double kI, double kD) { - controller.setPID(kP, kI, kD); - } -} diff --git a/src/main/java/frc/robot/swerve/controllers/TranslationYController.java b/src/main/java/frc/robot/swerve/controllers/TranslationYController.java deleted file mode 100644 index 42c682a5..00000000 --- a/src/main/java/frc/robot/swerve/controllers/TranslationYController.java +++ /dev/null @@ -1,68 +0,0 @@ -package frc.robot.swerve.controllers; - -import edu.wpi.first.math.MathUtil; -import edu.wpi.first.math.controller.ProfiledPIDController; -import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; -import frc.robot.swerve.SwerveConfig; - -/** - * Uses a profiled PID Controller to quickly turn the robot to a specified angle. Once the robot is - * within a certain tolerance of the goal angle, a PID controller is used to hold the robot at that - * angle. - */ -public class TranslationYController { - private final SwerveConfig config; - private final ProfiledPIDController controller; - private final double deadband = 1e-3; - - double calculatedValue = 0; - - public TranslationYController(SwerveConfig config) { - this.config = config; - this.controller = - new ProfiledPIDController( - config.getKPTranslationController(), - config.getKITranslationController(), - config.getKDTranslationController(), - config.getTranslationConstraints()); - - controller.setTolerance(config.getTranslationTolerance()); - SmartDashboard.putData("PID Controllers/Y Controller", controller); - } - - public double calculate(double goalMeters, double currentMeters) { - if (controller.atGoal()) { - return 0.0; - } - - double output = controller.calculate(currentMeters, goalMeters); - - if (Math.abs(output) > deadband) { - output += config.getKSdrive() * Math.signum(output); - } - - output = - MathUtil.clamp( - output, - -config.getTranslationConstraints().maxVelocity, - config.getTranslationConstraints().maxVelocity); - - // SmartDashboard.putNumber("Y Controller Output", output); - // SmartDashboard.putBoolean("Y At Goal", controller.atGoal()); - // SmartDashboard.putNumber("Y Position Error", controller.getPositionError()); - // SmartDashboard.putNumber("Y Tolerance", controller.getPositionTolerance()); - return output; - } - - public boolean atGoal() { - return controller.atGoal(); - } - - public void reset(double currentMeters) { - controller.reset(currentMeters); - } - - public void updatePID(double kP, double kI, double kD) { - controller.setPID(kP, kI, kD); - } -} diff --git a/src/main/java/frc/robot/vision/Vision.java b/src/main/java/frc/robot/vision/Vision.java deleted file mode 100644 index 4aecc233..00000000 --- a/src/main/java/frc/robot/vision/Vision.java +++ /dev/null @@ -1,726 +0,0 @@ -package frc.robot.vision; - -import com.ctre.phoenix6.Utils; -import edu.wpi.first.apriltag.AprilTagFieldLayout; -import edu.wpi.first.apriltag.AprilTagFields; -import edu.wpi.first.math.Matrix; -import edu.wpi.first.math.VecBuilder; -import edu.wpi.first.math.geometry.Pose2d; -import edu.wpi.first.math.geometry.Pose3d; -import edu.wpi.first.math.geometry.Rotation2d; -import edu.wpi.first.math.geometry.Transform2d; -import edu.wpi.first.math.geometry.Translation2d; -import edu.wpi.first.math.kinematics.ChassisSpeeds; -import edu.wpi.first.math.numbers.N1; -import edu.wpi.first.math.numbers.N3; -import edu.wpi.first.math.util.Units; -import edu.wpi.first.wpilibj.DriverStation; -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.Subsystem; -import frc.rebuilt.FieldHelpers; -import frc.robot.Robot; -import frc.robot.RobotStates; -import frc.robot.auton.Auton; -import frc.spectrumLib.Telemetry; -import frc.spectrumLib.Telemetry.PrintPriority; -import frc.spectrumLib.util.Util; -import frc.spectrumLib.vision.Limelight; -import frc.spectrumLib.vision.Limelight.LimelightConfig; -import frc.spectrumLib.vision.LimelightHelpers; -import frc.spectrumLib.vision.LimelightHelpers.RawFiducial; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; -import lombok.Getter; - -public class Vision implements Subsystem { - - public static class VisionConfig { - @Getter final String name = "Vision"; - - /* Limelight Configuration */ - @Getter final String backLL = "limelight-back"; - - @Getter - final LimelightConfig backConfig = - new LimelightConfig(backLL) - .withTranslation(-0.3084987734, 0.2134100126, 0.6502249886) - .withRotation(0, 0, Math.toRadians(180)); - - @Getter final String leftLL = "limelight-left"; - - @Getter - final LimelightConfig leftConfig = - new LimelightConfig(leftLL) - .withTranslation(0, 0.215, 0.188) - .withRotation(0, 0, Math.toRadians(90)); - - @Getter final String rightLL = "limelight-right"; - - @Getter - final LimelightConfig rightConfig = - new LimelightConfig(rightLL) - .withTranslation(-0.04445, 0.3027487722, 0.7137249886) - .withRotation(0, 0, -90); - - @Getter - final Translation2d robotToTurretCenter = - new Translation2d(Units.inchesToMeters(-5.5), Units.inchesToMeters(4.7)); - - @Getter - final Translation2d turretCenterToCamera = - new Translation2d(Units.inchesToMeters(-5.641455), 0); - - /* Pipeline configs */ - @Getter final int backTagPipeline = 0; - @Getter final int leftTagPipeline = 0; - @Getter final int rightTagPipeline = 0; - - /* Pose Estimation Constants */ - @Getter double visionStdDevX = 0.5; - @Getter double visionStdDevY = 0.5; - @Getter double visionStdDevTheta = 0.2; - - @Getter - final double kLargeVariance = 999999.0; // Don't fuse rotation if variance exceeds this - - @Getter final double kMaxTimeDeltaSeconds = 0.1; // Max time difference to consider fusion - - @Getter - final Matrix visionStdMatrix = - VecBuilder.fill(visionStdDevX, visionStdDevY, visionStdDevTheta); - } - - /* Limelights */ - @Getter public final Limelight backLL; - @Getter public final Limelight leftLL; - @Getter public final Limelight rightLL; - - public final Limelight[] allLimelights; - - int[] blueTags = {18, 19, 20, 21, 24, 25, 26, 27}; - int[] redTags = {2, 3, 4, 5, 8, 9, 10, 11, 12}; - - @Getter private static AprilTagFieldLayout tagLayout; - - private VisionConfig config; - - // ----------------------------------------------------------------------- - // "Set once" / change-detection state - // ----------------------------------------------------------------------- - - /** The last IMU mode we set on each limelight. null means "unknown/not set". */ - private final java.util.IdentityHashMap lastImuModeByLL = - new java.util.IdentityHashMap<>(); - - public Vision(VisionConfig config) { - this.config = config; - - backLL = new Limelight(config.backLL, config.backTagPipeline, config.backConfig); - leftLL = new Limelight(config.leftLL, config.leftTagPipeline, config.leftConfig); - rightLL = new Limelight(config.rightLL, config.rightTagPipeline, config.rightConfig); - - allLimelights = new Limelight[] {backLL, leftLL, rightLL}; - - /* Configure Limelight Settings Here (initial set) */ - for (Limelight limelight : allLimelights) { - limelight.setLEDMode(false); - setImuModeIfChanged(limelight, 1); - } - - tagLayout = AprilTagFieldLayout.loadField(AprilTagFields.k2026RebuiltWelded); - - this.register(); - Telemetry.print(getName() + " Subsystem Initialized"); - } - - @Override - public String getName() { - return config.getName(); - } - - @Override - public void periodic() { - setLimeLightOrientation(); - disabledLimelightUpdates(); - enabledLimelightUpdates(); - - logTelemetry(); - } - - public void logTelemetry() { - if (Util.disabled.getAsBoolean()) { - Telemetry.log("Vision/BackLL-MT2", getBackMegaTag2Pose()); - Telemetry.log("Vision/LeftLL-MT2", getLeftMegaTag2Pose()); - Telemetry.log("Vision/RightLL-MT2", getRightMegaTag2Pose()); - } - Telemetry.log("Vision/BackLL-MT1", getBackMegaTag1Pose()); - Telemetry.log("Vision/LeftLL-MT1", getLeftMegaTag1Pose()); - Telemetry.log("Vision/RightLL-MT1", getRightMegaTag1Pose()); - Robot.getField2d().getObject(backLL.getCameraName()).setPose(getBackMegaTag1Pose()); - Robot.getField2d().getObject(leftLL.getCameraName()).setPose(getLeftMegaTag1Pose()); - Robot.getField2d().getObject(rightLL.getCameraName()).setPose(getRightMegaTag1Pose()); - } - - public void triggerRewindCaptureForAllCameras() { - for (Limelight limelight : allLimelights) { - LimelightHelpers.triggerRewindCapture(limelight.getName(), 165); - } - } - - public Pose2d getBackMegaTag1Pose() { - Pose2d pose = backLL.getMegaTag1_Pose3d().toPose2d(); - if (pose != null) { - return pose; - } - return Pose2d.kZero; - } - - public Pose2d getLeftMegaTag1Pose() { - Pose2d pose = leftLL.getMegaTag1_Pose3d().toPose2d(); - if (pose != null) { - return pose; - } - return Pose2d.kZero; - } - - public Pose2d getRightMegaTag1Pose() { - Pose2d pose = rightLL.getMegaTag1_Pose3d().toPose2d(); - if (pose != null) { - return pose; - } - return Pose2d.kZero; - } - - public Pose2d getBackMegaTag2Pose() { - Pose2d pose = backLL.getMegaTag2_Pose2d(); - if (pose != null) { - return pose; - } - return Pose2d.kZero; - } - - public Pose2d getLeftMegaTag2Pose() { - Pose2d pose = leftLL.getMegaTag2_Pose2d(); - if (pose != null) { - return pose; - } - return Pose2d.kZero; - } - - public Pose2d getRightMegaTag2Pose() { - Pose2d pose = rightLL.getMegaTag2_Pose2d(); - if (pose != null) { - return pose; - } - return Pose2d.kZero; - } - - private void setImuModeIfChanged(Limelight limelight, int desiredMode) { - Integer lastMode = lastImuModeByLL.get(limelight); - if (lastMode == null || lastMode.intValue() != desiredMode) { - limelight.setIMUmode(desiredMode); - lastImuModeByLL.put(limelight, desiredMode); - } - } - - private void setLimeLightOrientation() { - double yaw = Robot.getSwerve().getRobotPose().getRotation().getDegrees(); - for (Limelight limelight : allLimelights) { - limelight.setRobotOrientation(yaw); - } - } - - private void disabledLimelightUpdates() { - if (Util.disabled.getAsBoolean()) { - Limelight bestLimelight = getBestLimelight(); - integrateSingleEstimate(getMT1VisionEstimate(bestLimelight, true)); - integrateSingleEstimate(getMT2VisionEstimate(bestLimelight)); - } - } - - private void enabledLimelightUpdates() { - if (Util.teleop.getAsBoolean() - || RobotStates.autoUpdatePose.getAsBoolean() - || Auton.autonLaunching.getAsBoolean()) { - Limelight bestLimelight = getBestLimelight(); - integrateSingleEstimate(getMT1VisionEstimate(bestLimelight, false)); - } - } - - private VisionFieldPoseEstimate getMT1VisionEstimate(Limelight ll, boolean forceIntegrateXY) { - if (!ll.targetInView()) { - ll.setTagStatus("No Targets in View"); - ll.sendInvalidStatus("No Targets in View Rejection"); - return null; - } - - boolean multiTags = ll.multipleTagsInView(); - double targetSize = ll.getTargetSize(); - Pose3d megaTag1Pose3d = ll.getMegaTag1_Pose3d(); - Pose2d megaTag1Pose2d = megaTag1Pose3d.toPose2d(); - RawFiducial[] tags = ll.getRawFiducial(); - double highestAmbiguity = 2; - ChassisSpeeds robotSpeed = Robot.getSwerve().getCurrentRobotChassisSpeeds(); - double robotLinearSpeed = - Math.hypot(robotSpeed.vxMetersPerSecond, robotSpeed.vyMetersPerSecond); - - // distance from current pose to vision estimated MT1 pose - double mt1PoseDifference = - Robot.getSwerve() - .getRobotPose() - .getTranslation() - .getDistance(megaTag1Pose2d.getTranslation()); - - // ambiguity / basic rejections - ll.setTagStatus(""); - if (tags != null) { - for (RawFiducial tag : tags) { - if (highestAmbiguity == 2 || tag.ambiguity > highestAmbiguity) { - highestAmbiguity = tag.ambiguity; - } - if (tag.ambiguity > 0.9) { - // ambiguity too high -> reject - ll.sendInvalidStatus("High Ambiguity Rejection"); - return null; - } - } - } - - if (rejectionCheck(megaTag1Pose2d, targetSize)) { - return null; - } - - if (Math.abs(megaTag1Pose3d.getRotation().getX()) > 5 - || Math.abs(megaTag1Pose3d.getRotation().getY()) > 5) { - // reject if pose is tilted in roll or pitch - ll.sendInvalidStatus("Roll/Pitch Rejection"); - return null; - } - - // Determine std devs similar to the original logic - double xyStds; - double degStds; - - if (robotLinearSpeed <= 0.2 && targetSize > 4) { - ll.sendValidStatus("Stationary close integration"); - xyStds = 0.1; - degStds = 0.1; - } else if (multiTags && targetSize > 2) { - ll.sendValidStatus("Strong Multi integration"); - xyStds = 0.1; - degStds = 0.1; - } else if (multiTags && targetSize > 0.2) { - ll.sendValidStatus("Multi integration"); - xyStds = 0.25; - degStds = 8; - } else if (targetSize > 2 && (mt1PoseDifference < 0.5)) { - ll.sendValidStatus("Close integration"); - xyStds = 0.5; - degStds = config.getKLargeVariance(); - } else if (targetSize > 1 && (mt1PoseDifference < 0.25)) { - ll.sendValidStatus("Proximity integration"); - xyStds = 1.0; - degStds = config.getKLargeVariance(); - } else if (highestAmbiguity < 0.25 && targetSize >= 0.03) { - ll.sendValidStatus("Stable integration"); - xyStds = 1.5; - degStds = config.getKLargeVariance(); - } else { - // shouldn't integrate - ll.sendInvalidStatus("Integration Criteria not Met"); - return null; - } - - // Strict with degree std and ambiguity for MegaTag1 - if (highestAmbiguity > 0.5) { - degStds = 15; - } - - if (robotSpeed.omegaRadiansPerSecond >= 0.5) { - degStds = 50; - } - - // If we're forcing integration (e.g., for testing), use very tight stds - if (forceIntegrateXY) { - xyStds = 0.01; - degStds = 0.01; - } - - Pose2d integratedPose = - new Pose2d(megaTag1Pose2d.getTranslation(), megaTag1Pose2d.getRotation()); - - double timestamp = Utils.fpgaToCurrentTime(ll.getMegaTag1PoseTimestamp()); - Matrix stdDevs = VecBuilder.fill(xyStds, xyStds, degStds); - int numTags = tags == null ? 1 : tags.length; - - return new VisionFieldPoseEstimate(integratedPose, timestamp, stdDevs, numTags); - } - - private VisionFieldPoseEstimate getMT2VisionEstimate(Limelight ll) { - if (!ll.targetInView()) { - ll.setTagStatus("No Targets in View"); - ll.sendInvalidStatus("No Targets in View Rejection"); - return null; - } - - boolean multiTags = ll.multipleTagsInView(); - double targetSize = ll.getTargetSize(); - Pose2d megaTag2Pose2d = ll.getMegaTag2_Pose2d(); - ChassisSpeeds robotSpeed = Robot.getSwerve().getCurrentRobotChassisSpeeds(); - double robotLinearSpeed = - Math.hypot(robotSpeed.vxMetersPerSecond, robotSpeed.vyMetersPerSecond); - - double mt2PoseDifference = - Robot.getSwerve() - .getRobotPose() - .getTranslation() - .getDistance(megaTag2Pose2d.getTranslation()); - - /* rejections */ - if (rejectionCheck(megaTag2Pose2d, targetSize)) { - return null; - } - - /* Determine standard deviations */ - double xyStds; - - if (robotLinearSpeed <= 0.2 && targetSize > 4) { - ll.sendValidStatus("Stationary close integration"); - xyStds = 0.1; - } else if (multiTags && targetSize > 2) { - ll.sendValidStatus("Strong Multi integration"); - xyStds = 0.1; - } else if (multiTags && targetSize > 0.2) { - ll.sendValidStatus("Multi integration"); - xyStds = 0.25; - } else if (targetSize > 2 && (mt2PoseDifference < 0.5 || DriverStation.isDisabled())) { - ll.sendValidStatus("Close integration"); - xyStds = 0.5; - } else if (targetSize > 1 && (mt2PoseDifference < 0.25 || DriverStation.isDisabled())) { - ll.sendValidStatus("Proximity integration"); - xyStds = 1.0; - } else if (targetSize >= 0.03) { - ll.sendValidStatus("Stable integration"); - xyStds = 1.5; - } else { - return null; // Shouldn't integrate - } - - // MegaTag2 doesn't provide rotation, so use large variance - double degStds = config.getKLargeVariance(); - - Pose2d integratedPose = - new Pose2d(megaTag2Pose2d.getTranslation(), megaTag2Pose2d.getRotation()); - - return new VisionFieldPoseEstimate( - integratedPose, - Utils.fpgaToCurrentTime(ll.getMegaTag2PoseTimestamp()), - VecBuilder.fill(xyStds, xyStds, degStds), - (int) ll.getTagCountInView()); - } - - /** Helper to integrate a single estimate */ - private void integrateSingleEstimate(VisionFieldPoseEstimate estimate) { - if (estimate != null) { - Robot.getSwerve() - .addVisionMeasurement( - estimate.getVisionRobotPoseMeters(), - estimate.getTimestampSeconds(), - estimate.getVisionMeasurementStdDevs()); - } - } - - /** Helper to integrate multiple estimates close in time by fusing them together first */ - @SuppressWarnings("unused") - private void integrateMultipleEstimates(VisionFieldPoseEstimate... estimates) { - // Collect non-null estimates - List list = new ArrayList<>(); - for (VisionFieldPoseEstimate e : estimates) { - if (e != null) list.add(e); - } - if (list.isEmpty()) return; - - // Sort by timestamp ascending (old -> new). fuseEstimates expects to project - // older -> newer. - list.sort(Comparator.comparingDouble(VisionFieldPoseEstimate::getTimestampSeconds)); - - // Iteratively group/fuse estimates close in time. - VisionFieldPoseEstimate currentGroup = list.get(0); - for (int i = 1; i < list.size(); i++) { - VisionFieldPoseEstimate next = list.get(i); - double timeDelta = - Math.abs(next.getTimestampSeconds() - currentGroup.getTimestampSeconds()); - - if (timeDelta < config.getKMaxTimeDeltaSeconds()) { - // Fuse into current group (currentGroup older, next newer) - currentGroup = fuseEstimates(currentGroup, next); - } else { - // No close timestamp: integrate current group and start a new one - integrateSingleEstimate(currentGroup); - currentGroup = next; - } - } - // integrate the final fused group - integrateSingleEstimate(currentGroup); - } - - private boolean rejectionCheck(Pose2d pose, double targetSize) { - /* rejections */ - if (FieldHelpers.poseOutOfField(pose)) { - return true; - } - - if (Math.abs(Robot.getSwerve().getCurrentRobotChassisSpeeds().omegaRadiansPerSecond) - >= 1.6) { - return true; - } - - // Final check, if it's small reject, else return false and integrate - return targetSize <= 0.025; - } - - /** - * Choose the limelight with the best view of multiple tags - * - * @return the best limelight - */ - public Limelight getBestLimelight() { - Limelight bestLimelight = backLL; - double bestScore = 0; - for (Limelight limelight : allLimelights) { - double score = 0; - // prefer LL with most tags, when equal tag count, prefer LL closer to tags - score += limelight.getTagCountInView(); - score += limelight.getTargetSize(); - - if (score > bestScore) { - bestScore = score; - bestLimelight = limelight; - } - } - return bestLimelight; - } - - /** reset pose to the best limelight's vision pose */ - public void resetPoseToVision() { - Limelight ll = getBestLimelight(); - resetPoseToVision( - ll.targetInView(), - ll.getMegaTag1_Pose3d(), - ll.getMegaTag2_Pose2d(), - ll.getMegaTag1PoseTimestamp()); - } - - /** - * Set robot pose to vision pose only if LL has good tag reading - * - * @return if the pose was accepted and integrated - */ - public boolean resetPoseToVision( - boolean targetInView, Pose3d botpose3D, Pose2d megaPose, double poseTimestamp) { - - boolean reject = false; - if (targetInView) { - // replace botpose with this.pose - Pose2d botpose = botpose3D.toPose2d(); - Pose2d pose; - - // Check if the vision pose is bad and don't trust it - if (FieldHelpers.poseOutOfField(botpose3D)) { // pose out of field - Telemetry.log("Pose out of field", reject); - reject = true; - } else if (Math.abs(botpose3D.getZ()) > 0.25) { // when in air - Telemetry.log("Pose in air", reject); - reject = true; - } else if ((Math.abs(botpose3D.getRotation().getX()) > 5 - || Math.abs(botpose3D.getRotation().getY()) > 5)) { // when tilted - - Telemetry.log("Pose tilted", reject); - reject = true; - } - - // don't continue - if (reject) { - return !reject; // return the success status - } - - // Posts Current X,Y, and Angle (Theta) values - double[] visionPose = { - botpose.getX(), botpose.getY(), botpose.getRotation().getDegrees() - }; - Telemetry.log("Current Vision Pose: ", visionPose); - - Robot.getSwerve() - .setVisionMeasurementStdDevs(VecBuilder.fill(0.00001, 0.00001, 0.00001)); - - Pose2d integratedPose = new Pose2d(megaPose.getTranslation(), botpose.getRotation()); - Robot.getSwerve().addVisionMeasurement(integratedPose, poseTimestamp); - pose = Robot.getSwerve().getRobotPose(); - // Gets updated pose of x, y, and theta values - visionPose = new double[] {pose.getX(), pose.getY(), pose.getRotation().getDegrees()}; - Telemetry.log("Vision Pose Reset To: ", visionPose); - - // print "success" - return true; - } - return false; // target not in view - } - - /** - * If at least one LL has an accurate pose - * - * @return true if at least one LL has an accurate pose - */ - public boolean hasAccuratePose() { - for (Limelight limelight : allLimelights) { - if (limelight.hasAccuratePose()) return true; - } - return false; - } - - /** Change all LL pipelines to the same pipeline */ - public void setLimelightPipelines(int pipeline) { - for (Limelight limelight : allLimelights) { - limelight.setLimelightPipeline(pipeline); - } - } - - public boolean tagsInView() { - DriverStation.Alliance alliance = - DriverStation.getAlliance().orElse(DriverStation.Alliance.Red); - if (alliance == DriverStation.Alliance.Blue) { - return Arrays.stream(allLimelights) - .mapToInt(ll -> (int) ll.getClosestTagID()) - .anyMatch(id -> Arrays.stream(blueTags).anyMatch(tag -> tag == id)); - } else if (alliance == DriverStation.Alliance.Red) { - return Arrays.stream(allLimelights) - .mapToInt(ll -> (int) ll.getClosestTagID()) - .anyMatch(id -> Arrays.stream(redTags).anyMatch(tag -> tag == id)); - } else { - return false; - } - } - - // ------------------------------------------------------------------------------ - // VisionStates Commands - // ------------------------------------------------------------------------------ - - /** Set all Limelights to blink */ - public Command blinkLimelights() { - Telemetry.print("Vision.blinkLimelights", PrintPriority.HIGH); - return startEnd( - () -> { - for (Limelight limelight : allLimelights) { - limelight.blinkLEDs(); - } - }, - () -> { - for (Limelight limelight : allLimelights) { - limelight.setLEDMode(false); - } - }) - .withName("Vision.blinkLimelights"); - } - - /** Only blinks left limelight */ - public Command solidLimelight() { - return startEnd( - () -> { - for (Limelight limelight : allLimelights) { - limelight.setLEDMode(true); - ; - } - }, - () -> { - for (Limelight limelight : allLimelights) { - limelight.setLEDMode(false); - } - }) - .withName("Vision.solidLimelight"); - } - - /** Fuses two vision pose estimates using inverse-variance weighting. (FRC254 2025) */ - private VisionFieldPoseEstimate fuseEstimates( - VisionFieldPoseEstimate a, VisionFieldPoseEstimate b) { - // Ensure b is the newer measurement - if (b.getTimestampSeconds() < a.getTimestampSeconds()) { - VisionFieldPoseEstimate tmp = a; - a = b; - b = tmp; - } - - // Project both estimates to the same timestamp using odometry - Transform2d a_T_b = - Robot.getSwerve() - .getPoseAtTimestamp(b.getTimestampSeconds()) - .minus(Robot.getSwerve().getPoseAtTimestamp(a.getTimestampSeconds())); - - Pose2d poseA = a.getVisionRobotPoseMeters().transformBy(a_T_b); - Pose2d poseB = b.getVisionRobotPoseMeters(); - - // Inverse‑variance weighting - var varianceA = - a.getVisionMeasurementStdDevs().elementTimes(a.getVisionMeasurementStdDevs()); - var varianceB = - b.getVisionMeasurementStdDevs().elementTimes(b.getVisionMeasurementStdDevs()); - - Rotation2d fusedHeading = poseB.getRotation(); - if (varianceA.get(2, 0) < config.getKLargeVariance() - && varianceB.get(2, 0) < config.getKLargeVariance()) { - fusedHeading = - new Rotation2d( - poseA.getRotation().getCos() / varianceA.get(2, 0) - + poseB.getRotation().getCos() / varianceB.get(2, 0), - poseA.getRotation().getSin() / varianceA.get(2, 0) - + poseB.getRotation().getSin() / varianceB.get(2, 0)); - } - - double weightAx = 1.0 / varianceA.get(0, 0); - double weightAy = 1.0 / varianceA.get(1, 0); - double weightBx = 1.0 / varianceB.get(0, 0); - double weightBy = 1.0 / varianceB.get(1, 0); - - Pose2d fusedPose = - new Pose2d( - new Translation2d( - (poseA.getTranslation().getX() * weightAx - + poseB.getTranslation().getX() * weightBx) - / (weightAx + weightBx), - (poseA.getTranslation().getY() * weightAy - + poseB.getTranslation().getY() * weightBy) - / (weightAy + weightBy)), - fusedHeading); - - Matrix fusedStdDev = - VecBuilder.fill( - Math.sqrt(1.0 / (weightAx + weightBx)), - Math.sqrt(1.0 / (weightAy + weightBy)), - Math.sqrt(1.0 / (1.0 / varianceA.get(2, 0) + 1.0 / varianceB.get(2, 0)))); - - int numTags = a.getNumTags() + b.getNumTags(); - double time = b.getTimestampSeconds(); - - return new VisionFieldPoseEstimate(fusedPose, time, fusedStdDev, numTags); - } - - @Getter - public class VisionFieldPoseEstimate { - private final Pose2d visionRobotPoseMeters; - private final double timestampSeconds; - private final Matrix visionMeasurementStdDevs; - private final int numTags; - - public VisionFieldPoseEstimate( - Pose2d visionRobotPoseMeters, - double timestampSeconds, - Matrix visionMeasurementStdDevs, - int numTags) { - this.visionRobotPoseMeters = visionRobotPoseMeters; - this.timestampSeconds = timestampSeconds; - this.visionMeasurementStdDevs = visionMeasurementStdDevs; - this.numTags = numTags; - } - } -} diff --git a/src/main/java/frc/robot/vision/VisionStates.java b/src/main/java/frc/robot/vision/VisionStates.java deleted file mode 100644 index 19c62e3c..00000000 --- a/src/main/java/frc/robot/vision/VisionStates.java +++ /dev/null @@ -1,32 +0,0 @@ -package frc.robot.vision; - -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.button.Trigger; -import frc.robot.Robot; - -public class VisionStates { - - private static Vision vision = Robot.getVision(); - - public static final Trigger seeingTag = new Trigger(vision::tagsInView); - - public static void setupDefaultCommand() { - vision.setDefaultCommand(vision.blinkLimelights().withName("Vision.default")); - } - - public static void setStates() {} - - public static Command resetVisionPose() { - return vision.runOnce(vision::resetPoseToVision) - .withName("VisionStates.resetPoseToVision") - .ignoringDisable(true); - } - - public static Command blinkLimelights() { - return vision.blinkLimelights().withName("VisionStates.blinkLimelights"); - } - - public static Command solidLimelight() { - return vision.solidLimelight().withName("VisionCommands.solidLimelight"); - } -} diff --git a/src/main/java/frc/robot/vision/VisionSystem.java b/src/main/java/frc/robot/vision/VisionSystem.java deleted file mode 100644 index bfcdc5e0..00000000 --- a/src/main/java/frc/robot/vision/VisionSystem.java +++ /dev/null @@ -1,69 +0,0 @@ -// See: https://docs.photonvision.org/en/latest/docs/simulation/simulation.html -package frc.robot.vision; - -import edu.wpi.first.apriltag.AprilTagFieldLayout; -import edu.wpi.first.apriltag.AprilTagFields; -import edu.wpi.first.math.geometry.Pose2d; -import edu.wpi.first.math.geometry.Rotation3d; -import edu.wpi.first.math.geometry.Transform3d; -import edu.wpi.first.math.geometry.Translation3d; -import edu.wpi.first.wpilibj2.command.SubsystemBase; -import org.photonvision.PhotonCamera; -import org.photonvision.simulation.SimCameraProperties; -import org.photonvision.simulation.VisionSystemSim; - -public class VisionSystem extends SubsystemBase { - @SuppressWarnings("unused") - private final PhotonCamera camera = new PhotonCamera("cameraName"); - // private final PhotonCamera frontCam = new PhotonCamera(VisionConfig.FRONT_LL); - // private final PhotonCamera backCam = new PhotonCamera(VisionConfig.RIGHT_LL); - private final VisionSystemSim visionSim = new VisionSystemSim("main"); - private final Pose2dSupplier getSimPose; - - Transform3d robotToFrontCamera = - new Transform3d( - new Translation3d(0, 0, 0.5), // Centered on the robot, 0.5m up - new Rotation3d(0, Math.toRadians(-15), 0)); // Pitched 15 deg up - Transform3d robotToBackCamera = - new Transform3d( - new Translation3d(0, 0, 0.5), // Centered on the robot, 0.5m up - new Rotation3d(0, Math.toRadians(-15), 0)); // Pitched 15 deg up - - @FunctionalInterface - public interface Pose2dSupplier { - Pose2d getPose2d(); - } - - public VisionSystem(Pose2dSupplier getSimPose) { - this.getSimPose = getSimPose; - - // Setup simulated camera properties - SimCameraProperties props = new SimCameraProperties(); - props.setCalibError(0.25, 0.08); - props.setFPS(20.0); - props.setAvgLatencyMs(35.0); - props.setLatencyStdDevMs(5.0); - - // Setup simulated camera - // PhotonCameraSim cameraSimFront = new PhotonCameraSim(frontCam, props); - // PhotonCameraSim cameraSimBack = new PhotonCameraSim(backCam, props); - // Draw field wireframe in simulated camera view - // cameraSimFront.enableDrawWireframe(true); - // cameraSimBack.enableDrawWireframe(false); - - // // Add simulated camera to vision sim - // visionSim.addCamera(cameraSimFront, robotToFrontCamera); - // visionSim.addCamera(cameraSimBack, robotToBackCamera); - - // Add AprilTags to vision sim - AprilTagFieldLayout tagLayout = - AprilTagFieldLayout.loadField(AprilTagFields.k2026RebuiltWelded); - visionSim.addAprilTags(tagLayout); - } - - @Override - public void simulationPeriodic() { - // Update the vision system with the simulated robot pose - visionSim.update(getSimPose.getPose2d()); - } -} diff --git a/src/main/java/frc/spectrumLib/README.md b/src/main/java/frc/spectrumLib/README.md index bcc76e58..f5059d11 100644 --- a/src/main/java/frc/spectrumLib/README.md +++ b/src/main/java/frc/spectrumLib/README.md @@ -1,18 +1,157 @@ # SpectrumLib -Used for code that is shared between our robots. 3847, 8515, Flash, etc. Often code that we would want to use year to year. +Reusable robot library shared across Spectrum teams (3847, 8515, Flash, etc.). Contains framework abstractions, hardware wrappers, telemetry utilities, and simulation helpers designed to carry forward year-to-year. -## Description +--- -This should have any of our helper classes that we use year to year. This includes things like telemetry, default mechanisms classes, swerve templates, gamepad base class, etc. +## Package Structure -### Dependencies +``` +frc.spectrumLib +├── framework/ Base classes and interfaces for robot and subsystem architecture +├── hardware/ Hardware wrappers: TalonFX, CANcoder, servo, and RIO identity +├── telemetry/ Logging, tunable values, and battery usage tracking +├── util/ General-purpose utilities and math helpers +├── mechanism/ Abstract TalonFX-driven mechanism base class +├── gamepads/ Xbox controller abstraction with deadbanding and rumble +├── leds/ AddressableLED wrapper with built-in pattern library +├── sim/ Mechanism2d simulation helpers (arm, roller, linear) +├── swerve/ Swerve-specific utilities (SysID, Maple-Sim bridge) +└── vision/ Limelight helpers and vision logging +``` -* WPILib -* AdvantageKit -* PathPlanner -* CTRE Phoenix 6 +--- -### Installing +## Packages -* Copy this into your robot project +### `framework` +Core structural interfaces and base classes. + +| Class | Description | +|-------|-------------| +| `SpectrumRobot` | Extends `TimedRobot`; manages global `SpectrumSubsystem` registration and calls `setupStates()`/`setupDefaultCommand()` on all subsystems | +| `SpectrumSubsystem` | Interface extending WPILib `Subsystem`; requires `setupStates()` and `setupDefaultCommand()` | +| `SpectrumState` | Named boolean state backed by a WPILib `Trigger`; supports timed, toggled, and command-driven state transitions | + +--- + +### `hardware` +Low-level hardware wrappers and robot identity constants. + +| Class | Description | +|-------|-------------| +| `Rio` | Enum mapping RoboRIO serial numbers to robot identities; exposes `Rio.CANIVORE` and `Rio.RIO_CANBUS` bus name constants | +| `SpectrumCANcoder` | Configures a CANcoder and wires it into a `TalonFX` as Remote, Fused, or Sync feedback | +| `SpectrumCANcoderConfig` | Configuration holder for CANcoder offset, gear ratios, inversion, and attachment flag | +| `SpectrumServo` | PWM servo wrapper that also implements `Subsystem` | +| `TalonFXFactory` | Factory for creating `TalonFX` instances with consistent default configuration | + +--- + +### `telemetry` +Logging, alerts, and runtime-tunable values. + +| Class | Description | +|-------|-------------| +| `Telemetry` | DogLog-based logging system; provides `log()`, `print()`, alert monitoring, and a `tunable()` NetworkTables subscriber factory | +| `BatteryLogger` | Accumulates per-subsystem current/power/energy each loop and logs totals via `logPower()` | +| `TuneValue` | SmartDashboard-backed tunable `double` for in-match parameter adjustment | + +--- + +### `util` +General-purpose utilities, math, and data structures. + +| Class | Description | +|-------|-------------| +| `CachedDouble` | Wraps a `DoubleSupplier` and caches its value once per scheduler iteration | +| `CanDeviceId` | Typed CAN device identifier (device number + bus name) | +| `Conversions` | Unit conversion helpers (rotations ↔ inches, RPM ↔ RPS, etc.) | +| `CrashTracker` | Logs uncaught exceptions to a file on the RIO for post-match debugging | +| `Curve` / `ExpCurve` | Exponential input curve with deadband and scalar for joystick shaping | +| `Network` | NetworkTables helper utilities | +| `Trio` | Generic three-element tuple | +| `Util` | Miscellaneous utilities; exposes `Util.teleop`, `Util.autoMode`, `Util.disabled` triggers | +| `exceptions/KillRobotException` | Thrown to trigger a controlled robot shutdown on fatal errors | + +--- + +### `mechanism` +Abstract base class for all TalonFX-driven mechanisms. + +| Class | Description | +|-------|-------------| +| `Mechanism` | Manages motor construction, follower configuration, control requests (voltage, velocity, motion magic, torque-FOC), sensor reads, soft limits, current limits, and simulation hooks. All hardware access is gated by `isAttached()`. | + +Extend `Mechanism` and call its protected setters from command `execute()` bodies. Inner class `Mechanism.Config` holds all TalonFX configuration and PID/FF gains. + +--- + +### `gamepads` +Xbox controller abstraction. + +| Class | Description | +|-------|-------------| +| `Gamepad` | Abstract class wrapping `CommandXboxController`; provides deadbanded/curved axis reads, bumper/trigger modifier combos, stick direction helpers, alliance-aware cardinals, and a `rumbleCommand()` factory | + +--- + +### `leds` +AddressableLED subsystem with a pattern library. + +| Class | Description | +|-------|-------------| +| `SpectrumLEDs` | Manages an `AddressableLED` strip or view; provides `solid()`, `blink()`, `breathe()`, `rainbow()`, `chase()`, `wave()`, `bounce()`, `ombre()`, `countdown()`, and `stripe()` pattern factories | + +Supports multi-zone strips via `AddressableLEDBufferView` and a priority system to prevent low-priority commands from overriding higher-priority ones. + +--- + +### `sim` +Mechanism2d simulation helpers for visualizing robot mechanisms in DriverStation. + +| Class | Description | +|-------|-------------| +| `ArmSim` / `ArmConfig` | Simulates a rotating arm | +| `RollerSim` / `RollerConfig` | Simulates a spinning roller/wheel | +| `LinearSim` / `LinearConfig` | Simulates a linear extension | +| `Mount` / `Mountable` | Attachment point system for mounting sims onto other sims | +| `Circle` | Utility for drawing circular shapes in Mechanism2d | + +--- + +### `swerve` +Swerve-specific utilities. + +| Class | Description | +|-------|-------------| +| `MapleSimSwerveDrivetrain` | Maple-Sim simulation bridge for CTRE swerve | +| `SysID` | SysID characterization routine wrapper (translation, rotation, steer) | + +--- + +### `vision` +Limelight vision utilities. + +| Class | Description | +|-------|-------------| +| `Limelight` | Wrapper around `LimelightHelpers` with null-safe MegaTag1/MegaTag2 pose access, tag-count queries, and distance estimation | +| `LimelightHelpers` | Vendored Limelight utility library | +| `VisionLogger` | Logs vision pose estimates and tag data to telemetry | + +--- + +## Dependencies + +- [WPILib](https://github.com/wpilibsuite/allwpilib) +- [CTRE Phoenix 6](https://pro.docs.ctr-electronics.com/en/stable/) +- [DogLog](https://github.com/jonahsnider/doglog) +- [PathPlanner](https://github.com/mjansen4857/pathplanner) +- [Maple-Sim](https://github.com/Shenzhen-Robotics-Alliance/Maple-Sim) *(simulation only)* +- [Lombok](https://projectlombok.org/) *(compile-time `@Getter`/`@Setter` generation)* + +--- + +## Usage + +Copy the `spectrumLib` directory into your robot project under `src/main/java/frc/`. Extend `SpectrumRobot` as your robot base class, implement `SpectrumSubsystem` on each subsystem, and extend `Mechanism` for any TalonFX-driven mechanism. diff --git a/src/main/java/frc/spectrumLib/SpectrumCANcoderConfig.java b/src/main/java/frc/spectrumLib/SpectrumCANcoderConfig.java deleted file mode 100644 index 2964f1e6..00000000 --- a/src/main/java/frc/spectrumLib/SpectrumCANcoderConfig.java +++ /dev/null @@ -1,26 +0,0 @@ -package frc.spectrumLib; - -import lombok.Getter; -import lombok.Setter; - -public class SpectrumCANcoderConfig { - @Getter @Setter private int CANcoderID; - @Getter private double rotorToSensorRatio = 1; - @Getter private double sensorToMechanismRatio = 1; - @Getter private double offset = 0; - @Getter private boolean attached = false; - @Getter private boolean inverted = false; - - public SpectrumCANcoderConfig( - double rotorToSensorRatio, - double sensorToMechanismRatio, - double offset, - boolean attached, - boolean inverted) { - this.rotorToSensorRatio = rotorToSensorRatio; - this.sensorToMechanismRatio = sensorToMechanismRatio; - this.offset = offset; - this.attached = attached; - this.inverted = inverted; - } -} diff --git a/src/main/java/frc/spectrumLib/SpectrumServo.java b/src/main/java/frc/spectrumLib/SpectrumServo.java deleted file mode 100644 index 0d2faa66..00000000 --- a/src/main/java/frc/spectrumLib/SpectrumServo.java +++ /dev/null @@ -1,11 +0,0 @@ -package frc.spectrumLib; - -import edu.wpi.first.wpilibj.Servo; -import edu.wpi.first.wpilibj2.command.Subsystem; - -public class SpectrumServo extends Servo implements Subsystem { - - public SpectrumServo(int port) { - super(port); - } -} diff --git a/src/main/java/frc/spectrumLib/SpectrumSubsystem.java b/src/main/java/frc/spectrumLib/SpectrumSubsystem.java deleted file mode 100644 index b9ec7422..00000000 --- a/src/main/java/frc/spectrumLib/SpectrumSubsystem.java +++ /dev/null @@ -1,22 +0,0 @@ -package frc.spectrumLib; - -import edu.wpi.first.wpilibj2.command.Subsystem; - -/** - * The base interface for all Spectrum subsystems. Extends WPILib's Subsystem and adds common setup - * methods. - */ -public interface SpectrumSubsystem extends Subsystem { - - /** - * Set up the states and triggers for this subsystem. This is typically used to bind commands to - * SpectrumState triggers. - */ - void setupStates(); - - /** - * Set up the default command for this subsystem. This command will run when no other command is - * using this subsystem. - */ - void setupDefaultCommand(); -} diff --git a/src/main/java/frc/spectrumLib/TuneValue.java b/src/main/java/frc/spectrumLib/TuneValue.java deleted file mode 100644 index 8c16c0c1..00000000 --- a/src/main/java/frc/spectrumLib/TuneValue.java +++ /dev/null @@ -1,27 +0,0 @@ -package frc.spectrumLib; - -import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; -import java.util.function.DoubleSupplier; -import lombok.Getter; - -// Use this class to create a SmartDashboard tunable value -// You can put this in a Command to get the value from the SmartDashboard -public class TuneValue { - @Getter private double value; - @Getter private String name; - - public TuneValue(String name, double defaultValue) { - SmartDashboard.putNumber(name, defaultValue); - value = defaultValue; - this.name = name; - } - - public Double update() { - value = SmartDashboard.getNumber(name, value); - return value; - } - - public DoubleSupplier getSupplier() { - return this::update; - } -} diff --git a/src/main/java/frc/spectrumLib/SpectrumRobot.java b/src/main/java/frc/spectrumLib/framework/SpectrumRobot.java similarity index 51% rename from src/main/java/frc/spectrumLib/SpectrumRobot.java rename to src/main/java/frc/spectrumLib/framework/SpectrumRobot.java index 356c442d..99d7e38c 100644 --- a/src/main/java/frc/spectrumLib/SpectrumRobot.java +++ b/src/main/java/frc/spectrumLib/framework/SpectrumRobot.java @@ -1,4 +1,4 @@ -package frc.spectrumLib; +package frc.spectrumLib.framework; import edu.wpi.first.wpilibj.DriverStation; import edu.wpi.first.wpilibj.IterativeRobotBase; @@ -6,7 +6,6 @@ import edu.wpi.first.wpilibj.Watchdog; import edu.wpi.first.wpilibj2.command.CommandScheduler; import java.lang.reflect.Field; -import java.util.ArrayList; /** * The base robot class for Spectrum robots. Extends WPILib's TimedRobot and manages a collection of @@ -14,18 +13,10 @@ */ public class SpectrumRobot extends TimedRobot { - /** Create a single static instance of all of your subsystems */ - private static final ArrayList subsystems = new ArrayList<>(); - /** - * Add a subsystem to the global list of subsystems. - * - * @param subsystem The subsystem to add. + * Constructs a SpectrumRobot, silencing joystick connection warnings and extending the loop + * overrun watchdog timeout to 200 ms to accommodate longer periodic loops. */ - public static void add(SpectrumSubsystem subsystem) { - subsystems.add(subsystem); - } - public SpectrumRobot() { super(); DriverStation.silenceJoystickConnectionWarning(true); @@ -41,22 +32,4 @@ public SpectrumRobot() { } CommandScheduler.getInstance().setPeriod(0.20); } - - /** - * Set up default commands for all registered subsystems. Should be called during robot - * initialization. - */ - protected void setupDefaultCommands() { - // Setup Default Commands for all subsystems - subsystems.forEach(SpectrumSubsystem::setupDefaultCommand); - } - - /** - * Set up states and triggers for all registered subsystems. Should be called during robot - * initialization. - */ - protected void setupStates() { - // Bind Triggers for all subsystems - subsystems.forEach(SpectrumSubsystem::setupStates); - } } diff --git a/src/main/java/frc/spectrumLib/SpectrumState.java b/src/main/java/frc/spectrumLib/framework/SpectrumState.java similarity index 89% rename from src/main/java/frc/spectrumLib/SpectrumState.java rename to src/main/java/frc/spectrumLib/framework/SpectrumState.java index c79cbc6f..e347528c 100644 --- a/src/main/java/frc/spectrumLib/SpectrumState.java +++ b/src/main/java/frc/spectrumLib/framework/SpectrumState.java @@ -1,4 +1,4 @@ -package frc.spectrumLib; +package frc.spectrumLib.framework; import edu.wpi.first.wpilibj.Alert; import edu.wpi.first.wpilibj.Alert.AlertType; @@ -17,7 +17,9 @@ */ public class SpectrumState extends Trigger { + /** Shared map from state name to its current boolean value, polled by all Trigger instances. */ private static final HashMap stateConditions = new HashMap<>(); + private String name; private boolean value = false; private Alert alert; @@ -106,7 +108,7 @@ public Command setFalseForTime(DoubleSupplier time) { */ public Command setTrueForTimeWithCancel(DoubleSupplier time, Trigger cancelCondition) { return Commands.runOnce(() -> setState(true)) - .alongWith(new WaitCommand(time.getAsDouble()).onlyWhile(cancelCondition.not())) + .alongWith(new WaitCommand(time.getAsDouble()).onlyWhile(cancelCondition.negate())) .andThen( () -> { setState(false); @@ -129,10 +131,10 @@ public Command toggleToTrue() { } /** - * Command to set state to true, and then to false, ensuring your state will trigger change to - * false actions + * Command to set state to true, and then to false, ensuring triggers bound to the false + * transition fire reliably. * - * @return + * @return the command */ public Command toggleToFalse() { return setTrue() @@ -143,21 +145,38 @@ public Command toggleToFalse() { } /** - * @param value - * @return + * Creates an instant command that sets the state to the given value. + * + * @param value The desired state value + * @return the command */ public Command set(boolean value) { return Commands.runOnce(() -> setState(value)).ignoringDisable(true); } + /** + * Creates an instant command that sets the state to {@code true}. + * + * @return the command + */ public Command setTrue() { return set(true).withName(name + " state: SetTrue"); } + /** + * Creates an instant command that sets the state to {@code false}. + * + * @return the command + */ public Command setFalse() { return set(false).withName(name + " state: SetFalse"); } + /** + * Creates an instant command that flips the current state value. + * + * @return the command + */ public Command toggle() { return Commands.runOnce( () -> { diff --git a/src/main/java/frc/spectrumLib/gamepads/Gamepad.java b/src/main/java/frc/spectrumLib/gamepads/Gamepad.java index e48fbfb8..09d25f84 100644 --- a/src/main/java/frc/spectrumLib/gamepads/Gamepad.java +++ b/src/main/java/frc/spectrumLib/gamepads/Gamepad.java @@ -10,90 +10,226 @@ import edu.wpi.first.wpilibj2.command.CommandScheduler; import edu.wpi.first.wpilibj2.command.Commands; import edu.wpi.first.wpilibj2.command.InstantCommand; +import edu.wpi.first.wpilibj2.command.Subsystem; import edu.wpi.first.wpilibj2.command.button.CommandXboxController; import edu.wpi.first.wpilibj2.command.button.Trigger; -import frc.spectrumLib.SpectrumSubsystem; -import frc.spectrumLib.Telemetry; +import frc.spectrumLib.telemetry.Telemetry; import frc.spectrumLib.util.ExpCurve; import frc.spectrumLib.util.Util; import java.util.function.DoubleSupplier; import lombok.Getter; import lombok.Setter; +/** + * Abstract base class for robot gamepad (Xbox-compatible) controllers. + * + *

Wraps a WPILib {@link CommandXboxController} and exposes: + * + *

    + *
  • Pre-built {@link Trigger} fields for every button, bumper, trigger, stick-click, and D-pad + * direction. + *
  • Composite modifier triggers ({@link #noBumpers}, {@link #bothTriggers}, etc.) for + * chord-based bindings. + *
  • Exponential-curve axis helpers ({@link #leftStickCurve}, etc.) for driver-tuned response. + *
  • Stick-direction utilities ({@link #getLeftStickDirection()}, {@link + * #chooseCardinalDirections()}) for field-relative driving. + *
  • Rumble commands ({@link #rumbleCommand(double, double, double)}) for haptic feedback. + *
+ * + *

Subclass this once per operator role (pilot, copilot) and override {@link #setupStates()} and + * {@link #setupDefaultCommand()} to bind subsystem commands to triggers. + * + *

When {@link Config#isAttached()} returns {@code false}, all triggers remain permanently {@code + * false} and axis reads return {@code 0.0}. + */ // Gamepad class -public abstract class Gamepad implements SpectrumSubsystem { +public abstract class Gamepad implements Subsystem { + /** WPILib alert displayed on the driver station when this gamepad is disconnected. */ private Alert disconnectedAlert; + /** A trigger that is always {@code false}; used as a safe default before hardware is ready. */ public static final Trigger kFalse = new Trigger(() -> false); + /** The underlying WPILib Xbox controller used to read button and axis states. */ private CommandXboxController xboxController; + + /** Trigger for the A (cross) face button. */ protected Trigger A = kFalse; + + /** Trigger for the B (circle) face button. */ protected Trigger B = kFalse; + + /** Trigger for the X (square) face button. */ protected Trigger X = kFalse; + + /** Trigger for the Y (triangle) face button. */ protected Trigger Y = kFalse; + + /** Trigger for the left bumper (LB). */ protected Trigger leftBumper = kFalse; + + /** Trigger for the right bumper (RB). */ protected Trigger rightBumper = kFalse; + + /** Trigger active when the left analog trigger exceeds the configured deadzone threshold. */ protected Trigger leftTrigger = kFalse; + + /** Trigger active when the right analog trigger exceeds the configured deadzone threshold. */ protected Trigger rightTrigger = kFalse; + + /** Trigger for pressing the left analog stick (L3). */ protected Trigger leftStickClick = kFalse; + + /** Trigger for pressing the right analog stick (R3). */ protected Trigger rightStickClick = kFalse; + + /** Trigger for the Start / Menu button. */ protected Trigger start = kFalse; + + /** Trigger for the Select / Back / View button. */ protected Trigger select = kFalse; + + /** Trigger for D-pad up. */ protected Trigger upDpad = kFalse; + + /** Trigger for D-pad down. */ protected Trigger downDpad = kFalse; + + /** Trigger for D-pad left (including up-left and down-left diagonals). */ protected Trigger leftDpad = kFalse; + + /** Trigger for D-pad right (including up-right and down-right diagonals). */ protected Trigger rightDpad = kFalse; + + /** Trigger active when the left stick Y-axis exceeds the configured deadzone. */ protected Trigger leftStickY = kFalse; + + /** Trigger active when the left stick X-axis exceeds the configured deadzone. */ protected Trigger leftStickX = kFalse; + + /** Trigger active when the right stick Y-axis exceeds the configured deadzone. */ protected Trigger rightStickY = kFalse; + + /** Trigger active when the right stick X-axis exceeds the configured deadzone. */ protected Trigger rightStickX = kFalse; // Function bumper and trigger buttons + + /** Active when neither bumper is pressed. */ public Trigger noBumpers; + + /** Active when only the left bumper is pressed. */ public Trigger leftBumperOnly; + + /** Active when only the right bumper is pressed. */ public Trigger rightBumperOnly; + + /** Active when both bumpers are pressed simultaneously. */ public Trigger bothBumpers; + + /** Active when neither analog trigger is pressed. */ public Trigger noTriggers; + + /** Active when only the left trigger is pressed. */ public Trigger leftTriggerOnly; + + /** Active when only the right trigger is pressed. */ public Trigger rightTriggerOnly; + + /** Active when both analog triggers are pressed simultaneously. */ public Trigger bothTriggers; + + /** Active when no bumpers and no triggers are pressed (no modifier held). */ public Trigger noModifiers; + /** Most recently computed left-stick direction; retained when the stick returns to center. */ private Rotation2d storedLeftStickDirection = new Rotation2d(); + + /** Most recently computed right-stick direction; retained when the stick returns to center. */ private Rotation2d storedRightStickDirection = new Rotation2d(); + + /** + * {@code true} once the gamepad has been detected as connected and its triggers have been + * configured. + */ private boolean configured = false; // Used to determine if we detected the gamepad is plugged and we have configured // it + + /** {@code true} after the "gamepad not connected" warning has been printed once. */ private boolean printed = false; // Used to only print Gamepad Not Detected once + /** Exponential response curve applied to both left-stick axes. */ @Getter protected final ExpCurve leftStickCurve; + + /** Exponential response curve applied to both right-stick axes. */ @Getter protected final ExpCurve rightStickCurve; + + /** Exponential response curve applied to both analog trigger axes. */ @Getter protected final ExpCurve triggersCurve; + /** Trigger active during the teleoperated period. */ protected Trigger teleop = Util.teleop; + + /** Trigger active during the autonomous period. */ protected Trigger autoMode = Util.autoMode; + + /** Trigger active during the test mode period. */ protected Trigger testMode = Util.testMode; + + /** Trigger active while the robot is disabled. */ protected Trigger disabled = Util.disabled; + /** + * Configuration for a {@link Gamepad} instance, defining the DriverStation USB port, axis curve + * parameters, and whether the controller should be used on this robot. + */ public static class Config { + /** Human-readable controller name used in alerts and telemetry. */ @Getter private String name; + + /** USB port number as shown in the DriverStation application (0-indexed). */ @Getter private int port; // USB port on the DriverStation app // A configured value to say if we should use this controller on this robot + /** + * Whether this controller should be used on the current robot; {@code false} disables it. + */ @Getter @Setter private boolean attached; + /** Deadzone applied to both left-stick axes before the exponential curve. */ @Getter @Setter double leftStickDeadzone = 0.001; + + /** Exponent for the left-stick exponential response curve (1.0 = linear). */ @Getter @Setter double leftStickExp = 1.0; + + /** Output scalar applied after the left-stick exponential curve. */ @Getter @Setter double leftStickScalar = 1.0; + /** Deadzone applied to both right-stick axes before the exponential curve. */ @Getter @Setter double rightStickDeadzone = 0.001; + + /** Exponent for the right-stick exponential response curve. */ @Getter @Setter double rightStickExp = 1.0; + + /** Output scalar applied after the right-stick exponential curve. */ @Getter @Setter double rightStickScalar = 1.0; + /** Deadzone applied to both analog trigger axes before the exponential curve. */ @Getter @Setter double triggersDeadzone = 0.002; + + /** Exponent for the analog-trigger exponential response curve. */ @Getter @Setter double triggersExp = 1.0; + + /** Output scalar applied after the analog-trigger exponential curve. */ @Getter @Setter double triggersScalar = 1.0; + /** + * Creates a gamepad configuration for the given port. + * + * @param name human-readable controller name (used in alerts) + * @param port DriverStation USB port number (0-indexed) + */ public Config(String name, int port) { this.name = name; this.port = port; @@ -159,11 +295,13 @@ protected Gamepad(Config config) { leftDpad = xboxController .povLeft() - .or(xboxController.povUpLeft(), xboxController.povDownLeft()); + .or(xboxController.povUpLeft()) + .or(xboxController.povDownLeft()); rightDpad = xboxController .povRight() - .or(xboxController.povDownRight(), xboxController.povUpRight()); + .or(xboxController.povDownRight()) + .or(xboxController.povUpRight()); leftStickY = leftYTrigger(Threshold.ABS_GREATER, config.leftStickDeadzone); leftStickX = leftXTrigger(Threshold.ABS_GREATER, config.leftStickDeadzone); rightStickY = rightYTrigger(Threshold.ABS_GREATER, config.rightStickDeadzone); @@ -189,6 +327,11 @@ public void periodic() { configure(); } + /** + * Detects whether the gamepad has been connected since power-on and prints a one-time + * confirmation message. Also raises a {@link Alert} whenever the controller is disconnected. + * Called automatically by {@link #periodic()}. + */ // Configure the pilot controller public void configure() { if (config.isAttached()) { @@ -210,15 +353,23 @@ public void configure() { } } - // Reset the controller configure, should be used with - // CommandScheduler.getInstance.clearButtons() - // to reset buttons + /** + * Resets the controller configuration state so that the next {@link #configure()} call will + * re-detect connection and re-apply button bindings. Should be paired with {@code + * CommandScheduler.getInstance().clearButtons()}. + */ public void resetConfig() { configured = false; configure(); } - /* Zero is stick up, 90 is stick to the left*/ + /** + * Returns the current direction of the left stick as a {@link Rotation2d}. Zero points up + * (toward positive Y), and 90° points to the left (toward negative X). The last non-zero + * direction is retained when the stick is released. + * + * @return left-stick direction; zero-up / 90-left convention + */ public Rotation2d getLeftStickDirection() { double x = -1 * getLeftX(); double y = -1 * getLeftY(); @@ -229,6 +380,12 @@ public Rotation2d getLeftStickDirection() { return storedLeftStickDirection; } + /** + * Returns the current direction of the right stick as a {@link Rotation2d}. The last non-zero + * direction is retained when the stick is released. + * + * @return right-stick direction + */ public Rotation2d getRightStickDirection() { double x = getRightX(); double y = getRightY(); @@ -239,6 +396,11 @@ public Rotation2d getRightStickDirection() { return storedRightStickDirection; } + /** + * Snaps the left-stick direction to the nearest cardinal angle (0, ±π/2, π radians). + * + * @return the snapped angle in radians + */ public double getLeftStickCardinals() { double stickAngle = getLeftStickDirection().getRadians(); if (stickAngle > -Math.PI / 4 && stickAngle <= Math.PI / 4) { @@ -252,6 +414,11 @@ public double getLeftStickCardinals() { } } + /** + * Snaps the right-stick direction to the nearest cardinal angle (0, ±π/2, π radians). + * + * @return the snapped angle in radians + */ public double getRightStickCardinals() { double stickAngle = getRightStickDirection().getRadians(); if (stickAngle > -Math.PI / 4 && stickAngle <= Math.PI / 4) { @@ -265,12 +432,23 @@ public double getRightStickCardinals() { } } + /** + * Returns the Euclidean magnitude of the left stick deflection (0–√2 before curve, 0–1 after + * typical scalar). + * + * @return left-stick vector magnitude + */ public double getLeftStickMagnitude() { double x = -1 * getLeftX(); double y = -1 * getLeftY(); return Math.sqrt(x * x + y * y); } + /** + * Returns the Euclidean magnitude of the right stick deflection. + * + * @return right-stick vector magnitude + */ public double getRightStickMagnitude() { double x = getRightX(); double y = getRightY(); @@ -290,6 +468,12 @@ public double chooseCardinalDirections() { return getBlueAllianceStickCardinals(); } + /** + * Snaps the right stick to the nearest 45° increment using the Blue-alliance field orientation + * (forward = 0 rad). + * + * @return the snapped heading in radians for the Blue alliance perspective + */ public double getBlueAllianceStickCardinals() { double stickAngle = getRightStickDirection().getRadians(); if (stickAngle > -Math.PI / 8 && stickAngle <= Math.PI / 8) { @@ -342,27 +526,73 @@ else if (stickAngle < -Math.PI / 8 && stickAngle >= -3 * Math.PI / 8) { } } + /** + * Returns a {@link Trigger} that fires based on the left-stick Y axis and the given threshold + * comparison. + * + * @param t the {@link Threshold} comparison type + * @param threshold the value to compare against + * @return trigger based on the left Y axis + */ public Trigger leftYTrigger(Threshold t, double threshold) { return axisTrigger(t, threshold, this::getLeftY); } + /** + * Returns a {@link Trigger} that fires based on the left-stick X axis and the given threshold + * comparison. + * + * @param t the {@link Threshold} comparison type + * @param threshold the value to compare against + * @return trigger based on the left X axis + */ public Trigger leftXTrigger(Threshold t, double threshold) { return axisTrigger(t, threshold, this::getLeftX); } + /** + * Returns a {@link Trigger} that fires based on the right-stick Y axis and the given threshold + * comparison. + * + * @param t the {@link Threshold} comparison type + * @param threshold the value to compare against + * @return trigger based on the right Y axis + */ public Trigger rightYTrigger(Threshold t, double threshold) { return axisTrigger(t, threshold, this::getRightY); } + /** + * Returns a {@link Trigger} that fires based on the right-stick X axis and the given threshold + * comparison. + * + * @param t the {@link Threshold} comparison type + * @param threshold the value to compare against + * @return trigger based on the right X axis + */ public Trigger rightXTrigger(Threshold t, double threshold) { return axisTrigger(t, threshold, this::getRightX); } + /** + * Returns a {@link Trigger} that fires when either right-stick axis exceeds the given absolute + * threshold. + * + * @param threshold minimum absolute axis value to activate the trigger + * @return trigger active when the right stick is deflected beyond the threshold + */ public Trigger rightStick(double threshold) { return new Trigger( () -> Math.abs(getRightX()) >= threshold || Math.abs(getRightY()) >= threshold); } + /** + * Returns a {@link Trigger} that fires when either left-stick axis exceeds the given absolute + * threshold. + * + * @param threshold minimum absolute axis value to activate the trigger + * @return trigger active when the left stick is deflected beyond the threshold + */ public Trigger leftStick(double threshold) { return new Trigger( () -> Math.abs(getLeftX()) >= threshold || Math.abs(getLeftY()) >= threshold); @@ -385,9 +615,16 @@ private Trigger axisTrigger(Threshold t, double threshold, DoubleSupplier v) { }); } + /** + * Comparison type used by axis-based {@link Trigger} factories such as {@link + * #leftYTrigger(Threshold, double)}. + */ public enum Threshold { + /** Fires when the axis value is strictly greater than the threshold. */ GREATER, + /** Fires when the axis value is strictly less than the threshold. */ LESS, + /** Fires when the absolute axis value is greater than the threshold (deadband check). */ ABS_GREATER; } @@ -439,6 +676,11 @@ public Command rumbleCommand(Command command) { return command.alongWith(rumbleCommand(1, 0.5)).withName(command.getName()); } + /** + * Returns whether the physical gamepad is currently connected to the DriverStation. + * + * @return {@code true} if the controller is attached and reports as connected + */ public boolean isConnected() { if (config.attached) { return this.getHID().isConnected(); @@ -447,6 +689,11 @@ public boolean isConnected() { } } + /** + * Returns the raw right-trigger axis value (0–1), or {@code 0.0} if not connected. + * + * @return right-trigger axis value + */ protected double getRightTriggerAxis() { if (!isConnected()) { return 0.0; @@ -454,6 +701,11 @@ protected double getRightTriggerAxis() { return xboxController.getRightTriggerAxis(); } + /** + * Returns the raw left-trigger axis value (0–1), or {@code 0.0} if not connected. + * + * @return left-trigger axis value + */ protected double getLeftTriggerAxis() { if (!isConnected()) { return 0.0; @@ -461,6 +713,12 @@ protected double getLeftTriggerAxis() { return xboxController.getLeftTriggerAxis(); } + /** + * Returns the differential trigger value ({@code rightTrigger - leftTrigger}), useful as a + * single "twist" axis for field-relative rotation commands. + * + * @return twist value in the range [-1, 1] + */ protected double getTwist() { double right = getRightTriggerAxis(); double left = getLeftTriggerAxis(); @@ -468,6 +726,11 @@ protected double getTwist() { return value; } + /** + * Returns the left-stick X axis value, or {@code 0.0} if not connected. + * + * @return left X axis value in the range [-1, 1] + */ protected double getLeftX() { if (!isConnected()) { return 0.0; @@ -475,6 +738,11 @@ protected double getLeftX() { return xboxController.getLeftX(); } + /** + * Returns the left-stick Y axis value, or {@code 0.0} if not connected. + * + * @return left Y axis value in the range [-1, 1] (negative = up on most gamepads) + */ protected double getLeftY() { if (!isConnected()) { return 0.0; @@ -482,6 +750,11 @@ protected double getLeftY() { return xboxController.getLeftY(); } + /** + * Returns the right-stick X axis value, or {@code 0.0} if not connected. + * + * @return right X axis value in the range [-1, 1] + */ protected double getRightX() { if (!isConnected()) { return 0.0; @@ -489,6 +762,11 @@ protected double getRightX() { return xboxController.getRightX(); } + /** + * Returns the right-stick Y axis value, or {@code 0.0} if not connected. + * + * @return right Y axis value in the range [-1, 1] (negative = up on most gamepads) + */ protected double getRightY() { if (!isConnected()) { return 0.0; @@ -496,6 +774,12 @@ protected double getRightY() { return xboxController.getRightY(); } + /** + * Returns the underlying {@link GenericHID} for low-level access, or {@code null} if not + * attached. + * + * @return the raw HID device, or {@code null} + */ protected GenericHID getHID() { if (!config.attached) { return null; @@ -503,6 +787,12 @@ protected GenericHID getHID() { return xboxController.getHID(); } + /** + * Returns the underlying {@link GenericHID} for rumble output, or {@code null} if not + * connected. + * + * @return the raw HID device (only when connected), or {@code null} + */ protected GenericHID getRumbleHID() { if (!isConnected()) { return null; @@ -510,6 +800,13 @@ protected GenericHID getRumbleHID() { return xboxController.getHID(); } + /** + * Immediately sets the left and right rumble motor intensities. Use {@link + * #rumbleCommand(double, double, double)} for timed rumble sequences. + * + * @param leftIntensity left rumble motor intensity (0–1) + * @param rightIntensity right rumble motor intensity (0–1) + */ public void rumbleController(double leftIntensity, double rightIntensity) { if (!isConnected()) { return; diff --git a/src/main/java/frc/spectrumLib/Rio.java b/src/main/java/frc/spectrumLib/hardware/Rio.java similarity index 65% rename from src/main/java/frc/spectrumLib/Rio.java rename to src/main/java/frc/spectrumLib/hardware/Rio.java index f08662b8..66845ad4 100644 --- a/src/main/java/frc/spectrumLib/Rio.java +++ b/src/main/java/frc/spectrumLib/hardware/Rio.java @@ -1,29 +1,31 @@ -package frc.spectrumLib; +package frc.spectrumLib.hardware; import edu.wpi.first.wpilibj.Alert; import edu.wpi.first.wpilibj.Alert.AlertType; import edu.wpi.first.wpilibj.RobotBase; import edu.wpi.first.wpilibj.RobotController; +import frc.spectrumLib.telemetry.Telemetry; import java.util.HashMap; import java.util.Map; -/* - * Represents a specific RoboRIO, as a key for configurations. +/** + * Identifies the specific RoboRIO that is running, keyed by its serial number. Used to select + * robot-specific configurations at startup. * - * The serial numbers here can be found on the label on the back: add a leading zero. + *

Serial numbers are printed on the label on the back of the RoboRIO — prefix with a leading + * zero if needed. Keep entries in lexical order. Note that the serial number may change after + * reflashing the RoboRIO. * - * Please keep the ID strings in lexical order. - * - * Note that the ID string may change when you reflash the RoboRIO. - * Based on: https://github.com/Team100/all24/blob/2a109b28467cfddcafb93c7fc85ef60b56a628a2/lib/src/main/java/org/team100/lib/config/Identity.java + *

Based on: + * https://github.com/Team100/all24/blob/2a109b28467cfddcafb93c7fc85ef60b56a628a2/lib/src/main/java/org/team100/lib/config/Identity.java */ - public enum Rio { // 2026 Robots PHOTON2026("032B4BB3", true), PM_2026("0329AD07", true), - // FM_2026("", true), + FM_2026("", true), + OM_2026("", true), // 2025 Robots FM_2025("0329F2D1", true), @@ -34,6 +36,7 @@ public enum Rio { SIM("", true), // e.g. test default or simulation UNKNOWN(null, true); + /** Map from serial-number string to the corresponding {@link Rio} enum constant. */ private static final Map IDs = new HashMap<>(); static { @@ -46,9 +49,12 @@ public enum Rio { private static final Alert rioIdUnknown = new Alert("UNKNOWN RIO: ", AlertType.kError); private static final Alert rio1alert = new Alert("RIO 1.0", AlertType.kWarning); + /** The {@link Rio} constant that matches the hardware running this code. */ public static final Rio id = checkID(); - public static final String CANIVORE = "*"; // Use the first CANivore bus found + /** CANivore bus selector that chooses the first CANivore found on the system. */ + public static final String CANIVORE = "*"; + /** CAN bus name for the native RoboRIO CAN interface. */ public static final String RIO_CANBUS = "rio"; private final String serialNumber; @@ -89,6 +95,11 @@ private static Rio checkID() { return UNKNOWN; } + /** + * Returns {@code true} if this RoboRIO is a second-generation (RIO 2.0) controller. + * + * @return {@code true} for RIO 2.0, {@code false} for RIO 1.0 + */ public boolean isRio2() { return isRio2; } diff --git a/src/main/java/frc/spectrumLib/SpectrumCANcoder.java b/src/main/java/frc/spectrumLib/hardware/SpectrumCANcoder.java similarity index 61% rename from src/main/java/frc/spectrumLib/SpectrumCANcoder.java rename to src/main/java/frc/spectrumLib/hardware/SpectrumCANcoder.java index df4c85ba..fb6deac2 100644 --- a/src/main/java/frc/spectrumLib/SpectrumCANcoder.java +++ b/src/main/java/frc/spectrumLib/hardware/SpectrumCANcoder.java @@ -1,6 +1,5 @@ -package frc.spectrumLib; +package frc.spectrumLib.hardware; -import com.ctre.phoenix6.CANBus; import com.ctre.phoenix6.StatusCode; import com.ctre.phoenix6.configs.CANcoderConfiguration; import com.ctre.phoenix6.configs.TalonFXConfiguration; @@ -10,21 +9,41 @@ import com.ctre.phoenix6.signals.FeedbackSensorSourceValue; import com.ctre.phoenix6.signals.SensorDirectionValue; import frc.spectrumLib.mechanism.Mechanism.Config; +import frc.spectrumLib.telemetry.Telemetry; import lombok.Getter; +/** + * Wraps a CTRE CANcoder and applies Spectrum-specific configuration. On construction the encoder is + * configured and the supplied TalonFX motor's feedback source is updated to use it. + */ public class SpectrumCANcoder { + /** The underlying CTRE CANcoder hardware object. */ @Getter private CANcoder canCoder; + private SpectrumCANcoderConfig config; + /** Selects how the TalonFX reads position data from the remote CANcoder. */ public enum CANCoderFeedbackType { + /** Position is read remotely; motor encoder is used for velocity. */ RemoteCANcoder, + /** CANcoder position is fused with the motor encoder for high-bandwidth feedback. */ FusedCANcoder, + /** Motor encoder is synchronized to the CANcoder position on enable. */ SyncCANcoder, } private CANCoderFeedbackType feedbackSource = CANCoderFeedbackType.FusedCANcoder; + /** + * Creates and configures a SpectrumCANcoder, then updates the motor's feedback configuration. + * + * @param CANcoderID CAN device ID of the CANcoder + * @param config Configuration object containing offset, inversion, and ratio values + * @param motor The TalonFX whose feedback configuration will be updated + * @param mechConfig The mechanism configuration that holds the TalonFX config to modify + * @param feedbackSource How the TalonFX should read data from this CANcoder + */ public SpectrumCANcoder( int CANcoderID, SpectrumCANcoderConfig config, @@ -36,7 +55,8 @@ public SpectrumCANcoder( this.feedbackSource = feedbackSource; if (config.isAttached()) { - canCoder = new CANcoder(CANcoderID, new CANBus(Rio.CANIVORE)); + // Fused/Sync feedback requires the CANcoder to be on the same bus as the motor. + canCoder = new CANcoder(CANcoderID, motor.getNetwork()); CANcoderConfiguration canCoderConfigs = new CANcoderConfiguration(); canCoderConfigs.MagnetSensor.MagnetOffset = config.getOffset(); canCoderConfigs.MagnetSensor.SensorDirection = @@ -51,10 +71,24 @@ public SpectrumCANcoder( } } + /** + * Returns whether this CANcoder is configured as physically present on the robot. + * + * @return {@code true} if the CANcoder is attached + */ public boolean isAttached() { return config.isAttached(); } + /** + * Updates the TalonFX feedback configuration to reference this CANcoder using the chosen + * feedback source type and the ratios defined in the config. + * + * @param motor The TalonFX motor to reconfigure + * @param mechConfig The mechanism configuration whose stored TalonFX config is modified in + * place + * @return this instance, for chaining + */ public SpectrumCANcoder modifyMotorConfig(TalonFX motor, Config mechConfig) { TalonFXConfigurator configurator = motor.getConfigurator(); TalonFXConfiguration talonConfigMod = mechConfig.getTalonConfig(); @@ -80,6 +114,13 @@ public SpectrumCANcoder modifyMotorConfig(TalonFX motor, Config mechConfig) { return this; } + /** + * Checks whether a CANcoder configuration response indicates success. Prints a warning via + * {@link Telemetry} if the response is not OK. + * + * @param response The {@link StatusCode} returned by the CANcoder configurator + * @return {@code true} if the response is OK, {@code false} otherwise + */ public boolean canCoderResponseOK(StatusCode response) { if (!response.isOK()) { Telemetry.print( diff --git a/src/main/java/frc/spectrumLib/hardware/SpectrumCANcoderConfig.java b/src/main/java/frc/spectrumLib/hardware/SpectrumCANcoderConfig.java new file mode 100644 index 00000000..c20a6bd4 --- /dev/null +++ b/src/main/java/frc/spectrumLib/hardware/SpectrumCANcoderConfig.java @@ -0,0 +1,45 @@ +package frc.spectrumLib.hardware; + +import lombok.Getter; +import lombok.Setter; + +/** Configuration parameters for a {@link SpectrumCANcoder}. */ +public class SpectrumCANcoderConfig { + /** CAN device ID of the CANcoder; may be set after construction. */ + @Getter @Setter private int CANcoderID; + /** Gear ratio between the motor rotor and the CANcoder shaft (rotor turns / sensor turn). */ + @Getter private double rotorToSensorRatio = 1; + /** + * Gear ratio between the CANcoder shaft and the mechanism output (sensor turns / mechanism + * turn). + */ + @Getter private double sensorToMechanismRatio = 1; + /** Magnetic offset applied to the CANcoder reading, in rotations. */ + @Getter private double offset = 0; + /** Whether the CANcoder hardware is physically present on the robot. */ + @Getter private boolean attached = false; + /** Whether the CANcoder sensor direction is inverted (clockwise positive). */ + @Getter private boolean inverted = false; + + /** + * Creates a fully-specified CANcoder configuration. + * + * @param rotorToSensorRatio Gear ratio from motor rotor to CANcoder shaft + * @param sensorToMechanismRatio Gear ratio from CANcoder shaft to mechanism output + * @param offset Magnetic offset in rotations + * @param attached {@code true} if the CANcoder is physically installed + * @param inverted {@code true} to make clockwise rotation positive + */ + public SpectrumCANcoderConfig( + double rotorToSensorRatio, + double sensorToMechanismRatio, + double offset, + boolean attached, + boolean inverted) { + this.rotorToSensorRatio = rotorToSensorRatio; + this.sensorToMechanismRatio = sensorToMechanismRatio; + this.offset = offset; + this.attached = attached; + this.inverted = inverted; + } +} diff --git a/src/main/java/frc/spectrumLib/hardware/SpectrumServo.java b/src/main/java/frc/spectrumLib/hardware/SpectrumServo.java new file mode 100644 index 00000000..649d2e57 --- /dev/null +++ b/src/main/java/frc/spectrumLib/hardware/SpectrumServo.java @@ -0,0 +1,20 @@ +package frc.spectrumLib.hardware; + +import edu.wpi.first.wpilibj.Servo; +import edu.wpi.first.wpilibj2.command.Subsystem; + +/** + * A WPILib {@link Servo} that also implements {@link Subsystem}, allowing it to be registered with + * the command-based framework and participate in requirement checking. + */ +public class SpectrumServo extends Servo implements Subsystem { + + /** + * Creates a SpectrumServo connected to the given PWM port on the RoboRIO. + * + * @param port PWM channel (0–9) the servo signal wire is plugged into + */ + public SpectrumServo(int port) { + super(port); + } +} diff --git a/src/main/java/frc/spectrumLib/talonFX/TalonFXFactory.java b/src/main/java/frc/spectrumLib/hardware/TalonFXFactory.java similarity index 84% rename from src/main/java/frc/spectrumLib/talonFX/TalonFXFactory.java rename to src/main/java/frc/spectrumLib/hardware/TalonFXFactory.java index ad04945c..7c53e857 100644 --- a/src/main/java/frc/spectrumLib/talonFX/TalonFXFactory.java +++ b/src/main/java/frc/spectrumLib/hardware/TalonFXFactory.java @@ -1,4 +1,4 @@ -package frc.spectrumLib.talonFX; +package frc.spectrumLib.hardware; import com.ctre.phoenix6.CANBus; import com.ctre.phoenix6.configs.TalonFXConfiguration; @@ -26,15 +26,28 @@ public class TalonFXFactory { private static double neutralDeadband = 0.04; private static double supplyCurrentLimit = 40; + /** Utility class — not instantiable. */ private TalonFXFactory() {} - // create a CANTalon with the default (out of the box) configuration + /** + * Creates a TalonFX configured with Spectrum's default parameter set. + * + * @param id CAN device identifier (device number + bus name) + * @return the configured TalonFX + */ public static TalonFX createDefaultTalon(CanDeviceId id) { var talon = createTalon(id); talon.getConfigurator().apply(getDefaultConfig()); return talon; } + /** + * Creates a TalonFX and applies the supplied configuration. + * + * @param id CAN device identifier + * @param config The {@link TalonFXConfiguration} to apply + * @return the configured TalonFX + */ public static TalonFX createConfigTalon(CanDeviceId id, TalonFXConfiguration config) { var talon = createTalon(id); talon.getConfigurator().apply(config); @@ -68,6 +81,13 @@ public static TalonFX createPermanentFollowerTalon( return talon; } + /** + * Builds a {@link TalonFXConfiguration} populated with Spectrum's standard defaults: brake + * neutral mode, counter-clockwise positive invert, 4 % duty-cycle deadband, 40 A supply current + * limit, software and hardware limits disabled, rotor sensor feedback, and audio cues enabled. + * + * @return a new configuration object with default values applied + */ public static TalonFXConfiguration getDefaultConfig() { TalonFXConfiguration config = new TalonFXConfiguration(); diff --git a/src/main/java/frc/spectrumLib/leds/SpectrumLEDs.java b/src/main/java/frc/spectrumLib/leds/SpectrumLEDs.java index ac3d4e94..4325a51f 100644 --- a/src/main/java/frc/spectrumLib/leds/SpectrumLEDs.java +++ b/src/main/java/frc/spectrumLib/leds/SpectrumLEDs.java @@ -2,515 +2,889 @@ import static edu.wpi.first.units.Units.*; -import edu.wpi.first.units.measure.*; -import edu.wpi.first.wpilibj.AddressableLED; -import edu.wpi.first.wpilibj.AddressableLEDBuffer; -import edu.wpi.first.wpilibj.AddressableLEDBufferView; -import edu.wpi.first.wpilibj.LEDPattern; -import edu.wpi.first.wpilibj.LEDPattern.GradientType; -import edu.wpi.first.wpilibj.LEDReader; -import edu.wpi.first.wpilibj.LEDWriter; +import com.ctre.phoenix6.CANBus; +import com.ctre.phoenix6.configs.CANdleConfiguration; +import com.ctre.phoenix6.configs.LEDConfigs; +import com.ctre.phoenix6.controls.ColorFlowAnimation; +import com.ctre.phoenix6.controls.EmptyAnimation; +import com.ctre.phoenix6.controls.FireAnimation; +import com.ctre.phoenix6.controls.LarsonAnimation; +import com.ctre.phoenix6.controls.RainbowAnimation; +import com.ctre.phoenix6.controls.RgbFadeAnimation; +import com.ctre.phoenix6.controls.SingleFadeAnimation; +import com.ctre.phoenix6.controls.SolidColor; +import com.ctre.phoenix6.controls.StrobeAnimation; +import com.ctre.phoenix6.hardware.CANdle; +import com.ctre.phoenix6.signals.LarsonBounceValue; +import com.ctre.phoenix6.signals.LossOfSignalBehaviorValue; +import com.ctre.phoenix6.signals.RGBWColor; +import com.ctre.phoenix6.signals.StripTypeValue; import edu.wpi.first.wpilibj.Timer; import edu.wpi.first.wpilibj.util.Color; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.CommandScheduler; +import edu.wpi.first.wpilibj2.command.Subsystem; import edu.wpi.first.wpilibj2.command.button.Trigger; -import frc.spectrumLib.SpectrumRobot; -import frc.spectrumLib.SpectrumSubsystem; -import java.util.Map; import java.util.function.DoubleSupplier; import lombok.Getter; import lombok.Setter; -public class SpectrumLEDs implements SpectrumSubsystem { +/** + * CANdle-based addressable LED subsystem that wraps a CTRE {@link CANdle} and exposes a rich + * library of pattern factories (solid, stripe, blink, breathe, rainbow, chase, bounce, gradient, + * ombre, wave, countdown, etc.). + * + *

Patterns are split into two categories: + * + *

    + *
  • Hardware animation patterns ({@link #blink}, {@link #breathe}, {@link #rainbow}, + * {@link #scrollingRainbow}, {@link #chase}, {@link #bounce}, {@link #fire}, {@link + * #rgbCycle}) — these use {@code setControl()} with CANdle's built-in animation engine. They + * run autonomously in firmware and require no per-loop CPU work. + *
  • Software patterns ({@link #solid}, {@link #stripe}, {@link #gradient}, {@link + * #ombre}, {@link #wave}, {@link #countdown}, {@link #switchCountdown}, {@link #edges}) — + * these use {@code setControl(SolidColor)} which is a one-shot command resent each loop. When + * switching from a hardware animation to a software pattern, all animations are automatically + * cleared. + *
+ * + *

Multiple {@code SpectrumLEDs} instances can share a single physical {@link CANdle} device by + * passing the same {@link CANdle} reference in their {@link Config} objects and selecting + * non-overlapping {@code startIdx}/{@code numLeds} ranges. + * + *

Patterns are applied via {@link #setPattern(CANdlePattern, int)}, which returns a {@link + * Command} that runs continuously and respects the priority system ({@link #checkPriority(int)}). + */ +public class SpectrumLEDs implements Subsystem { + + // ------------------------------------------------------------------------- + // CANdlePattern functional interface + // ------------------------------------------------------------------------- - // Example Animation List - https://github.com/Aircoookie/WLED/wiki/List-of-effects-and-palettes + /** + * Functional interface for a LED pattern that drives a segment of a {@link CANdle} strip. + * + *

Implementations receive the live {@link CANdle} device, the first LED index in this + * instance's segment ({@code startIdx}), and the number of LEDs in the segment ({@code + * numLeds}). Hardware animation patterns call {@link CANdle#setControl}, software patterns call + * {@link CANdle#setControl(SolidColor)} (one-shot, resent each loop). + */ + @FunctionalInterface + public interface CANdlePattern { + /** + * Apply this pattern to the given LED segment. + * + * @param candle the {@link CANdle} device to write to + * @param startIdx the first LED index (inclusive); {@code 0} includes the 8 onboard LEDs + * @param numLeds the number of LEDs in this segment + */ + void applyTo(CANdle candle, int startIdx, int numLeds); + } + + /** + * Internal marker wrapper — returned by animation factory methods so that {@link + * #setPattern(CANdlePattern, int)} can detect when a transition from animation to software + * pattern occurs and clear the animation slots. + */ + private static final class HardwareAnimPattern implements CANdlePattern { + private final CANdlePattern impl; + + HardwareAnimPattern(CANdlePattern impl) { + this.impl = impl; + } + @Override + public void applyTo(CANdle candle, int startIdx, int numLeds) { + impl.applyTo(candle, startIdx, numLeds); + } + } + + /** Wraps a pattern lambda in a {@link HardwareAnimPattern} marker. */ + private static CANdlePattern hardwareAnim(CANdlePattern p) { + return new HardwareAnimPattern(p); + } + + // ------------------------------------------------------------------------- + // Config + // ------------------------------------------------------------------------- + + /** + * Configuration for a {@link SpectrumLEDs} subsystem instance. + * + *

Use {@link #Config(String, int, int, CANBus)} to create a standalone instance that owns + * and configures its {@link CANdle}, or {@link #Config(String, CANdle, int, int)} to share an + * already-configured device across multiple subsystems targeting different LED segments. + */ public static class Config { + /** Human-readable name used in telemetry. */ @Getter private String name; + + /** Whether this LED strip is physically connected to the robot. */ @Getter @Setter private boolean attached = true; - @Getter @Setter private AddressableLED led; - @Getter @Setter private AddressableLEDBuffer buffer; - @Getter @Setter private AddressableLEDBufferView view; - @Getter @Setter private int startingIndex = 0; - @Getter @Setter private int endingIndex = 0; - @Getter @Setter private int port = 0; - @Getter @Setter private int length; - // LED strip density - @Getter @Setter private Distance ledSpacing = Meters.of(1 / 120.0); - - public Config(String name, int length) { + + /** + * Pre-built {@link CANdle} to reuse. When non-null, {@link #deviceId} and {@link #canBus} + * are ignored and no hardware configuration is applied by this instance. + */ + @Getter @Setter private CANdle sharedCandle = null; + + /** CAN device ID used when no {@link #sharedCandle} is provided. */ + @Getter @Setter private int deviceId = 1; + + /** CAN bus used when no {@link #sharedCandle} is provided. */ + @Getter @Setter private CANBus canBus; + + /** + * First LED index (inclusive) in the strip. Use {@code 0} to include the 8 onboard status + * LEDs on the CANdle board itself; use {@code 8} to skip them. + */ + @Getter @Setter private int startIdx = 0; + + /** Number of LEDs in the segment owned by this instance. */ + @Getter @Setter private int numLeds; + + /** + * CANdle hardware animation slot (0–7) used by this instance's animation patterns. Each + * {@link SpectrumLEDs} instance sharing a single {@link CANdle} must use a distinct slot, + * otherwise their animations overwrite each other. + */ + @Getter @Setter private int animationSlot = 0; + + /** LED strip type (RGB, RGBW, GRB, etc.). Ignored when {@link #sharedCandle} is set. */ + @Getter @Setter private StripTypeValue stripType = StripTypeValue.RGB; + + /** + * Overall brightness scalar applied in hardware (0.0–1.0). Ignored when {@link + * #sharedCandle} is set. + */ + @Getter @Setter private double brightness = 1.0; + + /** + * Behavior of the strip when CAN signal is lost. Ignored when {@link #sharedCandle} is set. + */ + @Getter @Setter + private LossOfSignalBehaviorValue lossOfSignalBehavior = + LossOfSignalBehaviorValue.DisableLEDs; + + /** + * Creates a configuration for a standalone LED instance that creates and owns its own + * {@link CANdle}. Hardware configuration (strip type, brightness, loss-of-signal) is + * applied automatically in the constructor. + * + * @param name human-readable name for telemetry + * @param deviceId CAN device ID of the CANdle + * @param numLeds number of LEDs on the external strip (not counting the 8 onboard LEDs) + * @param canBus CAN bus the CANdle is on + */ + public Config(String name, int deviceId, int numLeds, CANBus canBus) { this.name = name; - this.length = length; - this.startingIndex = 0; - this.endingIndex = length - 1; + this.deviceId = deviceId; + this.numLeds = numLeds; + this.canBus = canBus; } - public Config( - String name, - AddressableLED l, - AddressableLEDBuffer lb, - int startingIndex, - int endingIndex) { + /** + * Creates a configuration for a LED zone that shares an existing, already-configured {@link + * CANdle} device. No hardware configuration is applied. + * + * @param name human-readable name for telemetry + * @param sharedCandle the {@link CANdle} to reuse + * @param startIdx first LED index (inclusive) in the shared strip for this zone + * @param numLeds number of LEDs in this zone + */ + public Config(String name, CANdle sharedCandle, int startIdx, int numLeds) { this.name = name; - this.led = l; - this.buffer = lb; - this.startingIndex = startingIndex; - this.endingIndex = endingIndex; + this.sharedCandle = sharedCandle; + this.startIdx = startIdx; + this.numLeds = numLeds; } } + // ------------------------------------------------------------------------- + // Fields + // ------------------------------------------------------------------------- + + /** Active configuration for this instance. */ @Getter private Config config; - @Getter protected final AddressableLED led; - @Getter protected final AddressableLEDBuffer ledBuffer; - @Getter protected final AddressableLEDBufferView ledView; - private boolean mainView = false; + /** The {@link CANdle} device (owned or shared). */ + @Getter protected final CANdle candle; - protected final LEDPattern defaultPattern = blink(Color.kOrange, 1); + /** {@code true} if the most recently applied pattern was a hardware animation. */ + private boolean lastWasAnimation = false; - @Getter - protected Command defaultCommand = - setPattern(defaultPattern, -1).withName("LEDs.defaultCommand"); + /** + * Default pattern shown when no other command requires this subsystem (orange blink). + * + *

Initialized in the constructor body (after {@link #config} is set) so that pattern + * factories can safely reference the config. + */ + protected final CANdlePattern defaultPattern; - public final Trigger defaultTrigger = new Trigger(() -> defaultCommand.isScheduled()); + /** + * The default command built by this class (displays {@link #defaultPattern} at lowest + * priority). Installed via {@code setDefaultCommand} in the constructor; subclasses may install + * their own default command to replace it. Use {@code getDefaultCommand()} (from {@link + * Subsystem}) to query whichever default is currently installed. + */ + protected final Command defaultCommand; - @Getter @Setter private int commandPriority = 0; + /** + * Trigger that is active while the currently installed default command (whichever one that is) + * is the one running — i.e. no higher-priority pattern owns the subsystem. + */ + public final Trigger defaultTrigger; + + /** + * Priority level of the pattern command currently running. Higher values indicate higher + * priority; {@link #setPattern(CANdlePattern, int)} stores this while a command runs and resets + * it to {@code -1} when the command ends. + */ + @Getter @Setter private int commandPriority = -1; + /** Spectrum purple color constant ({@code RGB 130, 103, 185}). */ public final Color purple = new Color(130, 103, 185); + + /** Convenience alias for {@link Color#kWhite}. */ public final Color white = Color.kWhite; + // ------------------------------------------------------------------------- + // Constructor + // ------------------------------------------------------------------------- + + /** + * Constructs the LED subsystem, configures the hardware (or reuses a shared device), and + * registers with the WPILib {@link CommandScheduler}. + * + * @param config the configuration describing the device, segment range, and strip type + */ public SpectrumLEDs(Config config) { this.config = config; - // Must be a PWM header, not MXP or DIO - if (config.getLed() == null) { - led = new AddressableLED(config.port); - // Length is expensive to set, so only set it once, then just update data - ledBuffer = new AddressableLEDBuffer(config.length); - led.setLength(ledBuffer.getLength()); - mainView = true; + if (config.getSharedCandle() != null) { + candle = config.getSharedCandle(); } else { - led = config.getLed(); - ledBuffer = config.buffer; + candle = new CANdle(config.getDeviceId(), config.getCanBus()); + CANdleConfiguration candleConfig = + new CANdleConfiguration() + .withLED( + new LEDConfigs() + .withStripType(config.getStripType()) + .withBrightnessScalar(config.getBrightness()) + .withLossOfSignalBehavior( + config.getLossOfSignalBehavior())); + candle.getConfigurator().apply(candleConfig); } - ledView = ledBuffer.createView(config.startingIndex, config.endingIndex); - - // Set the data - led.setData(ledBuffer); - setPattern(defaultPattern); - led.start(); + // Pattern fields are initialized here (after config is set) so factory methods + // can safely read config values. + defaultPattern = blink(Color.kOrange, 1.0); + defaultCommand = setPattern(defaultPattern, -1).withName("LEDs.defaultCommand"); + setDefaultCommand(defaultCommand); + defaultTrigger = + new Trigger( + () -> { + Command current = getCurrentCommand(); + return current != null && current == getDefaultCommand(); + }); - SpectrumRobot.add(this); CommandScheduler.getInstance().registerSubsystem(this); } - @Override - public void periodic() { - // Set the LEDs only if this is the main view - if (mainView) { - led.setData(ledBuffer); - } - } + // ------------------------------------------------------------------------- + // Subsystem API + // ------------------------------------------------------------------------- + /** + * Returns whether this LED strip is physically connected to the robot. + * + * @return {@code true} if attached + */ public boolean isAttached() { return config.isAttached(); } + /** + * Returns {@code true} if the most recently applied pattern was a CANdle hardware animation + * running in firmware, {@code false} if it was a software {@link SolidColor} pattern. + */ + public boolean isAnimating() { + return lastWasAnimation; + } + + /** + * Returns the currently running command that is applying a pattern to this subsystem, or {@code + * null} if no command is currently running. + * + * @return the currently running command, or {@code null} if none + */ + public String getCurrentCommandName() { + Command cmd = getCurrentCommand(); + return cmd != null ? cmd.getName() : "None"; + } + + /** + * Returns a {@link Trigger} that is active when the currently running command's priority is at + * or below the given value. Use to gate lower-priority commands from overriding higher-priority + * ones. + * + * @param priority the maximum priority level that allows the trigger to be active + * @return trigger active when {@link #commandPriority} ≤ priority + */ public Trigger checkPriority(int priority) { return new Trigger(() -> commandPriority <= priority); } - public Command setPattern(LEDPattern pattern, int priority) { + /** + * Returns a command that continuously applies {@code pattern} to the LED segment and records + * the given {@code priority} while running. The command runs while the robot is disabled. + * + *

When switching from a hardware animation to a software ({@link SolidColor}) pattern, all + * active animation slots are cleared automatically before the first software write. + * + * @param pattern the {@link CANdlePattern} to apply each loop cycle + * @param priority priority level stored in {@link #commandPriority} while this command runs + * @return a command that applies the pattern continuously + */ + public Command setPattern(CANdlePattern pattern, int priority) { return run(() -> { commandPriority = priority; - pattern.applyTo(ledView); + boolean isAnim = pattern instanceof HardwareAnimPattern; + // Clear this instance's animation slot once when transitioning to a software + // pattern. Only our own slot is cleared so other instances sharing the same + // CANdle keep their animations running. + if (lastWasAnimation && !isAnim) { + candle.setControl(new EmptyAnimation(config.getAnimationSlot())); + } + lastWasAnimation = isAnim; + pattern.applyTo(candle, config.getStartIdx(), config.getNumLeds()); }) + .finallyDo(() -> commandPriority = -1) .ignoringDisable(true) .withName("LEDs.setPattern"); } - public Command setPattern(LEDPattern pattern) { + /** + * Returns a command that continuously applies {@code pattern} to the LED segment at priority 0. + * + * @param pattern the {@link CANdlePattern} to apply + * @return a command that applies the pattern continuously at the default priority + */ + public Command setPattern(CANdlePattern pattern) { return setPattern(pattern, 0); } - @Override - public void setupStates() {} + // ------------------------------------------------------------------------- + // Internal helpers + // ------------------------------------------------------------------------- - @Override - public void setupDefaultCommand() { - setDefaultCommand( - setPattern(solid(Color.kOrange), 0) - .withName("SPECTRUM LED DEFAULT COMMAND SHOULD NOT BE RUNNING")); + /** + * Converts a WPILib {@link Color} (0.0–1.0 double components) to a {@link RGBWColor} with + * {@code W = 0}. + */ + private static RGBWColor toRGBW(Color color) { + return new RGBWColor( + (int) (color.red * 255), (int) (color.green * 255), (int) (color.blue * 255), 0); } + // ------------------------------------------------------------------------- + // Hardware animation pattern factories + // ------------------------------------------------------------------------- + /** - * LED Pattern Stripe, takes in a double percent and sets the first length number of LEDs to one - * color and the rest of the strip to another + * Blinking (strobe) pattern — alternates between {@code color} and off. Each half-cycle (on and + * off) lasts {@code onTimeSecs} seconds. + * + *

Implemented using {@link StrobeAnimation}. Frame rate = {@code 1 / onTimeSecs} Hz. + * + * @param color the blink color + * @param onTimeSecs duration in seconds of each on (and off) half-cycle + * @return a hardware animation {@link CANdlePattern} */ - public LEDPattern stripe(double percent, Color color1, Color color2) { - return LEDPattern.steps(Map.of(0.00, color1, percent, color2)); + public CANdlePattern blink(Color color, double onTimeSecs) { + RGBWColor rgbw = toRGBW(color); + // Lazy: animation created on first applyTo call using the runtime startIdx/numLeds. + StrobeAnimation[] holder = new StrobeAnimation[1]; + return hardwareAnim( + (candle, startIdx, numLeds) -> { + if (holder[0] == null) { + holder[0] = + new StrobeAnimation(startIdx, startIdx + numLeds - 1) + .withSlot(config.getAnimationSlot()) + .withColor(rgbw) + .withFrameRate(Hertz.of(1.0 / onTimeSecs)); + } + candle.setControl(holder[0]); + }); } /** - * Creates a solid LED pattern with the specified color. + * Breathing (sinusoidal fade-in/out) pattern — fades between the peak {@code color} and off. + * + *

Implemented using {@link SingleFadeAnimation}. Each animation frame changes brightness by + * 1%, so frame rate = {@code 200 / periodSecs} Hz for a complete 0→100→0% cycle. * - * @param color the color to be used for the solid LED pattern - * @return an LEDPattern object representing the solid color pattern + * @param color the peak color at full brightness + * @param periodSecs duration in seconds of one full breathe cycle + * @return a hardware animation {@link CANdlePattern} */ - public LEDPattern solid(Color color) { - return LEDPattern.solid(color); + public CANdlePattern breathe(Color color, double periodSecs) { + RGBWColor rgbw = toRGBW(color); + SingleFadeAnimation[] holder = new SingleFadeAnimation[1]; + return hardwareAnim( + (candle, startIdx, numLeds) -> { + if (holder[0] == null) { + holder[0] = + new SingleFadeAnimation(startIdx, startIdx + numLeds - 1) + .withSlot(config.getAnimationSlot()) + .withColor(rgbw) + .withFrameRate(Hertz.of(200.0 / periodSecs)); + } + candle.setControl(holder[0]); + }); } /** - * Creates an LED pattern that blinks with the specified on-time duration. + * Static rainbow — hue distributed evenly across the strip, advancing very slowly. * - * @param onTime the duration (in seconds) for which the LED stays on during each blink cycle - * @return an LEDPattern that blinks with the specified on-time duration + * @return a hardware animation {@link CANdlePattern} */ - public LEDPattern blink(Color c, double onTime) { - return solid(c).blink(Seconds.of(onTime)); + public CANdlePattern rainbow() { + return rainbow(1.0); } /** - * Creates a breathing LED pattern with the specified period. + * Static rainbow with configurable brightness. * - * @param period The period of the breathing effect in seconds. - * @return An LEDPattern object representing the breathing effect. + * @param brightness brightness scalar (0.0–1.0) + * @return a hardware animation {@link CANdlePattern} */ - public LEDPattern breathe(Color c, double period) { - return solid(c).breathe(Seconds.of(period)); + public CANdlePattern rainbow(double brightness) { + RainbowAnimation[] holder = new RainbowAnimation[1]; + return hardwareAnim( + (candle, startIdx, numLeds) -> { + if (holder[0] == null) { + holder[0] = + new RainbowAnimation(startIdx, startIdx + numLeds - 1) + .withSlot(config.getAnimationSlot()) + .withBrightness(brightness) + .withFrameRate(Hertz.of(3)); + } + candle.setControl(holder[0]); + }); } /** - * Creates and returns a rainbow LED pattern with specified brightness and saturation. + * Scrolling rainbow that advances quickly across the strip. * - * @return an LEDPattern object representing a rainbow pattern with maximum brightness (255) and - * medium saturation (128). + * @return a hardware animation {@link CANdlePattern} */ - public LEDPattern rainbow() { - return rainbow(255, 128); + public CANdlePattern scrollingRainbow() { + RainbowAnimation[] holder = new RainbowAnimation[1]; + return hardwareAnim( + (candle, startIdx, numLeds) -> { + if (holder[0] == null) { + holder[0] = + new RainbowAnimation(startIdx, startIdx + numLeds - 1) + .withSlot(config.getAnimationSlot()) + .withBrightness(1.0) + .withFrameRate(Hertz.of(60)); + } + candle.setControl(holder[0]); + }); } - public LEDPattern scrollingRainbow() { - return rainbow().scrollAtAbsoluteSpeed(MetersPerSecond.of(0.25), config.getLedSpacing()); + /** + * Chase / color-flow pattern — progressively lights LEDs one at a time across the strip and + * repeats. + * + *

Implemented using {@link ColorFlowAnimation}. Frame rate = {@code numLeds × speed} Hz so + * that {@code speed} full cycles occur per second. + * + * @param color the chase color + * @param speed desired number of full strip cycles per second + * @return a hardware animation {@link CANdlePattern} + */ + public CANdlePattern chase(Color color, double speed) { + RGBWColor rgbw = toRGBW(color); + ColorFlowAnimation[] holder = new ColorFlowAnimation[1]; + return hardwareAnim( + (candle, startIdx, numLeds) -> { + if (holder[0] == null) { + holder[0] = + new ColorFlowAnimation(startIdx, startIdx + numLeds - 1) + .withSlot(config.getAnimationSlot()) + .withColor(rgbw) + .withFrameRate(Hertz.of(numLeds * speed)); + } + candle.setControl(holder[0]); + }); } /** - * Generates a rainbow LED pattern with the specified saturation and value. + * Bouncing dot pattern — a pocket of light travels back and forth across the strip. + * + *

Implemented using {@link LarsonAnimation} with {@link LarsonBounceValue#Back}. Frame rate + * is computed so one back-and-forth cycle takes {@code durationSecs} seconds. * - * @param saturation the saturation level of the rainbow pattern (0-255) - * @param value the brightness value of the rainbow pattern (0-255) - * @return an LEDPattern object representing the rainbow pattern + * @param color the dot color + * @param durationSecs seconds per complete back-and-forth cycle + * @return a hardware animation {@link CANdlePattern} */ - public LEDPattern rainbow(int saturation, int value) { - return LEDPattern.rainbow(saturation, value); + public CANdlePattern bounce(Color color, double durationSecs) { + RGBWColor rgbw = toRGBW(color); + LarsonAnimation[] holder = new LarsonAnimation[1]; + return hardwareAnim( + (candle, startIdx, numLeds) -> { + if (holder[0] == null) { + // One full cycle = 2 * (numLeds - 1) LED-position advances. + double frameRate = 2.0 * Math.max(numLeds - 1, 1) / durationSecs; + holder[0] = + new LarsonAnimation(startIdx, startIdx + numLeds - 1) + .withSlot(config.getAnimationSlot()) + .withColor(rgbw) + .withSize(3) + .withBounceMode(LarsonBounceValue.Back) + .withFrameRate(Hertz.of(frameRate)); + } + candle.setControl(holder[0]); + }); } /** - * Creates a gradient LED pattern using the specified colors. + * Fire animation using the CANdle's built-in hardware animation engine. * - * @param colors The array of colors to be used in the gradient pattern. - * @return An LEDPattern object representing the gradient pattern. + * @return a hardware animation {@link CANdlePattern} */ - public LEDPattern gradient(Color... colors) { - return LEDPattern.gradient(GradientType.kContinuous, colors); + public CANdlePattern fire() { + FireAnimation[] holder = new FireAnimation[1]; + return hardwareAnim( + (candle, startIdx, numLeds) -> { + if (holder[0] == null) { + holder[0] = + new FireAnimation(startIdx, startIdx + numLeds - 1) + .withSlot(config.getAnimationSlot()) + .withFrameRate(Hertz.of(60)); + } + candle.setControl(holder[0]); + }); } /** - * Scrolls the given LED pattern at the specified speed. + * RGB color-cycle animation using the CANdle's built-in hardware animation engine. * - * @param pattern the LEDPattern to be scrolled - * @param speedMps the speed at which the pattern should scroll, in meters per second - * @return a new LEDPattern that represents the scrolled pattern + * @return a hardware animation {@link CANdlePattern} */ - public LEDPattern scroll(LEDPattern pattern, double speedMps) { - return pattern.scrollAtAbsoluteSpeed(MetersPerSecond.of(speedMps), config.getLedSpacing()); + public CANdlePattern rgbCycle() { + RgbFadeAnimation[] holder = new RgbFadeAnimation[1]; + return hardwareAnim( + (candle, startIdx, numLeds) -> { + if (holder[0] == null) { + holder[0] = + new RgbFadeAnimation(startIdx, startIdx + numLeds - 1) + .withSlot(config.getAnimationSlot()) + .withFrameRate(Hertz.of(30)); + } + candle.setControl(holder[0]); + }); } + // ------------------------------------------------------------------------- + // Software pattern factories (SolidColor one-shot, resent each loop) + // ------------------------------------------------------------------------- + /** - * Creates an LED chase pattern with the specified color, percentage, and speed. + * Solid color pattern. + * + *

Uses a single {@link SolidColor} control request (one-shot, resent each loop). * - * @param color1 The color to be used in the chase pattern. - * @param percent The percentage of the pattern that will be the specified color. - * @param speed The speed at which the pattern will scroll, in Hertz. - * @return An LEDPattern object representing the chase pattern. + * @param color the color to display + * @return a software {@link CANdlePattern} showing a constant solid color */ - public LEDPattern chase(Color color1, double percent, double speed) { - return LEDPattern.steps(Map.of(0.00, color1, percent, Color.kBlack)) - .scrollAtRelativeSpeed(Frequency.ofBaseUnits(speed, Hertz)); + public CANdlePattern solid(Color color) { + RGBWColor rgbw = toRGBW(color); + SolidColor[] holder = new SolidColor[1]; + return (candle, startIdx, numLeds) -> { + if (holder[0] == null) { + holder[0] = new SolidColor(startIdx, startIdx + numLeds - 1).withColor(rgbw); + } + candle.setControl(holder[0]); + }; } /** - * Creates a bouncing LED pattern with the specified color and duration. - * - * @param c the color of the bouncing LED - * @param durationInSeconds the duration of one complete bounce cycle in seconds - * @return an LEDPattern that applies the bouncing effect to the LEDs - */ - public LEDPattern bounce(Color c, double durationInSeconds) { - return new LEDPattern() { - @Override - public void applyTo(LEDReader reader, LEDWriter writer) { - int bufLen = reader.getLength(); - long currentTime = System.currentTimeMillis(); - double cycleTime = - durationInSeconds - * 1000; // Convert time to milliseconds for the entire cycle - double phase = - (currentTime % cycleTime) / cycleTime; // Phase of the cycle from 0 to 1 - - // Determine direction and position based on the phase - boolean backwards = phase > 0.5; - double position = backwards ? 2 * (1 - phase) : 2 * phase; - int ledPosition = (int) (position * bufLen); - - for (int tempLed = 0; tempLed < bufLen; tempLed++) { - if (tempLed == ledPosition) { - writer.setLED(tempLed, c); - } else if (tempLed == ledPosition - 1 || tempLed == ledPosition + 1) { - writer.setLED( - tempLed, new Color(c.red * 0.66, c.green * 0.66, c.blue * 0.66)); - } else if (tempLed == ledPosition - 2 || tempLed == ledPosition + 2) { - writer.setLED( - tempLed, new Color(c.red * 0.33, c.green * 0.33, c.blue * 0.33)); - } else { - writer.setLED(tempLed, Color.kBlack); - } - } + * Two-color stripe: the first {@code percent} fraction of LEDs shows {@code color1}, the + * remainder shows {@code color2}. + * + *

Uses two {@link SolidColor} controls (one-shot each, resent each loop). + * + * @param percent fraction of the strip (0.0–1.0) assigned to {@code color1} + * @param color1 color for the leading segment + * @param color2 color for the trailing segment + * @return a software {@link CANdlePattern} showing the two-color stripe + */ + public CANdlePattern stripe(double percent, Color color1, Color color2) { + RGBWColor rgbw1 = toRGBW(color1); + RGBWColor rgbw2 = toRGBW(color2); + SolidColor[][] holder = new SolidColor[1][]; + return (candle, startIdx, numLeds) -> { + if (holder[0] == null) { + int split = Math.max(0, Math.min((int) Math.round(numLeds * percent), numLeds)); + holder[0] = new SolidColor[2]; + // Segment 1 (may be empty if split == 0) + holder[0][0] = + (split > 0) + ? new SolidColor(startIdx, startIdx + split - 1).withColor(rgbw1) + : null; + // Segment 2 (may be empty if split == numLeds) + holder[0][1] = + (split < numLeds) + ? new SolidColor(startIdx + split, startIdx + numLeds - 1) + .withColor(rgbw2) + : null; + } + for (SolidColor req : holder[0]) { + if (req != null) candle.setControl(req); } }; } /** - * Creates an ombre LED pattern that transitions smoothly between two colors. - * - * @param startColor The starting color of the ombre effect. - * @param endColor The ending color of the ombre effect. - * @return An LEDPattern that applies the ombre effect to the LED strip. - */ - public LEDPattern ombre(Color startColor, Color endColor) { - return new LEDPattern() { - @Override - public void applyTo(LEDReader reader, LEDWriter writer) { - int bufLen = reader.getLength(); - long currentTime = System.currentTimeMillis(); - // The speed factor here determines how quickly the ombre moves along the strip - double phaseShift = - (currentTime / 1000.0) - * 0.58 // this is the speed, the higher the number the faster the - // ombre moves - % 1.0; // Modulo 1 to keep the phase within [0, 1] - for (int tempLed = 0; tempLed < bufLen; tempLed++) { - // Adjust ratio to include the phaseShift, causing the ombre to move - double ratio = - ((tempLed + (bufLen * phaseShift)) - / bufLen - % 1.0); // Modulo 1 to ensure the ratio loops within [0, - // 1] - - // Interpolate the red, green, and blue components separately using double - // precision - double red = (startColor.red * (1 - ratio)) + (endColor.red * ratio); - double green = (startColor.green * (1 - ratio)) + (endColor.green * ratio); - double blue = (startColor.blue * (1 - ratio)) + (endColor.blue * ratio); - - // Create a new color for the current LED - Color currentColor = new Color(red, green, blue); - - // Set the color of the current LED - writer.setLED(tempLed, currentColor); + * Linear gradient between two colors across the strip. Colors are pre-computed at first use. + * + *

Uses N {@link SolidColor} controls (one per LED, one-shot, resent each loop). + * + * @param color1 color at the start (index 0) of the segment + * @param color2 color at the end of the segment + * @return a software {@link CANdlePattern} showing the two-color gradient + */ + public CANdlePattern gradient(Color color1, Color color2) { + SolidColor[][] holder = new SolidColor[1][]; + return (candle, startIdx, numLeds) -> { + if (holder[0] == null) { + holder[0] = new SolidColor[numLeds]; + for (int i = 0; i < numLeds; i++) { + double ratio = (numLeds <= 1) ? 0.0 : (double) i / (numLeds - 1); + int r = (int) (color1.red * 255 * (1 - ratio) + color2.red * 255 * ratio); + int g = (int) (color1.green * 255 * (1 - ratio) + color2.green * 255 * ratio); + int b = (int) (color1.blue * 255 * (1 - ratio) + color2.blue * 255 * ratio); + holder[0][i] = + new SolidColor(startIdx + i, startIdx + i) + .withColor(new RGBWColor(r, g, b, 0)); } } + for (SolidColor req : holder[0]) { + candle.setControl(req); + } }; } /** - * Creates a wave LED pattern that transitions between two colors over a specified cycle length - * of LEDs and duration. - * - * @param c1 The first color in the wave pattern. - * @param c2 The second color in the wave pattern. - * @param cycleLength The length of the wave cycle in terms of LEDs. - * @param durationSecs The duration of the entire wave pattern in seconds. - * @return An LEDPattern that applies the wave effect to the LEDs. - */ - public LEDPattern wave(Color c1, Color c2, double cycleLength, double durationSecs) { - return new LEDPattern() { - @Override - public void applyTo(LEDReader reader, LEDWriter writer) { - int bufLen = reader.getLength(); - double currentTime = Timer.getFPGATimestamp(); - double phase = (currentTime % durationSecs) / durationSecs; - double x = (1 - phase) * 2.0 * Math.PI; - double xDiffPerLed = (2.0 * Math.PI) / cycleLength; - double waveExponent = 0.4; - - for (int tempLed = 0; tempLed < bufLen; tempLed++) { - x += xDiffPerLed; - - double ratio = (Math.pow(Math.sin(x), waveExponent) + 1.0) / 2.0; - if (Double.isNaN(ratio)) { - ratio = (-Math.pow(Math.sin(x + Math.PI), waveExponent) + 1.0) / 2.0; - } - if (Double.isNaN(ratio)) { - ratio = 0.5; - } - double red = (c1.red * (1 - ratio)) + (c2.red * ratio); - double green = (c1.green * (1 - ratio)) + (c2.green * ratio); - double blue = (c1.blue * (1 - ratio)) + (c2.blue * ratio); - writer.setLED(tempLed, new Color(red, green, blue)); + * Edge-highlight pattern — lights the first and last {@code length} LEDs with {@code color} and + * turns off the center LEDs. + * + *

Uses two or three {@link SolidColor} controls (one-shot, resent each loop). + * + * @param color the color to apply to the edge LEDs + * @param length the number of LEDs to illuminate at each end of the strip + * @return a software {@link CANdlePattern} showing lit edges and a dark center + */ + public CANdlePattern edges(Color color, int length) { + RGBWColor rgbw = toRGBW(color); + SolidColor[][] holder = new SolidColor[1][]; + return (candle, startIdx, numLeds) -> { + if (holder[0] == null) { + int clampedLen = Math.min(length, numLeds / 2); + int centerStart = startIdx + clampedLen; + int centerEnd = startIdx + numLeds - clampedLen - 1; + // left edge, right edge, (optional) center black + holder[0] = (centerStart <= centerEnd) ? new SolidColor[3] : new SolidColor[2]; + holder[0][0] = new SolidColor(startIdx, startIdx + clampedLen - 1).withColor(rgbw); + holder[0][1] = + new SolidColor(startIdx + numLeds - clampedLen, startIdx + numLeds - 1) + .withColor(rgbw); + if (holder[0].length == 3) { + holder[0][2] = + new SolidColor(centerStart, centerEnd) + .withColor(new RGBWColor(0, 0, 0, 0)); } } + for (SolidColor req : holder[0]) { + candle.setControl(req); + } }; } /** - * Creates an LEDPattern that represents a countdown effect. The countdown starts from a - * specified time and lasts for a given duration. During the countdown, the LEDs transition from - * yellow to red, and progressively turn off from the end of the strip towards the beginning. - * - * @param countStartTimeSec A DoubleSupplier that provides the start time of the countdown in - * seconds. - * @param durationInSeconds The total duration of the countdown in seconds. - * @return An LEDPattern that applies the countdown effect to the LEDs. - */ - public LEDPattern countdown(DoubleSupplier countStartTimeSec, double durationInSeconds) { - double startTime = countStartTimeSec.getAsDouble(); - return new LEDPattern() { - @Override - public void applyTo(LEDReader reader, LEDWriter writer) { - int bufLen = reader.getLength(); - double currentTimeSecs = Timer.getFPGATimestamp(); - double elapsedTimeInSeconds = currentTimeSecs - startTime; - - // Calculate the progress of the countdown - double progress = elapsedTimeInSeconds / durationInSeconds; - - // Calculate the number of LEDs to turn off based on the progress - int ledsToTurnOff = (int) (bufLen * progress); - - // Calculate the color transition from yellow to red based on the progress - // Yellow (255, 255, 0) to Red (255, 0, 0) - int red = 255; // Red component stays at 255 - int green = (int) (255 * (1 - progress)); // Green component decreases to 0 - Color countdownColor = new Color(red, green, 0); - - // Update the LEDs from the end of the strip towards the beginning - for (int tempLed = bufLen - 1; tempLed >= 0; tempLed--) { - if (bufLen - tempLed <= ledsToTurnOff) { - // Turn off the LEDs progressively - writer.setLED(tempLed, Color.kBlack); - } else { - // Set the remaining LEDs to the countdown color - writer.setLED(tempLed, countdownColor); - } - } - - // If the countdown is complete, ensure all LEDs are turned off - if (progress >= 1.0) { - for (int i = 0; i < bufLen; i++) { - writer.setLED(i, Color.kBlack); - } + * Animated ombre — transitions smoothly between two colors across the strip and scrolls the + * blend point over time. + * + *

Uses N {@link SolidColor} controls (one per LED, one-shot, resent each loop with updated + * colors). {@link RGBWColor} objects are created each loop since the type is immutable. + * + * @param startColor the leading color + * @param endColor the trailing color + * @return a software {@link CANdlePattern} showing the animated ombre + */ + public CANdlePattern ombre(Color startColor, Color endColor) { + SolidColor[][] holder = new SolidColor[1][]; + return (candle, startIdx, numLeds) -> { + if (holder[0] == null) { + holder[0] = new SolidColor[numLeds]; + for (int i = 0; i < numLeds; i++) { + holder[0][i] = new SolidColor(startIdx + i, startIdx + i); } } + // Speed: 0.58 strip-lengths per second + double phaseShift = (System.currentTimeMillis() / 1000.0) * 0.58 % 1.0; + for (int i = 0; i < numLeds; i++) { + double ratio = ((i + numLeds * phaseShift) / numLeds) % 1.0; + int r = (int) (startColor.red * 255 * (1 - ratio) + endColor.red * 255 * ratio); + int g = (int) (startColor.green * 255 * (1 - ratio) + endColor.green * 255 * ratio); + int b = (int) (startColor.blue * 255 * (1 - ratio) + endColor.blue * 255 * ratio); + holder[0][i].Color = new RGBWColor(r, g, b, 0); + candle.setControl(holder[0][i]); + } }; } - public LEDPattern switchCountdown(Color startingColor) { - - return new LEDPattern() { - @Override - public void applyTo(LEDReader reader, LEDWriter writer) { - - int[] times = { - 10, // surely there's a more efficient way to do this - 25, 25, 25, 25, 30, - }; - - int bufLen = reader.getLength(); - double currentTimeSecs = Timer.getMatchTime(); - - double elapsedTimeInSeconds = 140 - currentTimeSecs; - - // Calculate the progress of the countdown - int shiftTime = 0; - int cumulativeTime = 0; - Color color = Color.kBlack; - - for (int i = 0; i < times.length; i++) { - cumulativeTime += times[i]; - if (cumulativeTime > elapsedTimeInSeconds) { - shiftTime = times[i]; - switch (i) { - case 0, 5: // this is also kinda a mess but it works wtv - color = Color.kPurple; - break; - case 1, 3: - color = startingColor; - break; - case 2, 4: - if (startingColor == Color.kRed) color = Color.kBlue; - else color = Color.kRed; - break; - } - break; - } + /** + * Sinusoidal wave pattern blending between two colors. + * + *

Uses N {@link SolidColor} controls (one per LED, one-shot, resent each loop). + * + * @param c1 first wave color + * @param c2 second wave color + * @param cycleLength number of LEDs per wave period + * @param durationSecs period of the time-based animation in seconds + * @return a software {@link CANdlePattern} showing the wave + */ + public CANdlePattern wave(Color c1, Color c2, double cycleLength, double durationSecs) { + SolidColor[][] holder = new SolidColor[1][]; + return (candle, startIdx, numLeds) -> { + if (holder[0] == null) { + holder[0] = new SolidColor[numLeds]; + for (int i = 0; i < numLeds; i++) { + holder[0][i] = new SolidColor(startIdx + i, startIdx + i); } - double progress = 1 - (cumulativeTime - elapsedTimeInSeconds) / shiftTime; - - // Calculate the number of LEDs to turn off based on the progress - int ledsToTurnOff = (int) (bufLen * progress); - - // Update the LEDs from the end of the strip towards the beginning - for (int tempLed = bufLen - 1; tempLed >= 0; tempLed--) { - if (bufLen - tempLed <= ledsToTurnOff) { - // Turn off the LEDs progressively - writer.setLED(tempLed, Color.kBlack); - } else { - // Set the remaining LEDs to the countdown color - writer.setLED(tempLed, color); - } + } + double currentTime = Timer.getFPGATimestamp(); + double phase = (currentTime % durationSecs) / durationSecs; + double x = (1 - phase) * 2.0 * Math.PI; + double xDiffPerLed = (2.0 * Math.PI) / cycleLength; + double waveExponent = 0.4; + for (int i = 0; i < numLeds; i++) { + x += xDiffPerLed; + double ratio = (Math.pow(Math.sin(x), waveExponent) + 1.0) / 2.0; + if (Double.isNaN(ratio)) { + ratio = (-Math.pow(Math.sin(x + Math.PI), waveExponent) + 1.0) / 2.0; } + if (Double.isNaN(ratio)) ratio = 0.5; + int r = (int) (c1.red * 255 * (1 - ratio) + c2.red * 255 * ratio); + int g = (int) (c1.green * 255 * (1 - ratio) + c2.green * 255 * ratio); + int b = (int) (c1.blue * 255 * (1 - ratio) + c2.blue * 255 * ratio); + holder[0][i].Color = new RGBWColor(r, g, b, 0); + candle.setControl(holder[0][i]); + } + }; + } - // If the countdown is complete, ensure all LEDs are turned off - if (progress >= 1.0) { - for (int i = 0; i < bufLen; i++) { - writer.setLED(i, Color.kBlack); - } + /** + * Countdown pattern — LEDs transition from yellow to red and progressively turn off from the + * end of the segment toward the beginning as time elapses. + * + *

Uses N {@link SolidColor} controls (one per LED, one-shot, resent each loop). + * + * @param countStartTimeSec supplies the FPGA timestamp (seconds) when the countdown began + * @param durationInSeconds total countdown duration in seconds + * @return a software {@link CANdlePattern} showing the countdown + */ + public CANdlePattern countdown(DoubleSupplier countStartTimeSec, double durationInSeconds) { + SolidColor[][] holder = new SolidColor[1][]; + return (candle, startIdx, numLeds) -> { + if (holder[0] == null) { + holder[0] = new SolidColor[numLeds]; + for (int i = 0; i < numLeds; i++) { + holder[0][i] = new SolidColor(startIdx + i, startIdx + i); } } + // Read the supplier each loop (not at factory time) so patterns built at binding + // time still measure from the correct start when the command eventually runs. + double elapsed = Timer.getFPGATimestamp() - countStartTimeSec.getAsDouble(); + double progress = Math.min(elapsed / durationInSeconds, 1.0); + int ledsOff = (int) (numLeds * progress); + int green = (int) (255 * (1 - progress)); + for (int i = numLeds - 1; i >= 0; i--) { + holder[0][i].Color = + (numLeds - i <= ledsOff) + ? new RGBWColor(0, 0, 0, 0) + : new RGBWColor(255, green, 0, 0); + candle.setControl(holder[0][i]); + } }; } - public LEDPattern edges(Color c, double length) { - return new LEDPattern() { - public void applyTo(LEDReader reader, LEDWriter writer) { - int bufLen = reader.getLength(); - for (int i = 0; i < bufLen; i++) { - if (i < length || i > bufLen - length - 1) { - writer.setLED(i, c); - } else { - writer.setLED(i, Color.kBlack); + /** + * Alliance switch countdown — cycles through alliance colors (and purple) on a hard-coded + * match-time schedule, progressively turning off LEDs within each segment as time elapses. + * + *

Segment schedule (seconds remaining → color): + * + *

+     *  140–130  purple
+     *  130–105  startingColor
+     *  105–80   opponent color
+     *   80–55   startingColor
+     *   55–30   opponent color
+     *   30–0    purple
+     * 
+ * + *

Uses N {@link SolidColor} controls (one per LED, one-shot, resent each loop). + * + * @param startingColor the alliance color displayed during this robot's segments + * @return a software {@link CANdlePattern} reflecting the current switch-countdown state + */ + public CANdlePattern switchCountdown(Color startingColor) { + SolidColor[][] holder = new SolidColor[1][]; + return (candle, startIdx, numLeds) -> { + if (holder[0] == null) { + holder[0] = new SolidColor[numLeds]; + for (int i = 0; i < numLeds; i++) { + holder[0][i] = new SolidColor(startIdx + i, startIdx + i); + } + } + + int[] times = {10, 25, 25, 25, 25, 30}; + double elapsed = 140 - Timer.getMatchTime(); + + int shiftTime = 0; + int cumulativeTime = 0; + Color color = Color.kBlack; + + for (int i = 0; i < times.length; i++) { + cumulativeTime += times[i]; + if (cumulativeTime > elapsed) { + shiftTime = times[i]; + switch (i) { + case 0, 5 -> color = Color.kPurple; + case 1, 3 -> color = startingColor; + case 2, 4 -> color = + Color.kRed.equals(startingColor) ? Color.kBlue : Color.kRed; + default -> color = Color.kBlack; } + break; } } + + double progress = 1.0 - (cumulativeTime - elapsed) / Math.max(shiftTime, 1); + int ledsOff = (int) (numLeds * Math.min(progress, 1.0)); + RGBWColor segColor = toRGBW(color); + + for (int i = numLeds - 1; i >= 0; i--) { + holder[0][i].Color = + (numLeds - i <= ledsOff) ? new RGBWColor(0, 0, 0, 0) : segColor; + candle.setControl(holder[0][i]); + } }; } - - // LEDPattern Methods - // reversed() - // offsetBy(int offset) - // scrollAtAbsoluteSpeed(Distance speed, Distance spacing) - // scrollAtRelativeSpeed(Frequency velocity) - // blink(Time onTime, Time offTime) - // synchronizedBlink(BooleanSupplier signal) - // breathe(Time period) - // overlayOn(LEDPattern base) - // blend(LEDPattern other) - // mask(LEDPattern mask) - // atBrightness(Dimensionless relativeBrightness) - // progressMaskLayer(DoubleSupplier progressSupplier) - // steps(Map steps) } diff --git a/src/main/java/frc/spectrumLib/mechanism/Mechanism.java b/src/main/java/frc/spectrumLib/mechanism/Mechanism.java index 2df2ef33..e87293a5 100644 --- a/src/main/java/frc/spectrumLib/mechanism/Mechanism.java +++ b/src/main/java/frc/spectrumLib/mechanism/Mechanism.java @@ -25,12 +25,11 @@ import edu.wpi.first.wpilibj.DriverStation; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.Commands; +import edu.wpi.first.wpilibj2.command.Subsystem; import edu.wpi.first.wpilibj2.command.button.Trigger; import frc.robot.Robot; -import frc.spectrumLib.CachedDouble; -import frc.spectrumLib.SpectrumRobot; -import frc.spectrumLib.SpectrumSubsystem; -import frc.spectrumLib.talonFX.TalonFXFactory; +import frc.spectrumLib.hardware.TalonFXFactory; +import frc.spectrumLib.util.CachedDouble; import frc.spectrumLib.util.CanDeviceId; import frc.spectrumLib.util.Conversions; import java.util.function.DoubleSupplier; @@ -71,14 +70,29 @@ *

Note: This class assumes CTRE Phoenix 6 units (rotations, rotations/sec, etc.) and uses * {@code Config.talonConfig.Feedback.SensorToMechanismRatio} as the mechanism gearing ratio. */ -public abstract class Mechanism implements SpectrumSubsystem { +public abstract class Mechanism implements Subsystem { + + // ── Fields ───────────────────────────────────────────────────────────────── + + /** The primary (leader) TalonFX motor controller. */ @Getter protected TalonFX motor; + + /** Optional follower TalonFX motor controllers that mirror the leader. */ @Getter protected TalonFX[] followerMotors; + + /** Configuration object holding motor IDs, Talon settings, and mechanism parameters. */ public Config config; + /** Alert displayed when an unexpected current reading is detected during diagnostics. */ Alert currentAlert = new Alert("", AlertType.kWarning); + + /** The last closed-loop position setpoint (in rotations) sent to the motor. */ private double target = 0; + /** The last closed-loop velocity setpoint (in rotations per second) sent to the motor. */ + private double velocityTarget = 0; + + // Cached sensor readings — updated lazily each loop via CachedDouble private final CachedDouble cachedRotations; private final CachedDouble cachedPercentage; private final CachedDouble cachedVoltage; @@ -88,20 +102,30 @@ public abstract class Mechanism implements SpectrumSubsystem { private final CachedDouble cachedSupplyCurrent; private final CachedDouble cachedTemp; + // ── Constructors ─────────────────────────────────────────────────────────── + + /** + * Creates a Mechanism and, if {@link Config#isAttached()} is {@code true}, initializes the + * leader TalonFX and any configured follower motors. Sensor caches are always initialized so + * safe defaults ({@code 0}) are returned even when unattached. + * + * @param config the mechanism configuration (motor IDs, Talon settings, follower config, etc.) + */ protected Mechanism(Config config) { this.config = config; if (isAttached()) { motor = TalonFXFactory.createConfigTalon(config.id, config.talonConfig); BaseStatusSignal.setUpdateFrequencyForAll( - 100, + 250, motor.getDutyCycle(), motor.getMotorVoltage(), motor.getTorqueCurrent(), motor.getStatorCurrent(), motor.getSupplyCurrent(), motor.getPosition(), - motor.getVelocity()); + motor.getVelocity(), + motor.getDeviceTemp()); motor.optimizeBusUtilization(); followerMotors = new TalonFX[config.followerConfigs.length]; @@ -111,6 +135,16 @@ protected Mechanism(Config config) { config.followerConfigs[i].id, motor, config.followerConfigs[i].opposeLeader); + BaseStatusSignal.setUpdateFrequencyForAll( + 250, + followerMotors[i].getDutyCycle(), + followerMotors[i].getMotorVoltage(), + followerMotors[i].getTorqueCurrent(), + followerMotors[i].getStatorCurrent(), + followerMotors[i].getSupplyCurrent(), + followerMotors[i].getPosition(), + followerMotors[i].getVelocity(), + followerMotors[i].getDeviceTemp()); followerMotors[i].optimizeBusUtilization(); } } @@ -124,54 +158,99 @@ protected Mechanism(Config config) { cachedVelocity = new CachedDouble(this::updateVelocityRPM); cachedTemp = new CachedDouble(this::updateTemp); - SpectrumRobot.add(this); this.register(); } + /** + * Creates a Mechanism and explicitly overrides the {@code attached} flag in the config. + * + * @param config the mechanism configuration + * @param attached {@code true} to enable hardware; {@code false} to run in software-only mode + */ protected Mechanism(Config config, boolean attached) { - this(config); + // The override must be applied BEFORE the delegated constructor runs, since that + // constructor decides whether to create the motor hardware based on the flag. + this(applyAttachedOverride(config, attached)); + } + + private static Config applyAttachedOverride(Config config, boolean attached) { config.attached = attached; + return config; } + // ── Subsystem Overrides ──────────────────────────────────────────────────── + + /** + * Called once per scheduler loop. Concrete subclasses should override to implement their + * periodic state-machine logic, telemetry, and sensor updates. + */ @Override public void periodic() {} + /** + * Called once per simulation loop. Concrete subclasses should override to update simulation + * state (e.g., physics model inputs). + */ @Override public void simulationPeriodic() {} + /** + * Returns the human-readable name of this mechanism, as defined in its {@link Config}. + * + * @return the mechanism name + */ @Override public String getName() { return config.getName(); } + // ── Utility ──────────────────────────────────────────────────────────────── + + /** + * Returns {@code true} if physical hardware is attached and motor commands should be sent. + * + * @return {@code true} when hardware is available + */ public boolean isAttached() { return config.isAttached(); } + /** + * Reports the combined supply current draw of the leader motor and all followers to the battery + * logger. Does nothing if the mechanism is not attached. + */ public void logBatteryUsage() { if (isAttached()) { - // Get all motor currents double motorCurrent = motor.getSupplyCurrent().getValueAsDouble(); double followersCurrent = 0; for (TalonFX follower : followerMotors) { followersCurrent += follower.getSupplyCurrent().getValueAsDouble(); } - - // Report to battery logger Robot.getBatteryLogger() .reportCurrentUsage("Mechanisms/" + getName(), motorCurrent + followersCurrent); } } + /** + * Returns the name of the command currently scheduled on this subsystem, or {@code "none"} if + * no command is running. + * + * @return the current command name + */ protected String getCurrentCommandName() { Command currentCommand = this.getCurrentCommand(); if (currentCommand != null) { return currentCommand.getName(); } - return "none"; } + /** + * Returns a {@link Trigger} that is active whenever this subsystem's current command is its + * default command. + * + * @return trigger that is {@code true} while the default command is running + */ public Trigger runningDefaultCommand() { return new Trigger(this::isRunningDefaultCommand); } @@ -180,19 +259,57 @@ private boolean isRunningDefaultCommand() { return this.getCurrentCommand() == this.getDefaultCommand(); } - // Return the closed loop target we have sent to the motor. + /** + * Returns the last closed-loop setpoint (in rotations) sent to the motor by this class. + * + * @return the most recent target position in rotations + */ public double getTarget() { return target; } + /** + * Returns the last closed-loop velocity setpoint (in rotations per second) sent to the motor by + * this class. + * + * @return the most recent target velocity in rotations per second + */ + public double getVelocityTargetRPS() { + return velocityTarget; + } + + // ── Triggers ─────────────────────────────────────────────────────────────── + + /** + * Returns a {@link Trigger} that is active when the motor is within {@code tolerance} rotations + * of the last commanded target position. + * + * @param tolerance maximum allowable error in rotations + * @return trigger that is {@code true} when position error is within tolerance + */ public Trigger atTargetPosition(DoubleSupplier tolerance) { return new Trigger(() -> isAtTargetPosition(tolerance)); } - private boolean isAtTargetPosition(DoubleSupplier tolerance) { + /** + * Returns {@code true} when the motor is within {@code tolerance} rotations of the last + * commanded target position. + * + * @param tolerance maximum allowable error in rotations + * @return {@code true} when position error is within tolerance + */ + public boolean isAtTargetPosition(DoubleSupplier tolerance) { return Math.abs(cachedRotations.getAsDouble() - target) < tolerance.getAsDouble(); } + /** + * Returns a {@link Trigger} that is active when the motor position is within {@code tolerance} + * of {@code target} (both in rotations). + * + * @param target the desired position in rotations + * @param tolerance maximum allowable error in rotations + * @return trigger that is {@code true} when position is within tolerance of target + */ public Trigger atRotations(DoubleSupplier target, DoubleSupplier tolerance) { return new Trigger( () -> @@ -200,16 +317,52 @@ public Trigger atRotations(DoubleSupplier target, DoubleSupplier tolerance) { < tolerance.getAsDouble()); } + /** + * Returns {@code true} when the motor position is within {@code tolerance} of {@code target} + * (both in rotations). + * + * @param target the desired position in rotations + * @param tolerance maximum allowable error in rotations + * @return {@code true} when position is within tolerance of target + */ + public boolean isAtRotations(DoubleSupplier target, DoubleSupplier tolerance) { + return Math.abs(getPositionRotations() - target.getAsDouble()) < tolerance.getAsDouble(); + } + + /** + * Returns a {@link Trigger} that is active when the motor position is below {@code target + + * tolerance} rotations. + * + * @param target reference position in rotations + * @param tolerance offset added to target to form the upper bound + * @return trigger that is {@code true} when position is below the threshold + */ public Trigger belowRotations(DoubleSupplier target, DoubleSupplier tolerance) { return new Trigger( () -> getPositionRotations() < (target.getAsDouble() + tolerance.getAsDouble())); } + /** + * Returns a {@link Trigger} that is active when the motor position is above {@code target - + * tolerance} rotations. + * + * @param target reference position in rotations + * @param tolerance offset subtracted from target to form the lower bound + * @return trigger that is {@code true} when position is above the threshold + */ public Trigger aboveRotations(DoubleSupplier target, DoubleSupplier tolerance) { return new Trigger( () -> getPositionRotations() > (target.getAsDouble() - tolerance.getAsDouble())); } + /** + * Returns a {@link Trigger} that is active when the motor position is within {@code tolerance} + * of {@code target} (both as a percentage of max rotations). + * + * @param target the desired position as a percentage of max rotations + * @param tolerance maximum allowable error as a percentage + * @return trigger that is {@code true} when position is within tolerance of target + */ public Trigger atPercentage(DoubleSupplier target, DoubleSupplier tolerance) { return new Trigger( () -> @@ -217,16 +370,40 @@ public Trigger atPercentage(DoubleSupplier target, DoubleSupplier tolerance) { < tolerance.getAsDouble()); } + /** + * Returns a {@link Trigger} that is active when the motor position is below {@code target + + * tolerance} (as a percentage of max rotations). + * + * @param target reference position as a percentage + * @param tolerance offset added to target to form the upper bound + * @return trigger that is {@code true} when position is below the threshold + */ public Trigger belowPercentage(DoubleSupplier target, DoubleSupplier tolerance) { return new Trigger( () -> getPositionPercentage() < (target.getAsDouble() + tolerance.getAsDouble())); } + /** + * Returns a {@link Trigger} that is active when the motor position is above {@code target - + * tolerance} (as a percentage of max rotations). + * + * @param target reference position as a percentage + * @param tolerance offset subtracted from target to form the lower bound + * @return trigger that is {@code true} when position is above the threshold + */ public Trigger abovePercentage(DoubleSupplier target, DoubleSupplier tolerance) { return new Trigger( () -> getPositionPercentage() > (target.getAsDouble() - tolerance.getAsDouble())); } + /** + * Returns a {@link Trigger} that is active when the motor position is within {@code tolerance} + * of {@code target} (both in degrees). + * + * @param target the desired position in degrees + * @param tolerance maximum allowable error in degrees + * @return trigger that is {@code true} when position is within tolerance of target + */ public Trigger atDegrees(DoubleSupplier target, DoubleSupplier tolerance) { return new Trigger( () -> @@ -234,31 +411,79 @@ public Trigger atDegrees(DoubleSupplier target, DoubleSupplier tolerance) { < tolerance.getAsDouble()); } + /** + * Returns a {@link Trigger} that is active when the motor position is below {@code target + + * tolerance} degrees. + * + * @param target reference position in degrees + * @param tolerance offset added to target to form the upper bound + * @return trigger that is {@code true} when position is below the threshold + */ public Trigger belowDegrees(DoubleSupplier target, DoubleSupplier tolerance) { return new Trigger( () -> getPositionDegrees() < (target.getAsDouble() + tolerance.getAsDouble())); } + /** + * Returns a {@link Trigger} that is active when the motor position is above {@code target - + * tolerance} degrees. + * + * @param target reference position in degrees + * @param tolerance offset subtracted from target to form the lower bound + * @return trigger that is {@code true} when position is above the threshold + */ public Trigger aboveDegrees(DoubleSupplier target, DoubleSupplier tolerance) { return new Trigger( () -> getPositionDegrees() > (target.getAsDouble() - tolerance.getAsDouble())); } + /** + * Returns a {@link Trigger} that is active when the motor velocity is within {@code tolerance} + * RPM of {@code target}. + * + * @param target the desired velocity in RPM + * @param tolerance maximum allowable error in RPM + * @return trigger that is {@code true} when velocity is within tolerance of target + */ public Trigger atVelocityRPM(DoubleSupplier target, DoubleSupplier tolerance) { return new Trigger( () -> Math.abs(getVelocityRPM() - target.getAsDouble()) < tolerance.getAsDouble()); } + /** + * Returns a {@link Trigger} that is active when the motor velocity is below {@code target + + * tolerance} RPM. + * + * @param target reference velocity in RPM + * @param tolerance offset added to target to form the upper bound + * @return trigger that is {@code true} when velocity is below the threshold + */ public Trigger belowVelocityRPM(DoubleSupplier target, DoubleSupplier tolerance) { return new Trigger( () -> getVelocityRPM() < (target.getAsDouble() + tolerance.getAsDouble())); } + /** + * Returns a {@link Trigger} that is active when the motor velocity is above {@code target - + * tolerance} RPM. + * + * @param target reference velocity in RPM + * @param tolerance offset subtracted from target to form the lower bound + * @return trigger that is {@code true} when velocity is above the threshold + */ public Trigger aboveVelocityRPM(DoubleSupplier target, DoubleSupplier tolerance) { return new Trigger( () -> getVelocityRPM() > (target.getAsDouble() - tolerance.getAsDouble())); } + /** + * Returns a {@link Trigger} that is active when the motor stator current is within {@code + * tolerance} amps of {@code target}. + * + * @param target the desired stator current in amps + * @param tolerance maximum allowable error in amps + * @return trigger that is {@code true} when stator current is within tolerance of target + */ public Trigger atCurrent(DoubleSupplier target, DoubleSupplier tolerance) { return new Trigger( () -> @@ -266,20 +491,38 @@ public Trigger atCurrent(DoubleSupplier target, DoubleSupplier tolerance) { < tolerance.getAsDouble()); } + /** + * Returns a {@link Trigger} that is active when the motor stator current is below {@code target + * + tolerance} amps. + * + * @param target reference current in amps + * @param tolerance offset added to target to form the upper bound + * @return trigger that is {@code true} when stator current is below the threshold + */ public Trigger belowCurrent(DoubleSupplier target, DoubleSupplier tolerance) { return new Trigger( () -> getStatorCurrent() < (target.getAsDouble() + tolerance.getAsDouble())); } + /** + * Returns a {@link Trigger} that is active when the motor stator current is above {@code target + * - tolerance} amps. + * + * @param target reference current in amps + * @param tolerance offset subtracted from target to form the lower bound + * @return trigger that is {@code true} when stator current is above the threshold + */ public Trigger aboveCurrent(DoubleSupplier target, DoubleSupplier tolerance) { return new Trigger( () -> getStatorCurrent() > (target.getAsDouble() - tolerance.getAsDouble())); } + // ── Sensor Readings ──────────────────────────────────────────────────────── + /** - * Update the value of the stator current for the motor + * Reads the stator current directly from the motor hardware. * - * @return motor stator current in amps + * @return motor stator current in amps, or {@code 0} if not attached */ public double updateStatorCurrent() { if (config.attached) { @@ -289,7 +532,7 @@ public double updateStatorCurrent() { } /** - * Gets the stator current of the motor + * Returns the cached stator current of the motor. * * @return motor stator current in amps */ @@ -298,9 +541,9 @@ public double getStatorCurrent() { } /** - * Update the value of the supply current for the motor + * Reads the supply current directly from the motor hardware. * - * @return motor supply current in amps + * @return motor supply current in amps, or {@code 0} if not attached */ public double updateSupplyCurrent() { if (config.attached) { @@ -310,7 +553,7 @@ public double updateSupplyCurrent() { } /** - * Gets the supply current of the motor + * Returns the cached supply current of the motor. * * @return motor supply current in amps */ @@ -319,9 +562,9 @@ public double getSupplyCurrent() { } /** - * Update the value of the voltage for the motor + * Reads the motor voltage directly from hardware. * - * @return motor voltage in volts + * @return motor voltage in volts, or {@code 0} if not attached */ public double updateVoltage() { if (config.attached) { @@ -331,7 +574,7 @@ public double updateVoltage() { } /** - * Gets the voltage of the motor + * Returns the cached voltage of the motor. * * @return motor voltage in volts */ @@ -340,9 +583,9 @@ public double getVoltage() { } /** - * Update the value of the temperature for the motor + * Reads the motor temperature directly from hardware. * - * @return motor temperature in Celsius + * @return motor temperature in Celsius, or {@code 0} if not attached */ public double updateTemp() { if (config.attached) { @@ -352,7 +595,7 @@ public double updateTemp() { } /** - * Gets the temperature of the motor + * Returns the cached temperature of the motor. * * @return motor temperature in Celsius */ @@ -360,46 +603,52 @@ public double getTemp() { return cachedTemp.getAsDouble(); } + // ── Unit Conversions ─────────────────────────────────────────────────────── + /** - * Percentage to Rotations + * Converts a percentage of the mechanism's maximum range into an absolute rotation count. * - * @return rotations based on percentage of max rotations + * @param percent position as a percentage of max rotations (0–100) + * @return the equivalent position in rotations */ public double percentToRotations(DoubleSupplier percent) { return (percent.getAsDouble() / 100) * config.maxRotations; } /** - * Rotations to Percentage + * Converts an absolute rotation count into a percentage of the mechanism's maximum range. * - * @param rotations - * @return percentage of max rotations + * @param rotations position in rotations + * @return the equivalent percentage of max rotations (0–100) */ public double rotationsToPercent(DoubleSupplier rotations) { return (rotations.getAsDouble() / config.maxRotations) * 100; } /** - * Degrees to Rotations + * Converts degrees to rotations (1 rotation = 360 degrees). * - * @return rotations + * @param degrees angle in degrees + * @return the equivalent position in rotations */ public double degreesToRotations(DoubleSupplier degrees) { return (degrees.getAsDouble() / 360); } /** - * Rotations to Degrees + * Converts rotations to degrees (1 rotation = 360 degrees). * - * @param rotations - * @return degrees + * @param rotations position in rotations + * @return the equivalent angle in degrees */ public double rotationsToDegrees(DoubleSupplier rotations) { return 360 * rotations.getAsDouble(); } + // ── Position & Velocity ──────────────────────────────────────────────────── + /** - * Gets the position of the motor in rotations + * Returns the cached motor position in rotations. * * @return motor position in rotations */ @@ -408,9 +657,9 @@ public double getPositionRotations() { } /** - * Updates the position of the motor in rotations + * Reads the motor position directly from hardware. * - * @return motor position in rotations + * @return motor position in rotations, or {@code 0} if not attached */ private double updatePositionRotations() { if (config.attached) { @@ -420,16 +669,16 @@ private double updatePositionRotations() { } /** - * Gets the position of the motor in percentage of max rotations + * Returns the cached motor position as a percentage of max rotations. * - * @return motor position in percentage of max rotations + * @return motor position in percentage of max rotations (0–100) */ public double getPositionPercentage() { return cachedPercentage.getAsDouble(); } /** - * Updates the position of the motor and converts it to percentage of max rotations + * Computes the motor position as a percentage of max rotations using the cached rotation value. * * @return motor position in percentage of max rotations */ @@ -438,7 +687,7 @@ private double updatePositionPercentage() { } /** - * Gets the position of the motor in degrees + * Returns the cached motor position in degrees. * * @return motor position in degrees */ @@ -447,7 +696,7 @@ public double getPositionDegrees() { } /** - * Updates the position of the motor and converts it to degrees + * Computes the motor position in degrees using the cached rotation value. * * @return motor position in degrees */ @@ -456,9 +705,9 @@ private double updatePositionDegrees() { } /** - * Updates the velocity of the motor + * Reads the motor velocity directly from hardware in rotations per second (CTRE native units). * - * @return motor velocity in rotations/sec which are the CTRE native units + * @return motor velocity in rotations per second, or {@code 0} if not attached */ private double updateVelocityRPS() { if (config.attached) { @@ -468,28 +717,31 @@ private double updateVelocityRPS() { } /** - * Gets the velocity of the motor in RPM + * Returns the cached motor velocity in RPM. * - * @return motor velocity in RPM + * @return motor velocity in revolutions per minute */ public double getVelocityRPM() { return cachedVelocity.getAsDouble(); } /** - * Updates the velocity of the motor and converts it to RPM + * Computes the motor velocity in RPM from the raw RPS sensor reading. * - * @return motor velocity in RPM + * @return motor velocity in revolutions per minute */ private double updateVelocityRPM() { return Conversions.RPStoRPM(updateVelocityRPS()); } - /* Commands: see method in lambda for more information */ + // ── Command Factories ────────────────────────────────────────────────────── + /** - * Runs the Mechanism at a given velocity + * Returns a {@link Command} that continuously drives the mechanism at the specified velocity + * using closed-loop voltage control. * - * @param velocityRPM in revolutions per minute + * @param velocityRPM the target velocity in revolutions per minute + * @return a command that runs the mechanism at the given velocity */ public Command runVelocity(DoubleSupplier velocityRPM) { return run(() -> setVelocity(() -> Conversions.RPMtoRPS(velocityRPM))) @@ -497,11 +749,11 @@ public Command runVelocity(DoubleSupplier velocityRPM) { } /** - * Run the mechanism at given velocity rpm in TorqueCurrentFOC mode + * Returns a {@link Command} that continuously drives the mechanism at the specified velocity + * using closed-loop Torque Current FOC control (requires Phoenix Pro). * - * @param velocityRPM rotations per minute - * @return A Command that runs the mechanism at the given velocity in rpm using torque current - * FOC control. + * @param velocityRPM the target velocity in revolutions per minute + * @return a command that runs the mechanism at the given velocity using torque current FOC */ public Command runVelocityTcFocRPM(DoubleSupplier velocityRPM) { return run(() -> setVelocityTorqueCurrentFOC(() -> Conversions.RPMtoRPS(velocityRPM))) @@ -509,32 +761,33 @@ public Command runVelocityTcFocRPM(DoubleSupplier velocityRPM) { } /** - * Open-loop percent output control with voltage compensation + * Returns a {@link Command} that continuously applies an open-loop percent output to the + * mechanism using voltage compensation. * - * @param percent A fractional units between -1 and +1 - * @return A Command that runs the mechanism at the given percentage output. + * @param percent fractional output between -1 and +1 + * @return a command that runs the mechanism at the given percent output */ public Command runPercentage(DoubleSupplier percent) { return run(() -> setPercentOutput(percent)).withName(getName() + ".runPercentage"); } /** - * Apply a voltage output to the mechanism, bypassing any closed-loop control. + * Returns a {@link Command} that continuously applies the specified voltage to the mechanism, + * bypassing any closed-loop control. * - * @param voltage volts - * @return A Command that applies the specified voltage output to the mechanism. + * @param voltage the desired voltage in volts + * @return a command that applies the given voltage output */ public Command runVoltage(DoubleSupplier voltage) { return run(() -> setVoltageOutput(voltage)).withName(getName() + ".runVoltage"); } /** - * Apply a voltage output to the mechanism, bypassing any closed-loop control and ignoring - * software limits. + * Returns a {@link Command} that continuously applies the specified voltage to the mechanism, + * bypassing closed-loop control and ignoring software limit switches. * - * @param voltage volts - * @return A Command that applies the specified voltage output to the mechanism, ignoring - * software limits. + * @param voltage the desired voltage in volts + * @return a command that applies the given voltage output, ignoring software limits */ public Command runVoltageNoSoftLimit(DoubleSupplier voltage) { return run(() -> setVoltageOutputNoSoftLimit(voltage)) @@ -542,32 +795,33 @@ public Command runVoltageNoSoftLimit(DoubleSupplier voltage) { } /** - * Run the mechanism at the specified torque current in FOC control. + * Returns a {@link Command} that continuously drives the mechanism at the specified torque + * current using FOC control (requires Phoenix Pro). * - * @param current torque current - * @return A Command that runs the mechanism at the given torque current in FOC control. + * @param current the desired torque current in amps + * @return a command that runs the mechanism at the given torque current */ public Command runTorqueCurrentFoc(DoubleSupplier current) { return run(() -> setTorqueCurrentFoc(current)).withName(getName() + ".runTorqueCurrentFoc"); } /** - * Run to the specified position. + * Returns a {@link Command} that continuously moves the mechanism to the specified position + * using Motion Magic Torque Current FOC control (requires Phoenix Pro). * - * @param rotations position in revolutions - * @return A Command that runs the mechanism to the specified position in revolutions using FOC - * control. + * @param rotations the target position in rotations + * @return a command that moves the mechanism to the given position */ public Command moveToRotations(DoubleSupplier rotations) { return run(() -> setMMPositionFoc(rotations)).withName(getName() + ".runPoseRevolutions"); } /** - * Move to the specified position. + * Returns a {@link Command} that continuously moves the mechanism to the specified position + * using Motion Magic Torque Current FOC control (requires Phoenix Pro). * - * @param percent position in percentage of max revolutions - * @return A Command that runs the mechanism to the specified position in percentage of max - * revolutions using FOC control. + * @param percent the target position as a percentage of max rotations (0–100) + * @return a command that moves the mechanism to the given percentage position */ public Command moveToPercentage(DoubleSupplier percent) { return run(() -> setMMPositionFoc(() -> percentToRotations(percent))) @@ -575,11 +829,11 @@ public Command moveToPercentage(DoubleSupplier percent) { } /** - * Move to the specified position. + * Returns a {@link Command} that continuously moves the mechanism to the specified angular + * position using Motion Magic Torque Current FOC control (requires Phoenix Pro). * - * @param degrees position in degrees - * @return A Command that runs the mechanism to the specified position in degrees using FOC - * control. + * @param degrees the target position in degrees + * @return a command that moves the mechanism to the given position in degrees */ public Command moveToDegrees(DoubleSupplier degrees) { return run(() -> setMMPositionFoc(() -> degreesToRotations(degrees))) @@ -587,31 +841,32 @@ public Command moveToDegrees(DoubleSupplier degrees) { } /** - * Runs to the specified Motion Magic position using FOC control. Will require different PID and - * feedforward configs + * Returns a {@link Command} that continuously moves the mechanism to the specified position + * using Motion Magic Torque Current FOC control (requires Phoenix Pro). + * + *

Equivalent to {@link #moveToRotations(DoubleSupplier)} — prefer that method for clarity. * - * @param rotations position in revolutions - * @return A Command that runs the mechanism to the specified position in revolutions using - * Motion Magic FOC control. + * @param rotations the target position in rotations + * @return a command that moves the mechanism to the given position */ public Command runFocRotations(DoubleSupplier rotations) { return run(() -> setMMPositionFoc(rotations)).withName(getName() + ".runFOCPosition"); } /** - * Stops the mechanism. + * Returns a {@link Command} that stops the mechanism and holds it stopped for its duration. * - * @return A Command that stops the mechanism. + * @return a command that stops the mechanism */ public Command runStop() { return run(this::stop).withName(getName() + ".runStop"); } /** - * Temporarily sets the mechanism to coast mode. The configuration is applied when the command - * is started and reverted when the command is ended. + * Returns a {@link Command} that sets the mechanism to coast mode while active, then reverts to + * brake mode when the command ends. Safe to run while the robot is disabled. * - * @return A Command that sets the mechanism to coast mode. + * @return a command that temporarily enables coast mode */ public Command coastMode() { return startEnd(() -> setBrakeMode(false), () -> setBrakeMode(true)) @@ -620,10 +875,10 @@ public Command coastMode() { } /** - * Sets the motor to brake mode if it is in coast mode. + * Returns a {@link Command} that sets the mechanism to brake mode if it is currently in coast + * mode. Safe to run while the robot is disabled. * - * @return A Command that sets the mechanism to brake mode if it is in coast mode, ignoring - * disable. + * @return a command that ensures brake mode is active */ public Command ensureBrakeMode() { return runOnce(() -> setBrakeMode(true)) @@ -636,21 +891,40 @@ public Command ensureBrakeMode() { .withName(getName() + ".ensureBrakeMode"); } + /** + * Returns a {@link Command} that applies new supply and stator current limits to the mechanism. + * + * @param supplyLimit the new supply current limit in amps + * @param statorLimit the new stator current limit in amps + * @return a command that updates the current limits + */ protected Command runCurrentLimits(DoubleSupplier supplyLimit, DoubleSupplier statorLimit) { return Commands.runOnce(() -> setCurrentLimits(supplyLimit, statorLimit)); } + // ── Motor Control (Protected) ────────────────────────────────────────────── + + /** + * Immediately applies new supply and stator current limits to the motor configuration. + * + * @param supplyLimit the new supply current limit in amps + * @param statorLimit the new stator current limit in amps + */ protected void setCurrentLimits(DoubleSupplier supplyLimit, DoubleSupplier statorLimit) { applyCurrentLimit(supplyLimit, statorLimit); } + /** Stops the motor output. Does nothing if the mechanism is not attached. */ protected void stop() { if (isAttached()) { motor.stopMotor(); } } - /** Sets the mechanism position of the motor to 0 */ + /** + * Sets the mechanism's reported position to zero (tares the motor encoder). Does nothing if the + * mechanism is not attached. + */ protected void tareMotor() { if (isAttached()) { setMotorPosition(() -> 0); @@ -658,9 +932,9 @@ protected void tareMotor() { } /** - * Sets the mechanism position of the motor + * Sets the motor's internal position register to the specified value without moving the motor. * - * @param rotations rotations + * @param rotations the position to write to the motor in rotations */ protected void setMotorPosition(DoubleSupplier rotations) { if (isAttached()) { @@ -669,61 +943,67 @@ protected void setMotorPosition(DoubleSupplier rotations) { } /** - * Closed-loop Velocity Motion Magic with torque control (requires Pro) + * Closed-loop velocity control using Motion Magic with Torque Current FOC (requires Phoenix + * Pro). * - * @param velocityRPS rotations per second + * @param velocityRPS the target velocity in rotations per second */ protected void setMMVelocityFOC(DoubleSupplier velocityRPS) { if (isAttached()) { - target = velocityRPS.getAsDouble(); - MotionMagicVelocityTorqueCurrentFOC mm = config.mmVelocityFOC.withVelocity(target); + velocityTarget = velocityRPS.getAsDouble(); + MotionMagicVelocityTorqueCurrentFOC mm = + config.mmVelocityFOC.withVelocity(velocityTarget); motor.setControl(mm); } } /** - * Closed-loop Velocity with torque control (requires Pro) + * Closed-loop velocity control using Torque Current FOC (requires Phoenix Pro). * - * @param velocityRPS rotations per second + * @param velocityRPS the target velocity in rotations per second */ protected void setVelocityTorqueCurrentFOC(DoubleSupplier velocityRPS) { if (isAttached()) { - target = velocityRPS.getAsDouble(); - VelocityTorqueCurrentFOC output = config.velocityTorqueCurrentFOC.withVelocity(target); + velocityTarget = velocityRPS.getAsDouble(); + VelocityTorqueCurrentFOC output = + config.velocityTorqueCurrentFOC.withVelocity(velocityTarget); motor.setControl(output); } } /** - * Closed-loop Velocity with torque control (requires Pro) + * Closed-loop velocity control using Torque Current FOC with an RPM input (requires Phoenix + * Pro). The RPM value is converted to RPS internally before being sent to the motor. * - * @param velocityRPS rotations per second + * @param velocityRPM the target velocity in revolutions per minute */ - protected void setVelocityTCFOCrpm(DoubleSupplier velocityRPS) { + protected void setVelocityTCFOCrpm(DoubleSupplier velocityRPM) { if (isAttached()) { - target = Conversions.RPMtoRPS(velocityRPS.getAsDouble()); - VelocityTorqueCurrentFOC output = config.velocityTorqueCurrentFOC.withVelocity(target); + velocityTarget = Conversions.RPMtoRPS(velocityRPM.getAsDouble()); + VelocityTorqueCurrentFOC output = + config.velocityTorqueCurrentFOC.withVelocity(velocityTarget); motor.setControl(output); } } /** - * Closed-loop velocity control with voltage compensation + * Closed-loop velocity control with voltage compensation. * - * @param velocityRPS rotations per second + * @param velocityRPS the target velocity in rotations per second */ protected void setVelocity(DoubleSupplier velocityRPS) { if (isAttached()) { - target = velocityRPS.getAsDouble(); - VelocityVoltage output = config.velocityControl.withVelocity(target); + velocityTarget = velocityRPS.getAsDouble(); + VelocityVoltage output = config.velocityControl.withVelocity(velocityTarget); motor.setControl(output); } } /** - * Closed-loop Position Motion Magic with torque control (requires Pro) + * Closed-loop position control using Motion Magic with Torque Current FOC (requires Phoenix + * Pro). * - * @param rotations rotations + * @param rotations the target position in rotations */ protected void setMMPositionFoc(DoubleSupplier rotations) { if (isAttached()) { @@ -734,13 +1014,13 @@ protected void setMMPositionFoc(DoubleSupplier rotations) { } /** - * Closed-loop Position Motion Magic with torque control (requires Pro). Dynamic allows you to - * set velocity, acceleration, and jerk during the command. + * Closed-loop position control using Dynamic Motion Magic with Torque Current FOC (requires + * Phoenix Pro). Trajectory parameters can be changed every loop cycle. * - * @param rotations The target position in rotations. - * @param velocity The maximum velocity in rotations per second. - * @param acceleration The maximum acceleration in rotations per second squared. - * @param jerk The maximum jerk in rotations per second cubed. + * @param rotations the target position in rotations + * @param velocity the cruise velocity in rotations per second + * @param acceleration the acceleration in rotations per second squared + * @param jerk the jerk in rotations per second cubed */ protected void setDynMMPositionFoc( DoubleSupplier rotations, @@ -760,13 +1040,13 @@ protected void setDynMMPositionFoc( } /** - * Closed-loop Position Motion Magic with voltage control. Dynamic allows you to set velocity, - * acceleration, and jerk during the command. + * Closed-loop position control using Dynamic Motion Magic with voltage compensation. Trajectory + * parameters can be changed every loop cycle. * - * @param rotations The target position in rotations. - * @param velocity The maximum velocity in rotations per second. - * @param acceleration The maximum acceleration in rotations per second squared. - * @param jerk The maximum jerk in rotations per second cubed. + * @param rotations the target position in rotations + * @param velocity the cruise velocity in rotations per second + * @param acceleration the acceleration in rotations per second squared + * @param jerk the jerk in rotations per second cubed */ protected void setDynMMPositionVoltage( DoubleSupplier rotations, @@ -786,19 +1066,20 @@ protected void setDynMMPositionVoltage( } /** - * Closed-loop Position Motion Magic + * Closed-loop position control using Motion Magic with voltage compensation (slot 0). * - * @param rotations rotations + * @param rotations the target position in rotations */ protected void setMMPosition(DoubleSupplier rotations) { setMMPosition(rotations, 0); } /** - * Closed-loop Position Motion Magic using a slot other than 0 + * Closed-loop position control using Motion Magic with voltage compensation and an explicit + * PID/FF gain slot. * - * @param rotations rotations - * @param slot gains slot + * @param rotations the target position in rotations + * @param slot the gain slot to use (0, 1, or 2) */ public void setMMPosition(DoubleSupplier rotations, int slot) { if (isAttached()) { @@ -809,10 +1090,13 @@ public void setMMPosition(DoubleSupplier rotations, int slot) { } } + // ── Motor Control (Public) ───────────────────────────────────────────────── + /** - * Open-loop Percent output control with voltage compensation + * Open-loop percent output control with voltage compensation. The output voltage is {@code + * percent × voltageCompSaturation}. * - * @param percent fractional units between -1 and +1 + * @param percent fractional output between -1 and +1 */ public void setPercentOutput(DoubleSupplier percent) { if (isAttached()) { @@ -824,9 +1108,10 @@ public void setPercentOutput(DoubleSupplier percent) { } /** - * Open-loop voltage control + * Open-loop voltage control — applies the requested voltage directly without compensation + * scaling. * - * @param voltage volts + * @param voltage the desired voltage in volts */ public void setVoltageOutput(DoubleSupplier voltage) { if (isAttached()) { @@ -836,9 +1121,10 @@ public void setVoltageOutput(DoubleSupplier voltage) { } /** - * Open-loop voltage control that ignores software limits + * Open-loop voltage control that ignores software limit switches. Use with caution — this can + * drive the mechanism past its configured travel limits. * - * @param voltage volts + * @param voltage the desired voltage in volts */ public void setVoltageOutputNoSoftLimit(DoubleSupplier voltage) { if (isAttached()) { @@ -850,6 +1136,11 @@ public void setVoltageOutputNoSoftLimit(DoubleSupplier voltage) { } } + /** + * Applies a torque current setpoint using FOC control (requires Phoenix Pro). + * + * @param current the desired torque current in amps + */ public void setTorqueCurrentFoc(DoubleSupplier current) { if (isAttached()) { TorqueCurrentFOC output = config.torqueCurrentFOC.withOutput(current.getAsDouble()); @@ -857,6 +1148,14 @@ public void setTorqueCurrentFoc(DoubleSupplier current) { } } + // ── Hardware Configuration ───────────────────────────────────────────────── + + /** + * Sets the motor's neutral mode to brake or coast and immediately applies the change to + * hardware. + * + * @param isInBrake {@code true} to set brake mode; {@code false} to set coast mode + */ public void setBrakeMode(boolean isInBrake) { if (isAttached()) { config.configNeutralBrakeMode(isInBrake); @@ -864,6 +1163,12 @@ public void setBrakeMode(boolean isInBrake) { } } + /** + * Enables or disables the reverse software limit switch and immediately applies the change. The + * threshold is read from the current configuration. + * + * @param enabled {@code true} to enable the reverse soft limit; {@code false} to disable it + */ public void toggleReverseSoftLimit(boolean enabled) { if (isAttached()) { double threshold = config.talonConfig.SoftwareLimitSwitch.ReverseSoftLimitThreshold; @@ -872,6 +1177,13 @@ public void toggleReverseSoftLimit(boolean enabled) { } } + /** + * Enables or disables a forward/reverse torque current limit and immediately applies the + * change. When disabled, the peak torque current is reset to ±300 A (effectively unlimited). + * + * @param enabledLimit the torque current limit in amps when {@code enabled} is {@code true} + * @param enabled {@code true} to apply the limit; {@code false} to remove it + */ public void toggleTorqueCurrentLimit(DoubleSupplier enabledLimit, boolean enabled) { if (isAttached()) { if (enabled) { @@ -887,6 +1199,12 @@ public void toggleTorqueCurrentLimit(DoubleSupplier enabledLimit, boolean enable } } + /** + * Enables or disables the supply current limit and immediately applies the change. + * + * @param enabledLimit the supply current limit in amps + * @param enabled {@code true} to enable the limit; {@code false} to disable it + */ public void toggleSupplyCurrentLimit(DoubleSupplier enabledLimit, boolean enabled) { if (isAttached()) { if (enabled) { @@ -899,10 +1217,17 @@ public void toggleSupplyCurrentLimit(DoubleSupplier enabledLimit, boolean enable } } + /** + * Applies new supply and stator current limits if the requested values differ from the + * currently configured limits. The update is retried up to 10 times on failure. + * + * @param supplyLimit the new supply current limit in amps + * @param statorLimit the new stator current limit in amps + */ public void applyCurrentLimit(DoubleSupplier supplyLimit, DoubleSupplier statorLimit) { if (isAttached()) { if (config.talonConfig.CurrentLimits.StatorCurrentLimit != statorLimit.getAsDouble() - && config.talonConfig.CurrentLimits.SupplyCurrentLimit + || config.talonConfig.CurrentLimits.SupplyCurrentLimit != supplyLimit.getAsDouble()) { config.configSupplyCurrentLimit(Math.abs(supplyLimit.getAsDouble()), true); config.configStatorCurrentLimit(Math.abs(statorLimit.getAsDouble()), true); @@ -923,6 +1248,17 @@ public void applyCurrentLimit(DoubleSupplier supplyLimit, DoubleSupplier statorL } } + // ── Diagnostic Commands ──────────────────────────────────────────────────── + + /** + * Returns a {@link Command} that measures the average stator current over its runtime and fires + * a warning {@link Alert} if the average deviates from {@code expectedCurrent} by more than + * {@code tolerance}. + * + * @param expectedCurrent the expected average stator current in amps + * @param tolerance the maximum acceptable deviation in amps + * @return a diagnostic command that checks average current + */ public Command checkAvgCurrent(DoubleSupplier expectedCurrent, DoubleSupplier tolerance) { return new Command() { double totalCurrent = 0; @@ -958,6 +1294,13 @@ public void end(boolean interrupted) { }; } + /** + * Returns a {@link Command} that tracks the peak stator current over its runtime and fires a + * warning {@link Alert} if the peak exceeds {@code expectedCurrent}. + * + * @param expectedCurrent the maximum acceptable peak stator current in amps + * @return a diagnostic command that checks peak current + */ public Command checkMaxCurrent(DoubleSupplier expectedCurrent) { return new Command() { double maxCurrent = 0; @@ -991,6 +1334,14 @@ public void end(boolean interrupted) { }; } + /** + * Returns a {@link Command} that tracks the peak stator current over its runtime and fires a + * warning {@link Alert} if the peak never reaches {@code expectedCurrent}. Use this to verify + * that a mechanism drew at least the expected minimum load. + * + * @param expectedCurrent the minimum acceptable peak stator current in amps + * @return a diagnostic command that checks whether a minimum current threshold was reached + */ public Command checkMinThresholdCurrent(DoubleSupplier expectedCurrent) { return new Command() { double maxCurrent = 0; @@ -1024,12 +1375,40 @@ public void end(boolean interrupted) { }; } + // ── Nested Classes ───────────────────────────────────────────────────────── + + /** + * Configuration for a TalonFX follower motor that mirrors the leader. + * + *

A follower automatically copies the leader's output. Set {@code opposeLeader} to {@link + * MotorAlignmentValue#Opposed} when the physical motor is mounted in the opposite direction and + * must spin in reverse to produce the same mechanism motion. + */ public static class FollowerConfig { + + /** Human-readable name for this follower motor (used in alerts and logging). */ @Getter private String name; + + /** CAN bus device ID and bus name for this follower motor. */ @Getter private CanDeviceId id; + + /** Whether hardware is attached for this follower. */ @Getter private boolean attached = true; + + /** + * Alignment of the follower relative to the leader. Use {@link MotorAlignmentValue#Opposed} + * when the follower is physically mounted in the opposite direction. + */ @Getter private MotorAlignmentValue opposeLeader = MotorAlignmentValue.Aligned; + /** + * Creates a follower motor configuration. + * + * @param name human-readable name for this follower + * @param id CAN device ID + * @param canbus CAN bus name (e.g., {@code "rio"} or {@code "canivore"}) + * @param opposeLeader alignment relative to the leader motor + */ public FollowerConfig( String name, int id, String canbus, MotorAlignmentValue opposeLeader) { this.name = name; @@ -1038,18 +1417,52 @@ public FollowerConfig( } } + /** + * Configuration for a {@link Mechanism}, encapsulating the TalonFX hardware configuration, + * control request objects, and mechanism-level parameters (gear ratio, soft limits, PID/FF + * gains, Motion Magic profile, current limits, etc.). + * + *

Subclass this in each concrete mechanism and call the {@code config*()} helpers in the + * constructor to set mechanism-specific parameters before passing the config to the {@link + * Mechanism} constructor. + */ public static class Config { + + /** Human-readable name for this mechanism (used in logging and alerts). */ @Getter private String name; + + /** + * Whether physical hardware is attached. Set to {@code false} to run in simulation-only + * mode. + */ @Getter @Setter private boolean attached = true; + + /** CAN bus device ID and bus name for the leader motor. */ @Getter private CanDeviceId id; + + /** Full TalonFX hardware configuration applied to the leader motor on startup. */ @Getter @Setter protected TalonFXConfiguration talonConfig; + + /** Total number of motors (leader + followers). */ @Getter private int numMotors = 1; - @Getter private double voltageCompSaturation = 12.0; // 12V by default + + /** + * Voltage compensation saturation value used by {@link Mechanism#setPercentOutput}. + * Defaults to 12 V. + */ + @Getter private double voltageCompSaturation = 12.0; + + /** Minimum mechanism position in rotations (used for range calculations). */ @Getter private double minRotations = 0; + + /** Maximum mechanism position in rotations (used for range calculations). */ @Getter private double maxRotations = 1; + /** Configurations for follower motors. Empty by default (no followers). */ @Getter private FollowerConfig[] followerConfigs = new FollowerConfig[0]; + // Pre-built control request objects — reused each loop to avoid GC pressure. + @Getter private MotionMagicVelocityTorqueCurrentFOC mmVelocityFOC = new MotionMagicVelocityTorqueCurrentFOC(0); @@ -1081,9 +1494,19 @@ public static class Config { @Getter private TorqueCurrentFOC torqueCurrentFOC = new TorqueCurrentFOC(0); - // Percent Output control using percentage of supply voltage. Should normally use VoltageOut + /** Percent (duty-cycle) output control — prefer {@link #voltageControl} in most cases. */ @Getter private DutyCycleOut percentOutput = new DutyCycleOut(0); + // ── Constructor ─────────────────────────────────────────────────────── + + /** + * Creates a base mechanism configuration with default Talon settings. Hardware limit + * switches are disabled by default. + * + * @param name human-readable name for this mechanism + * @param id CAN device ID of the leader motor + * @param canbus CAN bus name (e.g., {@code "rio"} or {@code "canivore"}) + */ public Config(String name, int id, String canbus) { this.name = name; this.id = new CanDeviceId(id, canbus); @@ -1094,6 +1517,14 @@ public Config(String name, int id, String canbus) { talonConfig.HardwareLimitSwitch.ReverseLimitEnable = false; } + // ── Config Helpers ──────────────────────────────────────────────────── + + /** + * Applies the current {@link TalonFXConfiguration} to the given motor and reports a warning + * to the DriverStation if the apply fails. + * + * @param talon the TalonFX motor to configure + */ public void applyTalonConfig(TalonFX talon) { StatusCode result = talon.getConfigurator().apply(talonConfig); if (!result.isOK()) { @@ -1102,30 +1533,62 @@ public void applyTalonConfig(TalonFX talon) { } } + /** + * Sets the follower motor configurations. + * + * @param followers one or more {@link FollowerConfig} objects describing follower motors + */ public void setFollowerConfigs(FollowerConfig... followers) { followerConfigs = followers; } + /** + * Sets the voltage compensation saturation voltage used by percent-output control. + * + * @param voltageCompSaturation the saturation voltage in volts (typically 12.0) + */ public void configVoltageCompensation(double voltageCompSaturation) { this.voltageCompSaturation = voltageCompSaturation; } + /** + * Configures the motor output as counter-clockwise positive (default for most mechanisms + * when viewed from the shaft end). + */ public void configCounterClockwise_Positive() { talonConfig.MotorOutput.Inverted = InvertedValue.CounterClockwise_Positive; } + /** Configures the motor output as clockwise positive (inverted relative to the default). */ public void configClockwise_Positive() { talonConfig.MotorOutput.Inverted = InvertedValue.Clockwise_Positive; } + /** + * Sets the peak forward output voltage. + * + * @param voltageLimit maximum forward voltage in volts + */ public void configForwardVoltageLimit(double voltageLimit) { talonConfig.Voltage.PeakForwardVoltage = voltageLimit; } + /** + * Sets the peak reverse output voltage. + * + * @param voltageLimit maximum reverse voltage in volts + */ public void configReverseVoltageLimit(double voltageLimit) { talonConfig.Voltage.PeakReverseVoltage = voltageLimit; } + /** + * Configures the supply current limit. The absolute value of {@code supplyLimit} is used, + * so negative values are automatically corrected. + * + * @param supplyLimit the supply current limit in amps + * @param enabled {@code true} to enable the limit + */ public void configSupplyCurrentLimit(double supplyLimit, boolean enabled) { if (supplyLimit < 0) { supplyLimit = -supplyLimit; @@ -1134,6 +1597,13 @@ public void configSupplyCurrentLimit(double supplyLimit, boolean enabled) { talonConfig.CurrentLimits.SupplyCurrentLimitEnable = enabled; } + /** + * Configures the stator current limit. The absolute value of {@code statorLimit} is used, + * so negative values are automatically corrected. + * + * @param statorLimit the stator current limit in amps + * @param enabled {@code true} to enable the limit + */ public void configStatorCurrentLimit(double statorLimit, boolean enabled) { if (statorLimit < 0) { statorLimit = -statorLimit; @@ -1142,6 +1612,12 @@ public void configStatorCurrentLimit(double statorLimit, boolean enabled) { talonConfig.CurrentLimits.StatorCurrentLimitEnable = enabled; } + /** + * Sets the peak forward torque current limit. The absolute value is used so negative inputs + * are corrected automatically. + * + * @param currentLimit peak forward torque current in amps + */ public void configForwardTorqueCurrentLimit(double currentLimit) { if (currentLimit < 0) { currentLimit = -currentLimit; @@ -1149,6 +1625,12 @@ public void configForwardTorqueCurrentLimit(double currentLimit) { talonConfig.TorqueCurrent.PeakForwardTorqueCurrent = currentLimit; } + /** + * Sets the peak reverse torque current limit. The value is forced negative so positive + * inputs are corrected automatically. + * + * @param currentLimit peak reverse torque current in amps (sign is corrected if positive) + */ public void configReverseTorqueCurrentLimit(double currentLimit) { if (currentLimit > 0) { currentLimit = -currentLimit; @@ -1156,38 +1638,84 @@ public void configReverseTorqueCurrentLimit(double currentLimit) { talonConfig.TorqueCurrent.PeakReverseTorqueCurrent = currentLimit; } + /** + * Sets the lower supply current limit, used to reduce dissipation after the upper limit has + * been triggered. + * + * @param currentLimit the lower supply current limit in amps + */ public void configLowerSupplyCurrentLimit(double currentLimit) { talonConfig.CurrentLimits.SupplyCurrentLowerLimit = currentLimit; } + /** + * Sets the time window for the lower supply current limit. + * + * @param time the time in seconds + */ public void configLowerSupplyCurrentTime(double time) { talonConfig.CurrentLimits.SupplyCurrentLowerTime = time; } + /** + * Sets the duty-cycle neutral deadband. Outputs below this magnitude are treated as zero. + * + * @param deadband deadband as a fraction of full output (e.g., {@code 0.001}) + */ public void configNeutralDeadband(double deadband) { talonConfig.MotorOutput.DutyCycleNeutralDeadband = deadband; } + /** + * Sets the peak forward and reverse duty-cycle output limits. + * + * @param forward maximum forward output (0 to 1) + * @param reverse maximum reverse output (-1 to 0) + */ public void configPeakOutput(double forward, double reverse) { talonConfig.MotorOutput.PeakForwardDutyCycle = forward; talonConfig.MotorOutput.PeakReverseDutyCycle = reverse; } + /** + * Configures the forward software limit switch. + * + * @param threshold the position threshold in rotations + * @param enabled {@code true} to enable the limit + */ public void configForwardSoftLimit(double threshold, boolean enabled) { talonConfig.SoftwareLimitSwitch.ForwardSoftLimitThreshold = threshold; talonConfig.SoftwareLimitSwitch.ForwardSoftLimitEnable = enabled; } + /** + * Configures the reverse software limit switch. + * + * @param threshold the position threshold in rotations + * @param enabled {@code true} to enable the limit + */ public void configReverseSoftLimit(double threshold, boolean enabled) { talonConfig.SoftwareLimitSwitch.ReverseSoftLimitThreshold = threshold; talonConfig.SoftwareLimitSwitch.ReverseSoftLimitEnable = enabled; } + /** + * Enables or disables continuous position wrap-around for closed-loop control. Useful for + * mechanisms that rotate continuously (e.g., swerve azimuth). + * + * @param enabled {@code true} to enable continuous wrap + */ public void configContinuousWrap(boolean enabled) { talonConfig.ClosedLoopGeneral.ContinuousWrap = enabled; } - // Configure optional motion magic velocity parameters + /** + * Configures optional Motion Magic velocity parameters (acceleration and feed-forward) for + * both FOC and voltage velocity control requests. + * + * @param acceleration the velocity acceleration in rotations per second squared + * @param feedforward the feed-forward term applied during velocity control + */ public void configMotionMagicVelocity(double acceleration, double feedforward) { mmVelocityFOC = mmVelocityFOC.withAcceleration(acceleration).withFeedForward(feedforward); @@ -1195,28 +1723,54 @@ public void configMotionMagicVelocity(double acceleration, double feedforward) { mmVelocityVoltage.withAcceleration(acceleration).withFeedForward(feedforward); } - // Configure optional motion magic position parameters + /** + * Configures the feed-forward term for Motion Magic position control requests. + * + * @param feedforward the feed-forward term to apply during position control + */ public void configMotionMagicPosition(double feedforward) { mmPositionFOC = mmPositionFOC.withFeedForward(feedforward); mmPositionVoltage = mmPositionVoltage.withFeedForward(feedforward); } + /** + * Configures the Motion Magic cruise velocity, acceleration, and jerk limits. + * + * @param cruiseVelocity maximum cruise velocity in rotations per second + * @param acceleration maximum acceleration in rotations per second squared + * @param jerk maximum jerk in rotations per second cubed + */ public void configMotionMagic(double cruiseVelocity, double acceleration, double jerk) { talonConfig.MotionMagic.MotionMagicCruiseVelocity = cruiseVelocity; talonConfig.MotionMagic.MotionMagicAcceleration = acceleration; talonConfig.MotionMagic.MotionMagicJerk = jerk; } - // This is the ratio of rotor rotations to the mechanism's output. - // If a remote sensor is used this a ratio of sensor rotations to the mechanism's output. + /** + * Configures the sensor-to-mechanism gear ratio. This is the ratio of rotor rotations to + * one full mechanism output rotation (or sensor rotations if a remote sensor is used). + * + * @param gearRatio the gear ratio (e.g., {@code 11.25} means 11.25 rotor turns per output + * rotation) + */ public void configGearRatio(double gearRatio) { talonConfig.Feedback.SensorToMechanismRatio = gearRatio; } + /** + * Returns the currently configured sensor-to-mechanism gear ratio. + * + * @return the gear ratio + */ public double getGearRatio() { return talonConfig.Feedback.SensorToMechanismRatio; } + /** + * Sets the motor neutral mode to brake or coast. + * + * @param isInBrake {@code true} for brake mode; {@code false} for coast mode + */ public void configNeutralBrakeMode(boolean isInBrake) { if (isInBrake) { talonConfig.MotorOutput.NeutralMode = NeutralModeValue.Brake; @@ -1226,54 +1780,90 @@ public void configNeutralBrakeMode(boolean isInBrake) { } /** - * Defaults to slot 0 + * Configures PID gains in slot 0. * - * @param kP - * @param kI - * @param kD + * @param kP proportional gain + * @param kI integral gain + * @param kD derivative gain */ public void configPIDGains(double kP, double kI, double kD) { configPIDGains(0, kP, kI, kD); } + /** + * Configures PID gains in the specified slot. + * + * @param slot the gain slot (0, 1, or 2) + * @param kP proportional gain + * @param kI integral gain + * @param kD derivative gain + */ public void configPIDGains(int slot, double kP, double kI, double kD) { talonConfigFeedbackPID(slot, kP, kI, kD); } /** - * Defaults to slot 0 + * Configures feed-forward gains in slot 0. * - * @param kS - * @param kV - * @param kA - * @param kG + * @param kS static friction compensation (volts or amps) + * @param kV velocity feed-forward gain + * @param kA acceleration feed-forward gain + * @param kG gravity/load compensation gain */ public void configFeedForwardGains(double kS, double kV, double kA, double kG) { configFeedForwardGains(0, kS, kV, kA, kG); } + /** + * Configures feed-forward gains in the specified slot. + * + * @param slot the gain slot (0, 1, or 2) + * @param kS static friction compensation (volts or amps) + * @param kV velocity feed-forward gain + * @param kA acceleration feed-forward gain + * @param kG gravity/load compensation gain + */ public void configFeedForwardGains(int slot, double kS, double kV, double kA, double kG) { talonConfigFeedForward(slot, kV, kA, kS, kG); } + /** + * Configures the feedback sensor source using a rotorOffset of {@code 0}. + * + * @param source the feedback sensor source (e.g., remote CANcoder) + */ public void configFeedbackSensorSource(FeedbackSensorSourceValue source) { configFeedbackSensorSource(source, 0); } + /** + * Configures the feedback sensor source and its rotational offset. + * + * @param source the feedback sensor source + * @param offset the feedback rotor offset in rotations + */ public void configFeedbackSensorSource(FeedbackSensorSourceValue source, double offset) { talonConfig.Feedback.FeedbackSensorSource = source; talonConfig.Feedback.FeedbackRotorOffset = offset; } /** - * Defaults to slot 0 + * Configures the gravity compensation type in slot 0. * - * @param isArm + * @param isArm {@code true} for {@link GravityTypeValue#Arm_Cosine} (rotating arm); {@code + * false} for {@link GravityTypeValue#Elevator_Static} (elevator) */ public void configGravityType(boolean isArm) { configGravityType(0, isArm); } + /** + * Configures the gravity compensation type in the specified slot. + * + * @param slot the gain slot (0, 1, or 2) + * @param isArm {@code true} for {@link GravityTypeValue#Arm_Cosine} (rotating arm); {@code + * false} for {@link GravityTypeValue#Elevator_Static} (elevator) + */ public void configGravityType(int slot, boolean isArm) { GravityTypeValue gravityType = isArm ? GravityTypeValue.Arm_Cosine : GravityTypeValue.Elevator_Static; @@ -1288,7 +1878,29 @@ public void configGravityType(int slot, boolean isArm) { } } - // Configure the TalonFXConfiguration feed forward gains + /** + * Sets the minimum and maximum rotation limits for the mechanism. These bounds are used by + * unit-conversion helpers such as {@link Mechanism#percentToRotations}. + * + * @param minRotation the minimum position in rotations + * @param maxRotation the maximum position in rotations + */ + protected void configMinMaxRotations(double minRotation, double maxRotation) { + this.minRotations = minRotation; + this.maxRotations = maxRotation; + } + + // ── Private Helpers ─────────────────────────────────────────────────── + + /** + * Applies feed-forward gains (kV, kA, kS, kG) to the specified TalonFX slot. + * + * @param slot the gain slot (0, 1, or 2) + * @param kV velocity feed-forward + * @param kA acceleration feed-forward + * @param kS static friction compensation + * @param kG gravity compensation + */ private void talonConfigFeedForward(int slot, double kV, double kA, double kS, double kG) { if (slot == 0) { talonConfig.Slot0.kV = kV; @@ -1310,6 +1922,14 @@ private void talonConfigFeedForward(int slot, double kV, double kA, double kS, d } } + /** + * Applies PID gains (kP, kI, kD) to the specified TalonFX slot. + * + * @param slot the gain slot (0, 1, or 2) + * @param kP proportional gain + * @param kI integral gain + * @param kD derivative gain + */ private void talonConfigFeedbackPID(int slot, double kP, double kI, double kD) { if (slot == 0) { talonConfig.Slot0.kP = kP; @@ -1327,16 +1947,5 @@ private void talonConfigFeedbackPID(int slot, double kP, double kI, double kD) { DriverStation.reportWarning("MechConfig: Invalid Feedback slot", false); } } - - /** - * Sets the minimum and maximum motor rotations - * - * @param minRotation - * @param maxRotation - */ - protected void configMinMaxRotations(double minRotation, double maxRotation) { - this.minRotations = minRotation; - this.maxRotations = maxRotation; - } } } diff --git a/src/main/java/frc/spectrumLib/sim/ArmConfig.java b/src/main/java/frc/spectrumLib/sim/ArmConfig.java index 7ee1809a..377fcf1c 100644 --- a/src/main/java/frc/spectrumLib/sim/ArmConfig.java +++ b/src/main/java/frc/spectrumLib/sim/ArmConfig.java @@ -5,33 +5,71 @@ import lombok.Getter; import lombok.Setter; +/** + * Configuration data for an arm simulation. Stores physical properties, display settings, and + * optional mount attachment used by {@link ArmSim}. + */ public class ArmConfig { + /** Number of Kraken X60 motors driving the arm. */ @Getter @Setter private int numMotors = 1; + /** Initial X position of the arm pivot in the Mechanism2d canvas (metres). */ @Getter @Setter private double initialX = 0.7; + /** Initial Y position of the arm pivot in the Mechanism2d canvas (metres). */ @Getter @Setter private double initialY = 0.3; + /** Current X position of the arm pivot used during simulation updates (metres). */ @Getter @Setter private double pivotX = 0.7; + /** Current Y position of the arm pivot used during simulation updates (metres). */ @Getter @Setter private double pivotY = 0.3; - - @Getter @Setter - private double ratio = - 50; // the number of rotations it takes for the mechanism to do one revolution - + /** Motor rotations required for one full revolution of the arm mechanism. */ + @Getter @Setter private double ratio = 50; + /** Visual length of the arm ligament in the Mechanism2d canvas (metres). */ @Getter @Setter private double length = 0.5; + /** Moment of inertia used by the physics simulation (kg·m²). */ @Getter @Setter private double simMOI = 1.2; + /** + * Distance from the pivot to the arm's centre of gravity used by the physics simulation + * (metres). + */ @Getter @Setter private double simCGLength = 0.2; + /** Minimum allowable arm angle (radians). */ @Getter @Setter private double minAngle = Math.toRadians(-60); + /** Maximum allowable arm angle (radians). */ @Getter @Setter private double maxAngle = Math.toRadians(90); + /** Arm angle at the start of the simulation (radians). */ @Getter @Setter private double startingAngle = Math.toRadians(90); + /** Whether the physics simulation should apply gravitational force to the arm. */ @Getter @Setter private boolean simulateGravity = true; + /** Whether this arm is attached to a parent {@link Mount}. */ @Getter private boolean mounted = false; + /** The parent mount this arm is attached to, or {@code null} if not mounted. */ @Getter private Mount mount; + /** X position of the mount at simulation start (metres). */ @Getter private double initMountX; + /** Y position of the mount at simulation start (metres). */ @Getter private double initMountY; + /** Angle of the mount at simulation start (radians). */ @Getter private double initMountAngle; + /** + * When {@code true} the arm's visual angle is expressed in absolute robot-frame degrees; when + * {@code false} it is relative to the parent mount's current angle. + */ @Getter private boolean absAngle; + /** Color used to draw the arm ligament in the Mechanism2d canvas. */ @Getter private Color8Bit color = new Color8Bit(Color.kBlue); + /** + * Creates an ArmConfig with the required physical and display parameters. Angle arguments are + * specified in degrees and stored internally as radians. + * + * @param initialX initial X position of the pivot in the Mechanism2d canvas (metres) + * @param initialY initial Y position of the pivot in the Mechanism2d canvas (metres) + * @param ratio motor rotations per one full arm revolution + * @param length visual arm length in the Mechanism2d canvas (metres) + * @param minAngleDegrees minimum allowable arm angle in degrees + * @param maxAngleDegrees maximum allowable arm angle in degrees + * @param startingAngleDegrees initial arm angle in degrees + */ public ArmConfig( double initialX, double initialY, @@ -51,11 +89,31 @@ public ArmConfig( this.pivotY = initialY; } + /** + * Sets the arm's display color in the Mechanism2d canvas. + * + * @param color the color to use + * @return this config for chaining + */ public ArmConfig setColor(Color8Bit color) { this.color = color; return this; } + public ArmConfig setSimulatedGravity(boolean simulateGravity) { + this.simulateGravity = simulateGravity; + return this; + } + + /** + * Attaches this arm to a {@link LinearSim} mount so its pivot tracks the linear stage's + * position. + * + * @param sim the linear stage to mount onto, or {@code null} to leave unmounted + * @param fixedAngle when {@code true} the arm angle is treated as absolute; when {@code false} + * it is relative to the mount's current angle + * @return this config for chaining + */ public ArmConfig setMount(LinearSim sim, boolean fixedAngle) { if (sim != null) { mounted = true; @@ -68,6 +126,15 @@ public ArmConfig setMount(LinearSim sim, boolean fixedAngle) { return this; } + /** + * Attaches this arm to a parent {@link ArmSim} mount so its pivot tracks the parent arm's tip + * position. + * + * @param sim the parent arm to mount onto, or {@code null} to leave unmounted + * @param absAngle when {@code true} the arm angle is expressed in the absolute robot frame; + * when {@code false} it is relative to the parent arm's current angle + * @return this config for chaining + */ public ArmConfig setMount(ArmSim sim, boolean absAngle) { if (sim != null) { mounted = true; diff --git a/src/main/java/frc/spectrumLib/sim/ArmSim.java b/src/main/java/frc/spectrumLib/sim/ArmSim.java index cd836a20..8b6b11b9 100644 --- a/src/main/java/frc/spectrumLib/sim/ArmSim.java +++ b/src/main/java/frc/spectrumLib/sim/ArmSim.java @@ -10,16 +10,32 @@ import edu.wpi.first.wpilibj.smartdashboard.MechanismRoot2d; import lombok.Getter; +/** + * WPILib-backed simulation of a single-jointed arm driven by one or more Kraken X60 motors. Updates + * the TalonFX sim state each robot period and animates the arm in a {@link Mechanism2d} canvas. + * Implements {@link Mount} so other mechanisms can be attached to this arm's tip, and implements + * {@link Mountable} so this arm can itself be attached to a parent mount. + */ public class ArmSim implements Mount, Mountable { private SingleJointedArmSim armSim; + /** Configuration containing physical properties and display settings for this arm. */ @Getter private ArmConfig config; private MechanismRoot2d armPivot; private MechanismLigament2d armMech2d; private TalonFXSimState armMotorSim; + /** Always {@link MountType#ARM}; used by child mechanisms to determine positioning logic. */ @Getter private final MountType mountType = MountType.ARM; + /** + * Creates and registers an arm simulation. + * + * @param config physical and display configuration for the arm + * @param mech the Mechanism2d canvas to draw the arm on + * @param armMotorSim the TalonFX sim state of the motor driving the arm + * @param name unique name prefix used for Mechanism2d element labels + */ public ArmSim(ArmConfig config, Mechanism2d mech, TalonFXSimState armMotorSim, String name) { this.config = config; this.armMotorSim = armMotorSim; @@ -31,7 +47,7 @@ public ArmSim(ArmConfig config, Mechanism2d mech, TalonFXSimState armMotorSim, S config.getSimCGLength(), config.getMinAngle(), config.getMaxAngle(), - false, // Simulate gravity (change back to true) + config.isSimulateGravity(), config.getStartingAngle()); armPivot = mech.getRoot(name + " Arm Pivot", config.getPivotX(), config.getPivotY()); @@ -45,6 +61,10 @@ public ArmSim(ArmConfig config, Mechanism2d mech, TalonFXSimState armMotorSim, S config.getColor())); } + /** + * Advances the arm physics simulation by one robot period, updates the TalonFX rotor position + * and velocity, and refreshes the Mechanism2d visualization. + */ public void simulationPeriodic() { // armMotorSim.setSupplyVoltage(RobotController.getBatteryVoltage()); armSim.setInput(armMotorSim.getMotorVoltage()); @@ -82,18 +102,39 @@ public void simulationPeriodic() { armPivot.setPosition(config.getPivotX(), config.getPivotY()); } + /** + * Returns the current arm angle from the WPILib physics simulation. + * + * @return arm angle in radians + */ public double getAngleRads() { return armSim.getAngleRads(); } + /** + * Returns how far the pivot has moved horizontally from its initial position. + * + * @return horizontal displacement in metres + */ public double getDisplacementX() { return config.getPivotX() - config.getInitialX(); } + /** + * Returns how far the pivot has moved vertically from its initial position. + * + * @return vertical displacement in metres + */ public double getDisplacementY() { return config.getPivotY() - config.getInitialY(); } + /** + * Returns the effective arm angle accounting for the parent mount's angle when mounted and not + * using an absolute angle reference. + * + * @return effective arm angle in radians + */ public double getAngle() { if (config.isMounted()) { if (config.isAbsAngle()) { @@ -105,10 +146,20 @@ public double getAngle() { return getAngleRads(); } + /** + * Returns the X coordinate of the arm pivot, used by child mechanisms as their mount point. + * + * @return pivot X position in metres + */ public double getMountX() { return config.getPivotX(); } + /** + * Returns the Y coordinate of the arm pivot, used by child mechanisms as their mount point. + * + * @return pivot Y position in metres + */ public double getMountY() { return config.getPivotY(); } diff --git a/src/main/java/frc/spectrumLib/sim/Circle.java b/src/main/java/frc/spectrumLib/sim/Circle.java index cff55b53..5318a378 100644 --- a/src/main/java/frc/spectrumLib/sim/Circle.java +++ b/src/main/java/frc/spectrumLib/sim/Circle.java @@ -9,6 +9,10 @@ import lombok.Getter; import lombok.Setter; +/** + * Renders a filled circle in a {@link Mechanism2d} canvas using evenly-spaced radial ligaments. + * Used by {@link RollerSim} to visualise the roller's spin state and color. + */ public class Circle { private MechanismRoot2d rollerAxle; @@ -16,13 +20,27 @@ public class Circle { @SuppressWarnings("unused") private MechanismLigament2d rollerViz; + /** Array of radial ligaments that together form the circle outline. */ @Getter private MechanismLigament2d[] circleBackground; + /** Number of radial lines used to approximate the circle. */ @Getter private int backgroundLines; + private double diameterInches; private MechanismRoot2d root; + /** Color applied to all background lines when the circle is drawn. */ @Setter private Color8Bit color = new Color8Bit(Color.kBlack); + /** Label prefix used when naming Mechanism2d elements. */ @Setter private String name; + /** + * Creates a Circle and immediately draws the radial background lines. + * + * @param backgroundLines number of evenly-spaced radial lines used to approximate the circle + * @param diameterInches diameter of the circle in inches + * @param name label prefix for Mechanism2d element names + * @param root the Mechanism2d root the circle is attached to + * @param mech the parent Mechanism2d canvas + */ public Circle( int backgroundLines, double diameterInches, @@ -38,6 +56,16 @@ public Circle( drawCircle(); } + /** + * Creates a Circle with a specified color and immediately draws the radial background lines. + * + * @param mech the parent Mechanism2d canvas + * @param backgroundLines number of evenly-spaced radial lines used to approximate the circle + * @param diameterInches diameter of the circle in inches + * @param name label prefix for Mechanism2d element names + * @param root the Mechanism2d root the circle is attached to + * @param color the initial color applied to every background line + */ public Circle( Mechanism2d mech, int backgroundLines, @@ -49,6 +77,10 @@ public Circle( this.color = color; } + /** + * Creates and attaches all radial background ligaments that form the circle, distributing them + * evenly around 360 degrees. + */ public void drawCircle() { for (int i = 0; i < backgroundLines; i++) { circleBackground[i] = @@ -62,6 +94,10 @@ public void drawCircle() { } } + /** + * Appends a short white indicator line to the roller axle so rotation direction is visible in + * the Mechanism2d canvas. + */ public void drawViz() { rollerViz = rollerAxle.append( @@ -73,12 +109,24 @@ public void drawViz() { new Color8Bit(Color.kWhite))); } + /** + * Sets all background radial lines to the same color. + * + * @param color the color to apply to every background line + */ public void setBackgroundColor(Color8Bit color) { for (int i = 0; i < backgroundLines; i++) { circleBackground[i].setColor(color); } } + /** + * Alternates two colors across the background lines, giving the circle a two-tone appearance + * (useful for indicating reverse spin direction). + * + * @param color8Bit color applied to even-indexed background lines + * @param color8Bit2 color applied to odd-indexed background lines + */ public void setHalfBackground(Color8Bit color8Bit, Color8Bit color8Bit2) { for (int i = 0; i < backgroundLines; i++) { if (i % 2 == 0) { diff --git a/src/main/java/frc/spectrumLib/sim/LinearConfig.java b/src/main/java/frc/spectrumLib/sim/LinearConfig.java index e1018b08..9db74af2 100644 --- a/src/main/java/frc/spectrumLib/sim/LinearConfig.java +++ b/src/main/java/frc/spectrumLib/sim/LinearConfig.java @@ -6,32 +6,69 @@ import lombok.Getter; import lombok.Setter; +/** + * Configuration data for a linear (elevator-style) mechanism simulation. Stores physical + * properties, Mechanism2d display settings, and optional mount attachment used by {@link + * LinearSim}. + */ public class LinearConfig { + /** Number of Kraken X60 motors driving the linear stage. */ @Getter private int numMotors = 1; + /** Gear ratio between the motor and the elevator drum. */ @Getter private double elevatorGearing = 5; + /** Mass of the moving carriage in kilograms, used by the physics simulation. */ @Getter private double carriageMassKg = 1; + /** Radius of the elevator drum in metres, used to convert rotations to linear position. */ @Getter private double drumRadius = Units.inchesToMeters(0.955 / 2); + /** Minimum travel height of the mechanism in metres. */ @Getter private double minHeight = 0; - + /** Maximum travel height of the mechanism in metres. */ @Getter private double maxHeight = 10000; // Units.inchesToMeters(Robot.config.elevator.maxHeight); // Display Config + /** + * Angle of the linear stage in the Mechanism2d canvas (degrees; 0 = horizontal, 90 = vertical, + * CCW positive). + */ @Getter private double angle = 90; // O is horizontal, 90 is vertical, CCW is positive + /** Color of the moving stage ligament in the Mechanism2d canvas. */ @Getter private Color8Bit color = new Color8Bit(Color.kPurple); + /** Stroke width of the stage ligaments in the Mechanism2d canvas. */ @Getter private double lineWidth = 10; + /** Initial X position of the static root in the Mechanism2d canvas (metres). */ @Getter private double initialX = 0.5; + /** Initial Y position of the static root in the Mechanism2d canvas (metres). */ @Getter private double initialY = 0; + /** Current X position of the static root, updated when mounted (metres). */ @Getter @Setter private double staticRootX = 0.5; + /** Current Y position of the static root, updated when mounted (metres). */ @Getter @Setter private double staticRootY = 0; + /** + * Visual length of the static (non-moving) stage ligament in the Mechanism2d canvas (metres). + */ @Getter private double staticLength = 20; + /** Visual length of the moving stage ligament in the Mechanism2d canvas (metres). */ @Getter private double movingLength = 20; + /** Whether this linear stage is attached to a parent {@link Mount}. */ @Getter private boolean mounted = false; + /** The parent mount this linear stage is attached to, or {@code null} if not mounted. */ @Getter private Mount mount; + /** X position of the mount at simulation start (metres). */ @Getter private double initMountX; + /** Y position of the mount at simulation start (metres). */ @Getter private double initMountY; + /** Angle of the mount at simulation start (radians). */ @Getter private double initMountAngle; + /** + * Creates a LinearConfig with the minimum required positioning and mechanical parameters. + * + * @param x initial X position of the stage root in the Mechanism2d canvas (metres) + * @param y initial Y position of the stage root in the Mechanism2d canvas (metres) + * @param gearing gear ratio between the motor and the elevator drum + * @param drumRadius radius of the elevator drum in metres + */ public LinearConfig(double x, double y, double gearing, double drumRadius) { this.initialX = x; this.initialY = y; @@ -41,47 +78,102 @@ public LinearConfig(double x, double y, double gearing, double drumRadius) { this.drumRadius = drumRadius; } + /** + * Sets the number of motors driving this linear stage. + * + * @param numMotors number of Kraken X60 motors + * @return this config for chaining + */ public LinearConfig setNumMotors(int numMotors) { this.numMotors = numMotors; return this; } + /** + * Sets the carriage mass used by the physics simulation. + * + * @param carriageMassKg mass of the moving carriage in kilograms + * @return this config for chaining + */ public LinearConfig setCarriageMass(double carriageMassKg) { this.carriageMassKg = carriageMassKg; return this; } + /** + * Sets the orientation angle of the linear stage in the Mechanism2d canvas. + * + * @param angle angle in degrees (0 = horizontal, 90 = vertical, CCW positive) + * @return this config for chaining + */ public LinearConfig setAngle(double angle) { this.angle = angle; return this; } + /** + * Sets the color of the moving stage ligament in the Mechanism2d canvas. + * + * @param color the display color + * @return this config for chaining + */ public LinearConfig setColor(Color8Bit color) { this.color = color; return this; } + /** + * Sets the stroke width of the stage ligaments in the Mechanism2d canvas. + * + * @param lineWidth stroke width in pixels + * @return this config for chaining + */ public LinearConfig setLineWidth(double lineWidth) { this.lineWidth = lineWidth; return this; } + /** + * Sets the visual length of the static (non-moving) stage ligament. + * + * @param lengthInches length in inches; stored internally as metres + * @return this config for chaining + */ public LinearConfig setStaticLength(double lengthInches) { this.staticLength = Units.inchesToMeters(lengthInches); ; return this; } + /** + * Sets the visual length of the moving stage ligament. + * + * @param lengthInches length in inches; stored internally as metres + * @return this config for chaining + */ public LinearConfig setMovingLength(double lengthInches) { this.movingLength = Units.inchesToMeters(lengthInches); return this; } + /** + * Sets the maximum travel height of the mechanism. + * + * @param lengthInches maximum height in inches; stored internally as metres + * @return this config for chaining + */ public LinearConfig setMaxHeight(double lengthInches) { this.maxHeight = Units.inchesToMeters(lengthInches); return this; } + /** + * Attaches this linear stage to a parent {@link LinearSim} mount so its root tracks the parent + * stage's position. + * + * @param sim the parent linear stage to mount onto, or {@code null} to leave unmounted + * @return this config for chaining + */ public LinearConfig setMount(LinearSim sim) { if (sim != null) { mounted = true; @@ -94,6 +186,13 @@ public LinearConfig setMount(LinearSim sim) { return this; } + /** + * Attaches this linear stage to a parent {@link ArmSim} mount so its root tracks the arm tip's + * position. + * + * @param sim the parent arm to mount onto, or {@code null} to leave unmounted + * @return this config for chaining + */ public LinearConfig setMount(ArmSim sim) { if (sim != null) { mounted = true; diff --git a/src/main/java/frc/spectrumLib/sim/LinearSim.java b/src/main/java/frc/spectrumLib/sim/LinearSim.java index 3b3e7401..24d4176b 100644 --- a/src/main/java/frc/spectrumLib/sim/LinearSim.java +++ b/src/main/java/frc/spectrumLib/sim/LinearSim.java @@ -11,6 +11,13 @@ import edu.wpi.first.wpilibj.util.Color8Bit; import lombok.Getter; +/** + * WPILib-backed simulation of a linear (elevator-style) mechanism driven by one or more Kraken X60 + * motors. Updates the TalonFX sim state each robot period and animates both a static backing + * ligament and a moving stage ligament in a {@link Mechanism2d} canvas. Implements {@link Mount} so + * other mechanisms can be attached to the moving stage, and implements {@link Mountable} so this + * stage can itself be attached to a parent mount. + */ public class LinearSim implements Mount, Mountable { private ElevatorSim elevatorSim; @@ -18,11 +25,22 @@ public class LinearSim implements Mount, Mountable { private final MechanismRoot2d root; private final MechanismLigament2d staticMech2d; private final MechanismLigament2d m_elevatorMech2d; + /** Configuration containing physical properties and display settings for this linear stage. */ @Getter private LinearConfig config; + private TalonFXSimState linearMotorSim; + /** Always {@link MountType#LINEAR}; used by child mechanisms to determine positioning logic. */ @Getter private final MountType mountType = MountType.LINEAR; + /** + * Creates and registers a linear mechanism simulation. + * + * @param config physical and display configuration for the linear stage + * @param mech the Mechanism2d canvas to draw the stage on + * @param linearMotorSim the TalonFX sim state of the motor driving the stage + * @param name unique name prefix used for Mechanism2d element labels + */ public LinearSim( LinearConfig config, Mechanism2d mech, TalonFXSimState linearMotorSim, String name) { this.config = config; @@ -61,6 +79,12 @@ public LinearSim( new Color8Bit(Color.kBlack))); } + /** + * Returns the moving stage {@link MechanismLigament2d}, useful for attaching additional visual + * elements. + * + * @return the elevator moving-stage ligament + */ public MechanismLigament2d getElevatorMech2d() { return m_elevatorMech2d; } @@ -75,6 +99,10 @@ private double getRotations() { * config.getElevatorGearing(); } + /** + * Advances the elevator physics simulation by one robot period, updates the TalonFX rotor + * position and velocity, and refreshes both the static and moving Mechanism2d ligaments. + */ public void simulationPeriodic() { elevatorSim.setInput(linearMotorSim.getMotorVoltage()); elevatorSim.update(TimedRobot.kDefaultPeriod); @@ -118,6 +146,12 @@ public void simulationPeriodic() { } } + /** + * Returns the horizontal component of the stage's current displacement from its initial + * position, accounting for the parent mount's angle when mounted. + * + * @return horizontal displacement in metres + */ public double getDisplacementX() { double angle; @@ -138,6 +172,12 @@ public double getDisplacementX() { + (config.getStaticRootX() - config.getInitialX()); } + /** + * Returns the vertical component of the stage's current displacement from its initial position, + * accounting for the parent mount's angle when mounted. + * + * @return vertical displacement in metres + */ public double getDisplacementY() { double angle; @@ -158,6 +198,12 @@ public double getDisplacementY() { + (config.getStaticRootY() - config.getInitialY()); } + /** + * Returns the effective absolute angle of the linear stage in radians, adding the parent + * mount's angle when mounted. + * + * @return effective stage angle in radians + */ public double getAngle() { if (config.isMounted()) { return config.getMount().getAngle() + Math.toRadians(config.getAngle()); @@ -166,10 +212,22 @@ public double getAngle() { } } + /** + * Returns the X coordinate of the static root of this stage, used by child mechanisms as their + * mount point. + * + * @return static root X position in metres + */ public double getMountX() { return config.getStaticRootX(); } + /** + * Returns the Y coordinate of the static root of this stage, used by child mechanisms as their + * mount point. + * + * @return static root Y position in metres + */ public double getMountY() { return config.getStaticRootY(); } diff --git a/src/main/java/frc/spectrumLib/sim/Mount.java b/src/main/java/frc/spectrumLib/sim/Mount.java index f3583838..813f9901 100644 --- a/src/main/java/frc/spectrumLib/sim/Mount.java +++ b/src/main/java/frc/spectrumLib/sim/Mount.java @@ -1,21 +1,62 @@ package frc.spectrumLib.sim; +/** + * Represents a simulation component that other mechanisms can be attached to. Implementations + * expose their current position and angle so child mechanisms can update their own position each + * simulation period. + */ public interface Mount { + /** + * Discriminates between the two supported mount types so child mechanisms can apply the correct + * positioning logic. + */ public enum MountType { + /** A linear (elevator-style) stage mount. */ LINEAR, + /** A single-jointed arm mount. */ ARM, } + /** + * Returns the type of this mount, used by child mechanisms to select positioning logic. + * + * @return this mount's {@link MountType} + */ MountType getMountType(); + /** + * Returns the horizontal displacement of this mount from its initial position (metres). + * + * @return horizontal displacement in metres + */ double getDisplacementX(); + /** + * Returns the vertical displacement of this mount from its initial position (metres). + * + * @return vertical displacement in metres + */ double getDisplacementY(); + /** + * Returns the current absolute angle of this mount in radians. + * + * @return angle in radians + */ double getAngle(); + /** + * Returns the X coordinate that child mechanisms should use as their attachment point (metres). + * + * @return mount X position in metres + */ double getMountX(); + /** + * Returns the Y coordinate that child mechanisms should use as their attachment point (metres). + * + * @return mount Y position in metres + */ double getMountY(); } diff --git a/src/main/java/frc/spectrumLib/sim/Mountable.java b/src/main/java/frc/spectrumLib/sim/Mountable.java index 9ca5fabd..ed0e050e 100644 --- a/src/main/java/frc/spectrumLib/sim/Mountable.java +++ b/src/main/java/frc/spectrumLib/sim/Mountable.java @@ -2,8 +2,31 @@ import frc.spectrumLib.sim.Mount.MountType; +/** + * Mixin interface for simulation components that can be attached to a {@link Mount}. Provides + * default geometry helpers that compute the component's updated canvas position each simulation + * period, taking the parent mount's type, current position, and current angle into account. + */ public interface Mountable { + /** + * Computes the updated X position of a mounted component given full explicit geometry + * parameters. + * + * @param mountType type of the parent mount (ARM or LINEAR) + * @param initialX component's initial X position on the canvas (metres) + * @param initialY component's initial Y position on the canvas (metres) + * @param initMountX mount's X position at simulation start (metres) + * @param initMountY mount's Y position at simulation start (metres) + * @param initMountAngle mount's angle at simulation start (radians) + * @param mountX mount's current X position (metres) + * @param mountY mount's current Y position (metres) + * @param displacementX mount's current horizontal displacement from its initial position + * (metres) + * @param displacementY mount's current vertical displacement from its initial position (metres) + * @param mountAngle mount's current angle (radians) + * @return updated X position of the component on the canvas (metres) + */ default double getUpdatedX( MountType mountType, double initialX, @@ -36,6 +59,24 @@ default double getUpdatedX( } } + /** + * Computes the updated Y position of a mounted component given full explicit geometry + * parameters. + * + * @param mountType type of the parent mount (ARM or LINEAR) + * @param initialX component's initial X position on the canvas (metres) + * @param initialY component's initial Y position on the canvas (metres) + * @param initMountX mount's X position at simulation start (metres) + * @param initMountY mount's Y position at simulation start (metres) + * @param initMountAngle mount's angle at simulation start (radians) + * @param mountX mount's current X position (metres) + * @param mountY mount's current Y position (metres) + * @param displacementX mount's current horizontal displacement from its initial position + * (metres) + * @param displacementY mount's current vertical displacement from its initial position (metres) + * @param mountAngle mount's current angle (radians) + * @return updated Y position of the component on the canvas (metres) + */ default double getUpdatedY( MountType mountType, double initialX, @@ -68,6 +109,12 @@ default double getUpdatedY( } } + /** + * Convenience overload that derives all geometry parameters from a {@link RollerConfig}. + * + * @param config the roller configuration carrying mount and initial-position data + * @return updated X position on the canvas (metres) + */ default double getUpdatedX(RollerConfig config) { Mount mount = config.getMount(); return getUpdatedX( @@ -84,6 +131,12 @@ default double getUpdatedX(RollerConfig config) { mount.getAngle()); } + /** + * Convenience overload that derives all geometry parameters from an {@link ArmConfig}. + * + * @param config the arm configuration carrying mount and initial-position data + * @return updated X position on the canvas (metres) + */ default double getUpdatedX(ArmConfig config) { Mount mount = config.getMount(); return getUpdatedX( @@ -100,6 +153,12 @@ default double getUpdatedX(ArmConfig config) { mount.getAngle()); } + /** + * Convenience overload that derives all geometry parameters from a {@link LinearConfig}. + * + * @param config the linear stage configuration carrying mount and initial-position data + * @return updated X position on the canvas (metres) + */ default double getUpdatedX(LinearConfig config) { Mount mount = config.getMount(); return getUpdatedX( @@ -116,6 +175,12 @@ default double getUpdatedX(LinearConfig config) { mount.getAngle()); } + /** + * Convenience overload that derives all geometry parameters from a {@link RollerConfig}. + * + * @param config the roller configuration carrying mount and initial-position data + * @return updated Y position on the canvas (metres) + */ default double getUpdatedY(RollerConfig config) { Mount mount = config.getMount(); return getUpdatedY( @@ -132,6 +197,12 @@ default double getUpdatedY(RollerConfig config) { mount.getAngle()); } + /** + * Convenience overload that derives all geometry parameters from an {@link ArmConfig}. + * + * @param config the arm configuration carrying mount and initial-position data + * @return updated Y position on the canvas (metres) + */ default double getUpdatedY(ArmConfig config) { Mount mount = config.getMount(); return getUpdatedY( @@ -148,6 +219,12 @@ default double getUpdatedY(ArmConfig config) { mount.getAngle()); } + /** + * Convenience overload that derives all geometry parameters from a {@link LinearConfig}. + * + * @param config the linear stage configuration carrying mount and initial-position data + * @return updated Y position on the canvas (metres) + */ default double getUpdatedY(LinearConfig config) { Mount mount = config.getMount(); return getUpdatedY( @@ -180,14 +257,41 @@ static double getAngleOffset( } } + /** + * Computes the straight-line distance between two points on the canvas. + * + * @param x1 X coordinate of the first point (metres) + * @param y1 Y coordinate of the first point (metres) + * @param x2 X coordinate of the second point (metres) + * @param y2 Y coordinate of the second point (metres) + * @return Euclidean distance in metres + */ static double getDistance(double x1, double y1, double x2, double y2) { return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)); } + /** + * Computes the X coordinate of a point at {@code radius} distance from {@code displacementX} in + * the direction of {@code angle}. + * + * @param radius distance from the reference point (metres) + * @param angle direction angle in radians + * @param displacementX reference X coordinate (metres) + * @return resulting X coordinate (metres) + */ static double getXWithAngle(double radius, double angle, double displacementX) { return radius * Math.cos(angle) + displacementX; } + /** + * Computes the Y coordinate of a point at {@code radius} distance from {@code displacementY} in + * the direction of {@code angle}. + * + * @param radius distance from the reference point (metres) + * @param angle direction angle in radians + * @param displacementY reference Y coordinate (metres) + * @return resulting Y coordinate (metres) + */ static double getYWithAngle(double radius, double angle, double displacementY) { return radius * Math.sin(angle) + displacementY; } diff --git a/src/main/java/frc/spectrumLib/sim/RollerConfig.java b/src/main/java/frc/spectrumLib/sim/RollerConfig.java index 170fe895..2247069e 100644 --- a/src/main/java/frc/spectrumLib/sim/RollerConfig.java +++ b/src/main/java/frc/spectrumLib/sim/RollerConfig.java @@ -4,42 +4,91 @@ import edu.wpi.first.wpilibj.util.Color8Bit; import lombok.Getter; +/** + * Configuration data for a roller mechanism simulation. Stores physical properties, display colors, + * canvas position, and optional mount attachment used by {@link RollerSim}. + */ public class RollerConfig { + /** Outer diameter of the roller in inches, used for physics and visual scaling. */ @Getter private double rollerDiameterInches = 2; + /** Number of radial lines used to draw the roller circle in the Mechanism2d canvas. */ @Getter private int backgroundLines = 36; + /** Gear ratio between the motor and the roller output shaft. */ @Getter private double gearRatio = 5; + /** Moment of inertia of the roller used by the flywheel physics simulation (kg·m²). */ @Getter private double simMOI = 0.01; + /** Color displayed when the roller is stationary or below the velocity threshold. */ @Getter private Color8Bit offColor = new Color8Bit(Color.kBlack); + /** Color displayed when the roller is spinning in the forward direction. */ @Getter private Color8Bit fwdColor = new Color8Bit(Color.kGreen); + /** Color displayed when the roller is spinning in the reverse direction. */ @Getter private Color8Bit revColor = new Color8Bit(Color.kRed); + /** Initial X position of the roller axle in the Mechanism2d canvas (metres). */ @Getter private double initialX = 0; + /** Initial Y position of the roller axle in the Mechanism2d canvas (metres). */ @Getter private double initialY = 0; + /** Whether this roller is attached to a parent {@link Mount}. */ @Getter private boolean mounted = false; + /** The parent mount this roller is attached to, or {@code null} if not mounted. */ @Getter private Mount mount; + /** X position of the mount at simulation start (metres). */ @Getter private double initMountX; + /** Y position of the mount at simulation start (metres). */ @Getter private double initMountY; + /** Angle of the mount at simulation start (radians). */ @Getter private double initMountAngle; + /** + * Creates a RollerConfig for a roller with the given diameter. + * + * @param diameterInches outer diameter of the roller in inches + */ public RollerConfig(double diameterInches) { rollerDiameterInches = diameterInches; } + /** + * Sets the gear ratio between the motor and the roller output shaft. + * + * @param ratio gear ratio (motor rotations per roller rotation) + * @return this config for chaining + */ public RollerConfig setGearRatio(double ratio) { gearRatio = ratio; return this; } + /** + * Sets the moment of inertia used by the flywheel physics simulation. + * + * @param moi moment of inertia in kg·m² + * @return this config for chaining + */ public RollerConfig setSimMOI(double moi) { simMOI = moi; return this; } + /** + * Sets the initial position of the roller axle in the Mechanism2d canvas. + * + * @param x initial X position in metres + * @param y initial Y position in metres + * @return this config for chaining + */ public RollerConfig setPosition(double x, double y) { initialX = x; initialY = y; return this; } + /** + * Attaches this roller to a {@link LinearSim} mount so its axle tracks the linear stage's + * position. + * + * @param sim the linear stage to mount onto, or {@code null} to leave unmounted + * @return this config for chaining + */ public RollerConfig setMount(LinearSim sim) { if (sim != null) { mounted = true; @@ -52,6 +101,12 @@ public RollerConfig setMount(LinearSim sim) { return this; } + /** + * Attaches this roller to an {@link ArmSim} mount so its axle tracks the arm tip's position. + * + * @param sim the parent arm to mount onto, or {@code null} to leave unmounted + * @return this config for chaining + */ public RollerConfig setMount(ArmSim sim) { if (sim != null) { mounted = true; diff --git a/src/main/java/frc/spectrumLib/sim/RollerSim.java b/src/main/java/frc/spectrumLib/sim/RollerSim.java index 50808d06..96ef9757 100644 --- a/src/main/java/frc/spectrumLib/sim/RollerSim.java +++ b/src/main/java/frc/spectrumLib/sim/RollerSim.java @@ -14,6 +14,12 @@ import edu.wpi.first.wpilibj.util.Color; import edu.wpi.first.wpilibj.util.Color8Bit; +/** + * WPILib-backed simulation of a roller (flywheel) mechanism driven by a single Kraken X60 motor. + * Updates the TalonFX sim state each robot period and animates the roller — including spin-color + * feedback — in a {@link Mechanism2d} canvas. Implements {@link Mountable} so the roller axle can + * follow a parent {@link Mount}. + */ public class RollerSim implements Mountable { private MechanismRoot2d rollerAxle; @@ -24,6 +30,14 @@ public class RollerSim implements Mountable { private RollerConfig config; private Circle roller; + /** + * Creates and registers a roller simulation. + * + * @param config physical and display configuration for the roller + * @param mech the Mechanism2d canvas to draw the roller on + * @param rollerMotorSim the TalonFX sim state of the motor driving the roller + * @param name unique name prefix used for Mechanism2d element labels + */ public RollerSim( RollerConfig config, Mechanism2d mech, TalonFXSimState rollerMotorSim, String name) { this.config = config; @@ -54,6 +68,11 @@ public RollerSim( mech); } + /** + * Advances the flywheel physics simulation by one robot period, updates the TalonFX rotor + * velocity and position, moves the axle to its current mount position, and updates the + * Mechanism2d color to reflect the roller's spin direction. + */ public void simulationPeriodic() { // double x, double y) { // ------ Update sim based on motor output rollerSim.setInput(rollerMotorSim.getMotorVoltage()); @@ -64,9 +83,11 @@ public void simulationPeriodic() { // double x, double y) { // Subtracting out the starting angle is necessary so the simulation can't "cheat" and use // the // sim as an absolute encoder. - double rotationsPerSecond = rollerSim.getAngularVelocityRadPerSec() / (2.0 * Math.PI); - rollerMotorSim.setRotorVelocity(rotationsPerSecond); - rollerMotorSim.addRotorPosition(rotationsPerSecond * TimedRobot.kDefaultPeriod); + // FlywheelSim reports mechanism-side velocity; the rotor spins gearRatio times faster. + double rotorRotationsPerSecond = + rollerSim.getAngularVelocityRadPerSec() / (2.0 * Math.PI) * config.getGearRatio(); + rollerMotorSim.setRotorVelocity(rotorRotationsPerSecond); + rollerMotorSim.addRotorPosition(rotorRotationsPerSecond * TimedRobot.kDefaultPeriod); // Update the axle as the robot moves if (config.isMounted()) { diff --git a/src/main/java/frc/spectrumLib/swerve/MapleSimSwerveDrivetrain.java b/src/main/java/frc/spectrumLib/swerve/MapleSimSwerveDrivetrain.java index f82df409..ade436b6 100644 --- a/src/main/java/frc/spectrumLib/swerve/MapleSimSwerveDrivetrain.java +++ b/src/main/java/frc/spectrumLib/swerve/MapleSimSwerveDrivetrain.java @@ -47,8 +47,13 @@ *

It replaces the {@link com.ctre.phoenix6.swerve.SimSwerveDrivetrain} class. */ public class MapleSimSwerveDrivetrain { + /** Simulation state of the Pigeon2 gyro, used to inject computed yaw and angular velocity. */ private final Pigeon2SimState pigeonSim; + + /** Simulated representations of each swerve module, indexed FL/FR/BL/BR. */ private final SimSwerveModule[] simModules; + + /** The underlying Maple-Sim drive simulation providing physics and odometry. */ public final SwerveDriveSimulation mapleSimDrive; /** @@ -148,11 +153,22 @@ public void update() { *

Represents the simulation of a single {@link SwerveModule}.

*/ protected static class SimSwerveModule { + /** Constants (gear ratios, friction voltages, wheel radius, etc.) for this module. */ public final SwerveModuleConstants< TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> moduleConstant; + + /** Maple-Sim physics simulation instance for this module. */ public final SwerveModuleSimulation moduleSimulation; + /** + * Constructs a simulated swerve module by wiring the Maple-Sim physics model to the CTRE + * motor controllers. + * + * @param moduleConstant constants for this swerve module + * @param moduleSimulation Maple-Sim simulation instance for this module + * @param module the real CTRE {@link SwerveModule} whose sim states will be driven + */ public SimSwerveModule( SwerveModuleConstants< TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> @@ -170,16 +186,40 @@ public SimSwerveModule( } // Static utils classes + + /** + * Adapts a {@link TalonFX} motor controller for use as a {@link SimulatedMotorController} in + * Maple-Sim by forwarding encoder state from the simulation into the CTRE sim state. + */ public static class TalonFXMotorControllerSim implements SimulatedMotorController { + /** CAN device ID of the underlying TalonFX. */ public final int id; + /** CTRE simulation state object used to inject position, velocity, and voltage. */ private final TalonFXSimState talonFXSimState; + /** + * Constructs the adapter for the given TalonFX. + * + * @param talonFX the TalonFX motor controller to wrap + */ public TalonFXMotorControllerSim(TalonFX talonFX) { this.id = talonFX.getDeviceID(); this.talonFXSimState = talonFX.getSimState(); } + /** + * Injects simulated rotor position, velocity, and supply voltage into the TalonFX sim + * state, then returns the motor output voltage requested by the controller's closed-loop + * algorithm. + * + * @param mechanismAngle current mechanism-side angle from the physics model + * @param mechanismVelocity current mechanism-side angular velocity from the physics model + * @param encoderAngle current encoder angle (rotor-side) from the physics model + * @param encoderVelocity current encoder angular velocity (rotor-side) from the physics + * model + * @return the voltage the controller is requesting from the simulated battery + */ @Override public Voltage updateControlSignal( Angle mechanismAngle, @@ -194,12 +234,25 @@ public Voltage updateControlSignal( } } + /** + * Extends {@link TalonFXMotorControllerSim} to also drive a remote CANcoder simulation state. + * Used for steer motors whose feedback device is a remote CANcoder. + */ @SuppressWarnings("all") public static class TalonFXMotorControllerWithRemoteCanCoderSim extends TalonFXMotorControllerSim { + /** CAN device ID of the remote CANcoder. */ private final int encoderId; + + /** CTRE simulation state of the remote CANcoder. */ private final CANcoderSimState remoteCancoderSimState; + /** + * Constructs the adapter for a steer motor paired with a remote CANcoder. + * + * @param talonFX the steer TalonFX motor controller + * @param cancoder the remote CANcoder used as the steer feedback sensor + */ public TalonFXMotorControllerWithRemoteCanCoderSim(TalonFX talonFX, CANcoder cancoder) { super(talonFX); this.remoteCancoderSimState = cancoder.getSimState(); @@ -207,6 +260,18 @@ public TalonFXMotorControllerWithRemoteCanCoderSim(TalonFX talonFX, CANcoder can this.encoderId = cancoder.getDeviceID(); } + /** + * Injects simulated supply voltage, position, and velocity into the remote CANcoder sim + * state, then delegates to the parent implementation to update the TalonFX and return the + * requested motor voltage. + * + * @param mechanismAngle current mechanism-side angle (written to the CANcoder) + * @param mechanismVelocity current mechanism-side angular velocity (written to the + * CANcoder) + * @param encoderAngle current encoder (rotor-side) angle (forwarded to TalonFX) + * @param encoderVelocity current encoder angular velocity (forwarded to TalonFX) + * @return the voltage the controller is requesting from the simulated battery + */ @Override public Voltage updateControlSignal( Angle mechanismAngle, diff --git a/src/main/java/frc/spectrumLib/swerve/SysID.java b/src/main/java/frc/spectrumLib/swerve/SysID.java index 0deeed69..e45396d4 100644 --- a/src/main/java/frc/spectrumLib/swerve/SysID.java +++ b/src/main/java/frc/spectrumLib/swerve/SysID.java @@ -6,23 +6,51 @@ import com.ctre.phoenix6.swerve.SwerveRequest; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine; -import frc.robot.swerve.Swerve; +import frc.robot.subsystems.swerve.Swerve; import lombok.Getter; +/** + * Encapsulates the three SysId characterization routines for a CTRE swerve drivetrain: translation, + * rotation, and steer-gains. + * + *

Construct one instance per robot, passing the {@link Swerve} subsystem. Then bind {@link + * #sysIdQuasistatic} and {@link #sysIdDynamic} to test-mode triggers. Change {@link + * #RoutineToApply} (by editing the source) to select which routine runs. + */ public class SysID { // private Swerve swerve; + + /** SysId routine that characterizes linear translation drive gains. */ @Getter private final SysIdRoutine SysIdRoutineTranslation; + + /** SysId routine that characterizes rotational drive gains. */ @Getter private final SysIdRoutine SysIdRoutineRotation; + + /** SysId routine that characterizes steer-module gains. */ @Getter private final SysIdRoutine SysIdRoutineSteer; + + /** The routine currently selected for quasistatic and dynamic test commands. */ private final SysIdRoutine RoutineToApply; + /** CTRE swerve request used during the translation characterization routine. */ private final SwerveRequest.SysIdSwerveTranslation TranslationCharacterization = new SwerveRequest.SysIdSwerveTranslation(); + + /** CTRE swerve request used during the rotation characterization routine. */ private final SwerveRequest.SysIdSwerveRotation RotationCharacterization = new SwerveRequest.SysIdSwerveRotation(); + + /** CTRE swerve request used during the steer-gains characterization routine. */ private final SwerveRequest.SysIdSwerveSteerGains SteerCharacterization = new SwerveRequest.SysIdSwerveSteerGains(); + /** + * Constructs all three SysId routines and selects {@link #SysIdRoutineTranslation} as the + * active routine. Change {@link #RoutineToApply} at the bottom of this constructor to switch + * which routine is exercised by {@link #sysIdQuasistatic} and {@link #sysIdDynamic}. + * + * @param swerve the {@link Swerve} subsystem that will be commanded during characterization + */ public SysID(Swerve swerve) { // this.swerve = swerve; @@ -79,10 +107,24 @@ public SysID(Swerve swerve) { * Both the sysid commands are specific to one particular sysid routine, change * which one you're trying to characterize */ + + /** + * Returns a quasistatic (slow ramp) characterization command for the active routine. + * + * @param direction the direction ({@link SysIdRoutine.Direction#kForward} or {@link + * SysIdRoutine.Direction#kReverse}) in which to ramp the output + * @return a command that slowly ramps the output and logs data for system identification + */ public Command sysIdQuasistatic(SysIdRoutine.Direction direction) { return RoutineToApply.quasistatic(direction); } + /** + * Returns a dynamic (step) characterization command for the active routine. + * + * @param direction the direction in which to apply the voltage step + * @return a command that applies a step voltage and logs data for system identification + */ public Command sysIdDynamic(SysIdRoutine.Direction direction) { return RoutineToApply.dynamic(direction); } diff --git a/src/main/java/frc/spectrumLib/BatteryLogger.java b/src/main/java/frc/spectrumLib/telemetry/BatteryLogger.java similarity index 66% rename from src/main/java/frc/spectrumLib/BatteryLogger.java rename to src/main/java/frc/spectrumLib/telemetry/BatteryLogger.java index ccfb7576..ce3f52f7 100644 --- a/src/main/java/frc/spectrumLib/BatteryLogger.java +++ b/src/main/java/frc/spectrumLib/telemetry/BatteryLogger.java @@ -5,28 +5,48 @@ // license that can be found in the LICENSE file at // the root directory of this project. -package frc.spectrumLib; +package frc.spectrumLib.telemetry; import java.util.HashMap; import java.util.Map; import lombok.Getter; import lombok.Setter; -/** Class for logging current, power, and energy usage. */ +/** + * Tracks and logs current draw, power, and cumulative energy consumption across subsystems each + * robot loop cycle. + */ public class BatteryLogger { + /** Duration of one robot loop in seconds, used to convert power (W) to energy (J). */ private final double loopPeriodSecs = 0.02; + /** When {@code false} all methods are no-ops, allowing the logger to be disabled at runtime. */ @Setter private boolean enabled = false; + /** + * Running total of current draw accumulated since the last {@link #logPower()} call, in amps. + */ @Getter private double totalCurrent = 0.0; + /** Running total of power accumulated since the last {@link #logPower()} call, in watts. */ @Getter private double totalPower = 0.0; + /** Cumulative energy consumed over the entire enabled session, in joules. */ @Getter private double totalEnergy = 0.0; + /** Battery terminal voltage used to convert current to power, in volts. */ @Setter private double batteryVoltage = 12.6; + /** Estimated current drawn by the RoboRIO itself, in amps. */ @Setter private double rioCurrent = 0.0; private Map subsystemCurrents = new HashMap<>(); private Map subsystemPowers = new HashMap<>(); private Map subsystemEnergies = new HashMap<>(); + /** + * Records the current draw for a named subsystem channel and accumulates it into the running + * totals. The {@code key} may use "/" or "-" as separators; parent keys are automatically + * aggregated. + * + * @param key Hierarchical name for the current consumer (e.g. {@code "Drive/FrontLeft"}) + * @param amps One or more current readings in amps; absolute values are summed + */ public void reportCurrentUsage(String key, double... amps) { if (enabled) { double totalAmps = 0.0; @@ -61,15 +81,23 @@ public void reportCurrentUsage(String key, double... amps) { } } + /** + * Appends control-overhead current consumers (roboRIO, CANcoders, Pigeon, CANivore, radio), + * then logs total and per-subsystem current, power, and energy to DogLog under the {@code + * BatteryLogger/} key hierarchy. Resets per-loop current and power accumulators afterward; + * cumulative energy is preserved across calls. + */ public void logPower() { if (enabled) { + // Controls overhead is added here so it is included in the totalCurrent log below. + // Subsystem currents have already been accumulated via logBatteryUsage() in periodic(). reportCurrentUsage("Controls/roboRIO", rioCurrent); reportCurrentUsage("Controls/CANcoders", 0.05 * 4); reportCurrentUsage("Controls/Pigeon", 0.04); reportCurrentUsage("Controls/CANivore", 0.03); reportCurrentUsage("Controls/Radio", 0.5); - // Log total and subsystem energy usage + // Log total (subsystems + controls overhead) and per-subsystem energy usage Telemetry.log("BatteryLogger/Current", totalCurrent, "amps"); Telemetry.log("BatteryLogger/Power", totalPower, "watts"); Telemetry.log("BatteryLogger/Energy", joulesToWattHours(totalEnergy), "wh"); diff --git a/src/main/java/frc/spectrumLib/Telemetry.java b/src/main/java/frc/spectrumLib/telemetry/Telemetry.java similarity index 75% rename from src/main/java/frc/spectrumLib/Telemetry.java rename to src/main/java/frc/spectrumLib/telemetry/Telemetry.java index 3680aa04..0efab9b2 100644 --- a/src/main/java/frc/spectrumLib/Telemetry.java +++ b/src/main/java/frc/spectrumLib/telemetry/Telemetry.java @@ -1,4 +1,4 @@ -package frc.spectrumLib; +package frc.spectrumLib.telemetry; import dev.doglog.DogLog; import dev.doglog.DogLogOptions; @@ -20,8 +20,13 @@ */ public class Telemetry extends DogLog implements Subsystem { + /** + * Tracks the most recent set of active alerts for each severity key to avoid duplicate log + * entries. + */ private static final Map previousAlerts = new HashMap<>(); + /** Named fault conditions that can be surfaced as structured log entries. */ public enum Fault { CAMERA_OFFLINE, AUTO_SHOT_TIMEOUT_TRIGGERED, @@ -29,21 +34,31 @@ public enum Fault { } /** - * Priority levels for printing to the console NORMAL: Low priority, only print if enabled HIGH: - * High priority, always print + * Priority levels for printing to the console. + * + *

    + *
  • {@link #NORMAL} — only printed when the global priority is also {@code NORMAL}. + *
  • {@link #HIGH} — always printed regardless of the global priority setting. + *
*/ public enum PrintPriority { NORMAL, HIGH } + /** Minimum priority level a message must have to be written to the console. */ private static PrintPriority priority = PrintPriority.HIGH; + /** + * Creates a Telemetry instance and registers it as a WPILib subsystem so its {@link + * #periodic()} method is called every loop cycle. + */ public Telemetry() { super(); register(); } + /** Called every robot loop cycle. Logs any newly active alerts from NetworkTables. */ @Override public void periodic() { logAlerts(); @@ -87,6 +102,12 @@ private static void setPriority(PrintPriority priority) { Telemetry.priority = priority; } + /** + * Wraps a command so that its initialization and end are logged to the "Commands" key. + * + * @param cmd The command to wrap + * @return a decorated command that logs lifecycle events and preserves the original name + */ public static Command log(Command cmd) { return cmd.deadlineFor( Commands.startEnd( @@ -105,11 +126,20 @@ public static void print(String output, PrintPriority priority) { log("Prints", out); } + /** + * Prints a message at {@link PrintPriority#NORMAL} priority. The message is always written to + * the DogLog "Prints" key but only echoed to stdout when the global priority allows it. + * + * @param output The string to print + */ public static void print(String output) { print(output, PrintPriority.NORMAL); } - // New method to log alerts from NetworkTables + /** + * Reads all active alerts from the SmartDashboard NetworkTable and logs any that are new since + * the last call under the "Alerts" DogLog key. + */ public static void logAlerts() { NetworkTableInstance ntInstance = NetworkTableInstance.getDefault(); logAlertType(ntInstance, "errors", "ERROR"); diff --git a/src/main/java/frc/spectrumLib/telemetry/TuneValue.java b/src/main/java/frc/spectrumLib/telemetry/TuneValue.java new file mode 100644 index 00000000..ef45183e --- /dev/null +++ b/src/main/java/frc/spectrumLib/telemetry/TuneValue.java @@ -0,0 +1,50 @@ +package frc.spectrumLib.telemetry; + +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import java.util.function.DoubleSupplier; +import lombok.Getter; + +/** + * A numeric value that is published to SmartDashboard and can be modified at runtime without + * redeploying code. Useful for tuning PID gains, speeds, and other constants during development. + * Place a call to {@link #update()} or pass {@link #getSupplier()} inside a command to read the + * latest value each loop. + */ +public class TuneValue { + /** Current value, refreshed on each call to {@link #update()}. */ + @Getter private double value; + /** SmartDashboard key under which this value is published and read. */ + @Getter private String name; + + /** + * Creates a TuneValue, publishing {@code defaultValue} to SmartDashboard under {@code name}. + * + * @param name SmartDashboard key + * @param defaultValue Initial value written to SmartDashboard + */ + public TuneValue(String name, double defaultValue) { + SmartDashboard.putNumber(name, defaultValue); + value = defaultValue; + this.name = name; + } + + /** + * Reads the current value from SmartDashboard and caches it locally. + * + * @return the latest value from SmartDashboard + */ + public Double update() { + value = SmartDashboard.getNumber(name, value); + return value; + } + + /** + * Returns a {@link DoubleSupplier} that calls {@link #update()} each time it is queried, + * suitable for passing to command factories that accept live-updating suppliers. + * + * @return a supplier backed by this TuneValue + */ + public DoubleSupplier getSupplier() { + return this::update; + } +} diff --git a/src/main/java/frc/spectrumLib/CachedDouble.java b/src/main/java/frc/spectrumLib/util/CachedDouble.java similarity index 58% rename from src/main/java/frc/spectrumLib/CachedDouble.java rename to src/main/java/frc/spectrumLib/util/CachedDouble.java index 1fcf6246..02d1bebb 100644 --- a/src/main/java/frc/spectrumLib/CachedDouble.java +++ b/src/main/java/frc/spectrumLib/util/CachedDouble.java @@ -1,4 +1,4 @@ -package frc.spectrumLib; +package frc.spectrumLib.util; import edu.wpi.first.wpilibj2.command.SubsystemBase; import java.util.function.DoubleSupplier; @@ -12,15 +12,30 @@ public class CachedDouble extends SubsystemBase implements DoubleSupplier { private double value; private final DoubleSupplier source; + /** + * Creates a CachedDouble wrapping the given supplier. + * + * @param source the underlying supplier whose value is cached each scheduler iteration + */ public CachedDouble(DoubleSupplier source) { this.source = source; } + /** + * Called by the scheduler each iteration to invalidate the cached value so the next {@link + * #getAsDouble()} call re-queries the source. + */ @Override public void periodic() { cached = false; } + /** + * Returns the cached value of the source supplier, querying the supplier at most once per + * scheduler iteration. + * + * @return the supplier's value for the current iteration + */ @Override public double getAsDouble() { if (!cached) { diff --git a/src/main/java/frc/spectrumLib/util/CanDeviceId.java b/src/main/java/frc/spectrumLib/util/CanDeviceId.java index eeb855bd..bd334d29 100644 --- a/src/main/java/frc/spectrumLib/util/CanDeviceId.java +++ b/src/main/java/frc/spectrumLib/util/CanDeviceId.java @@ -2,28 +2,60 @@ // Based on 254-2023 Class // https://github.com/Team254/FRC-2023-Public/blob/main/src/main/java/com/team254/lib/drivers/CanDeviceId.java +/** + * Identifies a CAN device by its numeric device ID and the CAN bus name it lives on. Equality and + * hashing consider both fields, so two instances with the same device number on different buses are + * treated as distinct. + */ public class CanDeviceId { private final int mDeviceNumber; private final String mBus; + /** + * Creates a CAN device identifier with an explicit bus name. + * + * @param deviceNumber the numeric CAN ID assigned to the device + * @param bus the name of the CAN bus (e.g. {@code "rio"} or {@code "canivore"}) + */ public CanDeviceId(int deviceNumber, String bus) { mDeviceNumber = deviceNumber; mBus = bus; } // Use the default bus name (empty string). + /** + * Creates a CAN device identifier on the default CAN bus (empty string). + * + * @param deviceNumber the numeric CAN ID assigned to the device + */ public CanDeviceId(int deviceNumber) { this(deviceNumber, ""); } + /** + * Returns the numeric CAN device ID. + * + * @return the device number + */ public int getDeviceNumber() { return mDeviceNumber; } + /** + * Returns the CAN bus name this device is on. + * + * @return the bus name, or an empty string for the default bus + */ public String getBus() { return mBus; } + /** + * Type-safe equality check against another {@link CanDeviceId}. + * + * @param other the other instance to compare + * @return {@code true} if both the device number and bus name match + */ public boolean equals(CanDeviceId other) { return equals((Object) other); } diff --git a/src/main/java/frc/spectrumLib/util/CrashTracker.java b/src/main/java/frc/spectrumLib/util/CrashTracker.java index 904311b9..b9b1999d 100644 --- a/src/main/java/frc/spectrumLib/util/CrashTracker.java +++ b/src/main/java/frc/spectrumLib/util/CrashTracker.java @@ -1,7 +1,7 @@ package frc.spectrumLib.util; import edu.wpi.first.wpilibj.RobotBase; -import frc.spectrumLib.Telemetry; +import frc.spectrumLib.telemetry.Telemetry; import java.io.FileNotFoundException; import java.io.FileWriter; import java.io.IOException; diff --git a/src/main/java/frc/spectrumLib/util/ExpCurve.java b/src/main/java/frc/spectrumLib/util/ExpCurve.java index a77dcaac..cc5313d2 100644 --- a/src/main/java/frc/spectrumLib/util/ExpCurve.java +++ b/src/main/java/frc/spectrumLib/util/ExpCurve.java @@ -44,7 +44,10 @@ public ExpCurve(double expVal, double offset, double scalar, double deadzone) { } /** - * @param input value to be mapped + * Applies the full exponential curve pipeline: deadzone, exponent, scalar, then offset. + * + * @param input the raw input value to be mapped (typically in [-1, 1]) + * @return the mapped output value */ @Override public double calculate(double input) { diff --git a/src/main/java/frc/spectrumLib/util/Network.java b/src/main/java/frc/spectrumLib/util/Network.java index 11e228b7..57462fa9 100644 --- a/src/main/java/frc/spectrumLib/util/Network.java +++ b/src/main/java/frc/spectrumLib/util/Network.java @@ -63,9 +63,11 @@ public static String getIPaddress() { } /** - * Gets the IP Address of the device at the address such as "limelight.local" + * Resolves and returns the IP address of a device identified by its mDNS or hostname address + * (e.g. {@code "limelight.local"}). * - * @return the IP Address of the device + * @param deviceNameAddress the hostname or mDNS name to resolve + * @return the resolved IP address string, or {@code "UNKNOWN"} if resolution fails */ public static String getIPaddress(String deviceNameAddress) { InetAddress localHost; diff --git a/src/main/java/frc/spectrumLib/util/Trio.java b/src/main/java/frc/spectrumLib/util/Trio.java index 5cf77472..df6ff51f 100644 --- a/src/main/java/frc/spectrumLib/util/Trio.java +++ b/src/main/java/frc/spectrumLib/util/Trio.java @@ -47,6 +47,11 @@ public B getSecond() { return m_second; } + /** + * Returns the third object. + * + * @return The third object. + */ public C getThird() { return m_third; } diff --git a/src/main/java/frc/spectrumLib/util/Util.java b/src/main/java/frc/spectrumLib/util/Util.java index f62570ec..809ac07f 100644 --- a/src/main/java/frc/spectrumLib/util/Util.java +++ b/src/main/java/frc/spectrumLib/util/Util.java @@ -9,20 +9,42 @@ /** From 254 lib imported from 1678-2024 Contains basic functions that are used often. */ public class Util { + /** Small value used for floating-point equality comparisons. */ public static final double EPSILON = 1e-12; /** Prevent this class from being instantiated. */ private Util() {} - /** Limits the given input to the given magnitude. */ + /** + * Clamps {@code v} to the range [{@code -maxMagnitude}, {@code maxMagnitude}]. + * + * @param v the value to clamp + * @param maxMagnitude the maximum absolute value allowed + * @return the clamped value + */ public static double limit(double v, double maxMagnitude) { return limit(v, -maxMagnitude, maxMagnitude); } + /** + * Clamps {@code v} to the range [{@code min}, {@code max}]. + * + * @param v the value to clamp + * @param min the lower bound (inclusive) + * @param max the upper bound (inclusive) + * @return the clamped value + */ public static double limit(double v, double min, double max) { return Math.min(max, Math.max(min, v)); } + /** + * Checks whether {@code v} is strictly within [{@code -maxMagnitude}, {@code maxMagnitude}]. + * + * @param v the value to test + * @param maxMagnitude the maximum absolute value (exclusive bound) + * @return {@code true} if {@code |v| < maxMagnitude} + */ public static boolean inRange(double v, double maxMagnitude) { return inRange(v, -maxMagnitude, maxMagnitude); } @@ -32,15 +54,40 @@ public static boolean inRange(double v, double min, double max) { return v > min && v < max; } + /** + * Checks whether the value supplied by {@code v} is strictly between the values supplied by + * {@code min} and {@code max}. + * + * @param v supplier of the value to test + * @param min supplier of the lower bound (exclusive) + * @param max supplier of the upper bound (exclusive) + * @return {@code true} if {@code min.get() < v.get() < max.get()} + */ public static boolean inRange(DoubleSupplier v, DoubleSupplier min, DoubleSupplier max) { return v.getAsDouble() > min.getAsDouble() && v.getAsDouble() < max.getAsDouble(); } + /** + * Linearly interpolates between {@code a} and {@code b} by a factor {@code x}, clamped to [0, + * 1]. + * + * @param a the start value ({@code x = 0}) + * @param b the end value ({@code x = 1}) + * @param x the interpolation factor, clamped to [0, 1] + * @return the interpolated value + */ public static double interpolate(double a, double b, double x) { x = limit(x, 0.0, 1.0); return a + (b - a) * x; } + /** + * Joins a list of objects into a single string with the given delimiter. + * + * @param delim the delimiter placed between consecutive elements + * @param strings the list of objects whose {@code toString()} values are joined + * @return the joined string + */ public static String joinStrings(final String delim, final List strings) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < strings.size(); ++i) { @@ -52,18 +99,49 @@ public static String joinStrings(final String delim, final List strings) { return sb.toString(); } + /** + * Checks whether {@code a} and {@code b} are within {@code epsilon} of each other. + * + * @param a first value + * @param b second value + * @param epsilon the allowed absolute difference + * @return {@code true} if {@code |a - b| <= epsilon} + */ public static boolean epsilonEquals(double a, double b, double epsilon) { return (a - epsilon <= b) && (a + epsilon >= b); } + /** + * Checks whether {@code a} and {@code b} are within {@link #EPSILON} of each other. + * + * @param a first value + * @param b second value + * @return {@code true} if {@code |a - b| <= EPSILON} + */ public static boolean epsilonEquals(double a, double b) { return epsilonEquals(a, b, EPSILON); } + /** + * Checks whether integer {@code a} and {@code b} are within {@code epsilon} of each other. + * + * @param a first integer value + * @param b second integer value + * @param epsilon the allowed absolute difference + * @return {@code true} if {@code |a - b| <= epsilon} + */ public static boolean epsilonEquals(int a, int b, int epsilon) { return (a - epsilon <= b) && (a + epsilon >= b); } + /** + * Checks whether every element in {@code list} is within {@code epsilon} of {@code value}. + * + * @param list the list of doubles to check + * @param value the target value each element is compared against + * @param epsilon the allowed absolute difference for each comparison + * @return {@code true} if all elements are within {@code epsilon} of {@code value} + */ public static boolean allCloseTo(final List list, double value, double epsilon) { boolean result = true; for (Double value_in : list) { diff --git a/src/main/java/frc/spectrumLib/util/exceptions/KillRobotException.java b/src/main/java/frc/spectrumLib/util/exceptions/KillRobotException.java index 3c311a87..0f9805a8 100644 --- a/src/main/java/frc/spectrumLib/util/exceptions/KillRobotException.java +++ b/src/main/java/frc/spectrumLib/util/exceptions/KillRobotException.java @@ -1,10 +1,15 @@ package frc.spectrumLib.util.exceptions; +/** + * Unchecked exception thrown to signal that the robot code should stop executing immediately. + * Intended for unrecoverable error states where continued operation would be unsafe. + */ public class KillRobotException extends RuntimeException { - /* - * Required when we want to add a custom message when throwing the exception - * as throw new CustomUncheckedException(" Custom Unchecked Exception "); + /** + * Creates a KillRobotException with a descriptive message. + * + * @param message explanation of the condition that triggered the kill */ public KillRobotException(String message) { // calling super invokes the constructors of all super classes @@ -12,21 +17,21 @@ public KillRobotException(String message) { super(message); } - /* - * Required when we want to wrap the exception generated inside the catch block and rethrow it - * as catch(ArrayIndexOutOfBoundsException e) { - * throw new CustomUncheckedException(e); - * } + /** + * Creates a KillRobotException wrapping an underlying cause. + * + * @param cause the original exception that led to this kill condition */ public KillRobotException(Throwable cause) { // call appropriate parent constructor super(cause); } - /* - * Required when we want both the above - * as catch(ArrayIndexOutOfBoundsException e) { - * throw new CustomUncheckedException(e, "File not found"); - * } + + /** + * Creates a KillRobotException with both a message and an underlying cause. + * + * @param message explanation of the condition that triggered the kill + * @param throwable the original exception that led to this kill condition */ public KillRobotException(String message, Throwable throwable) { // call appropriate parent constructor diff --git a/src/main/java/frc/spectrumLib/vision/Limelight.java b/src/main/java/frc/spectrumLib/vision/Limelight.java index 49a093e2..020ce17c 100644 --- a/src/main/java/frc/spectrumLib/vision/Limelight.java +++ b/src/main/java/frc/spectrumLib/vision/Limelight.java @@ -4,7 +4,7 @@ import edu.wpi.first.math.geometry.Pose3d; import edu.wpi.first.math.util.Units; import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; -import frc.robot.vision.Vision.VisionConfig; +import frc.robot.subsystems.vision.Vision.VisionConfig; import frc.spectrumLib.vision.LimelightHelpers.LimelightResults; import frc.spectrumLib.vision.LimelightHelpers.PoseEstimate; import frc.spectrumLib.vision.LimelightHelpers.RawFiducial; @@ -13,23 +13,59 @@ import lombok.Setter; import lombok.experimental.Accessors; +/** + * Provides a high-level interface to a single Limelight camera for AprilTag-based pose estimation + * and basic targeting. + * + *

All methods are safe to call when the camera is not attached ({@link #isAttached()} returns + * {@code false}); they return zero / false / empty values in that case. + * + *

Two pose estimation flavors are supported: + * + *

    + *
  • MegaTag1 — full 3-D pose estimation using one or more AprilTags. + *
  • MegaTag2 — fused estimate that incorporates the robot heading supplied via {@link + * #setRobotOrientation(double)}. + *
+ */ public class Limelight { /* Limelight Configuration */ + /** + * Configuration for a single Limelight camera, including its network-table name and physical + * mounting position on the robot. + * + *

The Lombok {@code @Accessors(chain = true)} annotation allows fluent setter calls: {@code + * config.setName("limelight").setAttached(true)}. + */ @Accessors(chain = true) public static class LimelightConfig { /** Must match to the name given in LL dashboard */ @Getter @Setter private String name; + /** Whether this camera is physically connected to the robot. */ @Getter @Setter private boolean attached = true; + /** + * Whether pose measurements from this camera are currently being fused into the estimator. + */ @Getter @Setter private boolean isIntegrating; + /** Physical Config */ + /** + * Forward offset of the camera from the robot center in meters (positive = toward front). + */ @Getter private double forward, right, up; // meters + /** Orientation of the camera in degrees (roll/pitch/yaw relative to robot frame). */ @Getter private double roll, pitch, yaw; // degrees + /** + * Creates a configuration for the named Limelight camera. + * + * @param name the network-table name assigned to the camera in the LL dashboard + */ public LimelightConfig(String name) { this.name = name; } @@ -66,33 +102,76 @@ public LimelightConfig withRotation(double roll, double pitch, double yaw) { } /* Debug */ + /** Formatter used for printing pose coordinates to SmartDashboard. */ private final DecimalFormat df = new DecimalFormat(); + + /** Active configuration for this Limelight instance. */ private LimelightConfig config; + + /** Whether pose measurements from this camera are currently being integrated. */ @Getter @Setter private boolean isIntegrating = false; + + /** Network-table name of this camera (mirrors {@link LimelightConfig#getName()}). */ @Getter private String cameraName = "default"; + + /** Human-readable string describing the current integration status, logged for diagnostics. */ @Getter @Setter private String logStatus = ""; + + /** Human-readable string describing the currently visible tag(s), logged for diagnostics. */ @Getter @Setter private String tagStatus = ""; + /** + * Constructs a Limelight wrapper from a fully populated {@link LimelightConfig}. + * + * @param config the camera configuration + */ public Limelight(LimelightConfig config) { this.config = config; + cameraName = config.getName(); } + /** + * Constructs a Limelight wrapper with a default configuration for the given camera name. + * + * @param name the network-table name of the camera + */ public Limelight(String name) { cameraName = name; config = new LimelightConfig(name); } + /** + * Constructs a Limelight wrapper, explicitly setting whether the camera is attached. + * + * @param name the network-table name of the camera + * @param attached {@code true} if the camera is physically present on the robot + */ public Limelight(String name, boolean attached) { cameraName = name; config = new LimelightConfig(name).setAttached(attached); } + /** + * Constructs a Limelight wrapper and immediately sets its active pipeline. + * + * @param name the network-table name of the camera + * @param pipeline the pipeline index to activate (see {@link + * frc.robot.subsystems.vision.Vision.VisionConfig}) + */ public Limelight(String name, int pipeline) { this(name); cameraName = name; setLimelightPipeline(pipeline); } + /** + * Constructs a Limelight wrapper with an explicit configuration and immediately sets its active + * pipeline. + * + * @param name the network-table name of the camera + * @param pipeline the pipeline index to activate + * @param config the fully populated {@link LimelightConfig} to use + */ public Limelight(String name, int pipeline, LimelightConfig config) { this(name); cameraName = name; @@ -100,10 +179,20 @@ public Limelight(String name, int pipeline, LimelightConfig config) { setLimelightPipeline(pipeline); } + /** + * Returns the network-table name of this camera. + * + * @return the camera name as configured in the LL dashboard + */ public String getName() { return config.getName(); } + /** + * Returns whether this camera is physically connected to the robot. + * + * @return {@code true} if attached + */ public boolean isAttached() { return config.isAttached(); } @@ -159,11 +248,21 @@ public boolean multipleTagsInView() { return getTagCountInView() > 1; } + /** + * Returns the number of AprilTags included in the current MegaTag1 pose estimate. + * + * @return number of tags contributing to the current pose estimate, or {@code 0} if not + * attached or no estimate is available + */ public double getTagCountInView() { if (!isAttached()) { return 0; } - return LimelightHelpers.getBotPoseEstimate_wpiBlue(config.getName()).tagCount; + PoseEstimate est = LimelightHelpers.getBotPoseEstimate_wpiBlue(config.getName()); + if (est == null) { + return 0; + } + return est.tagCount; // if (retrieveJSON() == null) return 0; @@ -183,6 +282,11 @@ public double getClosestTagID() { return LimelightHelpers.getFiducialID(config.getName()); } + /** + * Returns the area of the primary target as a percentage of the camera image (0–100). + * + * @return target area percentage, or {@code 0} if not attached + */ public double getTargetSize() { if (!isAttached()) { return 0; @@ -225,6 +329,12 @@ public Pose2d getMegaTag2_Pose2d() { return poseEstimate.pose; } + /** + * Returns the full MegaTag1 {@link PoseEstimate}, including timestamp, tag count, and raw + * fiducials. Returns an empty estimate when not attached or when no estimate is available. + * + * @return the MegaTag1 pose estimate in the WPILib Blue origin frame + */ public PoseEstimate getMegaTag1_PoseEstimate() { if (!isAttached()) { return new PoseEstimate(); @@ -237,6 +347,12 @@ public PoseEstimate getMegaTag1_PoseEstimate() { return poseEstimate; } + /** + * Returns the full MegaTag2 {@link PoseEstimate} (heading-fused), including timestamp and tag + * count. Returns an empty estimate when not attached or when no estimate is available. + * + * @return the MegaTag2 pose estimate in the WPILib Blue origin frame + */ public PoseEstimate getMegaTag2_PoseEstimate() { if (!isAttached()) { return new PoseEstimate(); @@ -250,6 +366,12 @@ public PoseEstimate getMegaTag2_PoseEstimate() { return poseEstimate; } + /** + * Returns {@code true} when the pose estimate is considered accurate — i.e., multiple tags are + * visible and the combined target area exceeds a minimum threshold. + * + * @return {@code true} if the pose estimate meets the accuracy criteria + */ public boolean hasAccuratePose() { if (!isAttached()) { return false; @@ -271,8 +393,18 @@ public double getDistanceToTagFromCamera() { return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); } + /** + * Returns the raw fiducial data for all currently detected AprilTags. + * + * @return array of {@link RawFiducial} entries from the MegaTag1 estimate; empty array if not + * attached or no estimate is available + */ public RawFiducial[] getRawFiducial() { - return LimelightHelpers.getBotPoseEstimate_wpiBlue(config.name).rawFiducials; + PoseEstimate est = LimelightHelpers.getBotPoseEstimate_wpiBlue(config.name); + if (est == null) { + return new RawFiducial[0]; + } + return est.rawFiducials; } /** @@ -284,7 +416,11 @@ public double getMegaTag1PoseTimestamp() { if (!isAttached()) { return 0; } - return LimelightHelpers.getBotPoseEstimate_wpiBlue(config.getName()).timestampSeconds; + PoseEstimate est = LimelightHelpers.getBotPoseEstimate_wpiBlue(config.getName()); + if (est == null) { + return 0; + } + return est.timestampSeconds; } /** @@ -296,8 +432,11 @@ public double getMegaTag2PoseTimestamp() { if (!isAttached()) { return 0; } - return LimelightHelpers.getBotPoseEstimate_wpiBlue_MegaTag2(config.getName()) - .timestampSeconds; + PoseEstimate est = LimelightHelpers.getBotPoseEstimate_wpiBlue_MegaTag2(config.getName()); + if (est == null) { + return 0; + } + return est.timestampSeconds; } /** @@ -329,15 +468,25 @@ public double getDistanceToTarget(double targetHeight) { return 0; } return (targetHeight - config.up) - / Math.tan(Units.degreesToRadians(config.roll + getVerticalOffset())); + / Math.tan(Units.degreesToRadians(config.pitch + getVerticalOffset())); } + /** + * Marks this camera as actively integrating and updates the log status message. + * + * @param message a human-readable description of why integration is valid + */ public void sendValidStatus(String message) { config.isIntegrating = true; this.isIntegrating = config.isIntegrating; logStatus = message; } + /** + * Marks this camera as not integrating and updates the log status message. + * + * @param message a human-readable description of why integration is invalid + */ public void sendInvalidStatus(String message) { config.isIntegrating = false; this.isIntegrating = config.isIntegrating; @@ -376,6 +525,12 @@ public void setRobotOrientation(double degrees) { LimelightHelpers.SetRobotOrientation(config.name, degrees, 0, 0, 0, 0, 0); } + /** + * Sets the robot orientation and yaw rate for the Limelight's internal IMU fusion (MegaTag2). + * + * @param degrees robot heading in degrees (positive counter-clockwise) + * @param angularRate current yaw rate in degrees per second + */ public void setRobotOrientation(double degrees, double angularRate) { if (!isAttached()) { return; @@ -383,6 +538,11 @@ public void setRobotOrientation(double degrees, double angularRate) { LimelightHelpers.SetRobotOrientation(config.name, degrees, angularRate, 0, 0, 0, 0); } + /** + * Sets the IMU mode on the Limelight. + * + * @param mode the IMU mode index (refer to the LL documentation for valid values) + */ public void setIMUmode(int mode) { if (!isAttached()) { return; @@ -390,6 +550,13 @@ public void setIMUmode(int mode) { LimelightHelpers.SetIMUMode(config.name, mode); } + /** + * Returns the X offset of the primary target in robot space (meters along the robot's + * left/right axis). + * + * @return target X translation in meters, or {@code -99999} if not attached or no target in + * view + */ public double getTagTx() { if (!isAttached()) { return -99999; @@ -404,6 +571,11 @@ public double getTagTx() { return tx; } + /** + * Returns the area of the primary target as a percentage of the camera image. + * + * @return target area (0–100 %), or {@code -99999} if not attached or no target in view + */ public double getTagTA() { if (!isAttached()) { return -99999; @@ -417,6 +589,13 @@ public double getTagTA() { return ta; } + /** + * Returns the Z-axis rotation of the primary target in robot space (yaw in radians, converted + * to degrees). + * + * @return target yaw rotation in degrees, or {@code -99999} if not attached or no target in + * view + */ public double getTagRotationDegrees() { if (!isAttached()) { return -99999; @@ -425,10 +604,10 @@ public double getTagRotationDegrees() { return -99999; } - double rotation = + double rotationRadians = LimelightHelpers.getTargetPose3d_RobotSpace(cameraName).getRotation().getZ(); - return rotation; + return Math.toDegrees(rotationRadians); } /** diff --git a/src/main/java/frc/spectrumLib/vision/VisionLogger.java b/src/main/java/frc/spectrumLib/vision/VisionLogger.java index 6357b9da..e8007451 100644 --- a/src/main/java/frc/spectrumLib/vision/VisionLogger.java +++ b/src/main/java/frc/spectrumLib/vision/VisionLogger.java @@ -1,67 +1,120 @@ package frc.spectrumLib.vision; import edu.wpi.first.math.geometry.Pose2d; -import frc.spectrumLib.Telemetry; +import frc.spectrumLib.telemetry.Telemetry; import lombok.Getter; +/** + * Logs telemetry data from a {@link Limelight} camera to the robot's data-logging system (DogLog) + * under the {@code Vision//} namespace. + * + *

Each method reads the corresponding value from the camera, forwards it to {@link + * frc.spectrumLib.telemetry.Telemetry#log}, and also returns the value so callers can use it + * directly without a second camera query. + */ public class VisionLogger { - /** Tracks position with Limelight using current logger (DogLog) to record data */ + /** The Limelight camera whose data is being logged. */ private final Limelight limelight; + /** Namespace prefix used in all telemetry keys ({@code Vision//...}). */ @Getter private String name; + /** + * Constructs a logger for the given Limelight camera. + * + * @param name the namespace prefix used in telemetry keys + * @param limelight the camera to read from + */ public VisionLogger(String name, Limelight limelight) { this.limelight = limelight; this.name = name; } + /** + * Logs and returns whether the camera is currently connected. + * + * @return {@code true} if the camera is reachable over the network + */ public boolean getCameraConnection() { - Telemetry.log("Vision " + name + " ConnectionStatus", limelight.isCameraConnected()); - return limelight.isCameraConnected(); + boolean connected = limelight.isCameraConnected(); + Telemetry.log("Vision/" + name + "/ConnectionStatus", connected); + return connected; } - public boolean getIntegratingStatus() { // Vision/Integrating - Telemetry.log("Vision " + name + " IntegratingStatus", limelight.isIntegrating()); - return limelight.isIntegrating(); + /** + * Logs and returns whether pose measurements are currently being fused into the estimator. + * + * @return {@code true} if the camera is actively integrating + */ + public boolean getIntegratingStatus() { + boolean integrating = limelight.isIntegrating(); + Telemetry.log("Vision/" + name + "/IntegratingStatus", integrating); + return integrating; } + /** + * Logs and returns the camera's human-readable integration status message. + * + * @return the current log status string from the camera + */ public String getLogStatus() { - Telemetry.log("Vision " + name + " LogStatus", limelight.getLogStatus()); - return limelight.getLogStatus(); + String status = limelight.getLogStatus(); + Telemetry.log("Vision/" + name + "/LogStatus", status); + return status; } + /** + * Logs and returns the camera's human-readable tag-detection status message. + * + * @return the current tag status string from the camera + */ public String getTagStatus() { - Telemetry.log("Vision " + name + " TagStatus", limelight.getTagStatus()); - return limelight.getLogStatus(); + String status = limelight.getTagStatus(); + Telemetry.log("Vision/" + name + "/TagStatus", status); + return status; } + /** + * Logs and returns the robot's 2-D pose derived from the MegaTag1 estimate. + * + * @return the MegaTag1 pose projected to 2-D in the WPILib Blue origin frame + */ public Pose2d getPose() { - Telemetry.log("Vision " + name + " Pose", limelight.getMegaTag1_Pose3d().toPose2d()); - return limelight.getMegaTag1_Pose3d().toPose2d(); + Pose2d pose = limelight.getMegaTag1_Pose3d().toPose2d(); + Telemetry.log("Vision/" + name + "/MT1Pose", pose); + return pose; } + /** + * Logs and returns the robot's 2-D pose from the MegaTag2 (heading-fused) estimate. + * + * @return the MegaTag2 {@link Pose2d} in the WPILib Blue origin frame + */ public Pose2d getMegaPose() { - Telemetry.log("Vision " + name + " MegaPose", limelight.getMegaTag2_Pose2d()); - return limelight.getMegaTag2_Pose2d(); - } - - public double getPoseX() { - Telemetry.log("Vision " + name + " PoseX", getPose().getX()); - return getPose().getX(); - } - - public double getPoseY() { - Telemetry.log("Vision " + name + " PoseY", getPose().getY()); - return getPose().getY(); + Pose2d pose = limelight.getMegaTag2_Pose2d(); + Telemetry.log("Vision/" + name + "/MT2Pose", pose); + return pose; } + /** + * Logs and returns the number of AprilTags contributing to the current pose estimate. + * + * @return the tag count from the MegaTag1 estimate + */ public double getTagCount() { - Telemetry.log("Vision " + name + " TagCount", limelight.getTagCountInView()); - return limelight.getTagCountInView(); + double count = limelight.getTagCountInView(); + Telemetry.log("Vision/" + name + "/TagCount", count); + return count; } + /** + * Logs and returns the area of the primary target as a percentage of the camera image. + * + * @return target area (0–100 %) + */ public double getTargetSize() { - Telemetry.log("Vision " + name + " TargetSize", limelight.getTargetSize()); - return limelight.getTargetSize(); + double size = limelight.getTargetSize(); + Telemetry.log("Vision/" + name + "/TargetSize", size); + return size; } } diff --git a/vendordeps/Phoenix6-26.1.3.json b/vendordeps/Phoenix6-26.3.0.json similarity index 92% rename from vendordeps/Phoenix6-26.1.3.json rename to vendordeps/Phoenix6-26.3.0.json index d5bc4a24..0567ab33 100644 --- a/vendordeps/Phoenix6-26.1.3.json +++ b/vendordeps/Phoenix6-26.3.0.json @@ -1,7 +1,7 @@ { - "fileName": "Phoenix6-26.1.3.json", + "fileName": "Phoenix6-26.3.0.json", "name": "CTRE-Phoenix (v6)", - "version": "26.1.3", + "version": "26.3.0", "frcYear": "2026", "uuid": "e995de00-2c64-4df5-8831-c1441420ff19", "mavenUrls": [ @@ -19,14 +19,14 @@ { "groupId": "com.ctre.phoenix6", "artifactId": "wpiapi-java", - "version": "26.1.3" + "version": "26.3.0" } ], "jniDependencies": [ { "groupId": "com.ctre.phoenix6", "artifactId": "api-cpp", - "version": "26.1.3", + "version": "26.3.0", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -40,7 +40,7 @@ { "groupId": "com.ctre.phoenix6", "artifactId": "tools", - "version": "26.1.3", + "version": "26.3.0", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -54,7 +54,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "api-cpp-sim", - "version": "26.1.3", + "version": "26.3.0", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -68,7 +68,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "tools-sim", - "version": "26.1.3", + "version": "26.3.0", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -82,7 +82,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simTalonSRX", - "version": "26.1.3", + "version": "26.3.0", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -96,7 +96,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simVictorSPX", - "version": "26.1.3", + "version": "26.3.0", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -110,7 +110,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simPigeonIMU", - "version": "26.1.3", + "version": "26.3.0", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -124,7 +124,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProTalonFX", - "version": "26.1.3", + "version": "26.3.0", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -138,7 +138,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProTalonFXS", - "version": "26.1.3", + "version": "26.3.0", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -152,7 +152,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProCANcoder", - "version": "26.1.3", + "version": "26.3.0", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -166,7 +166,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProPigeon2", - "version": "26.1.3", + "version": "26.3.0", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -180,7 +180,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProCANrange", - "version": "26.1.3", + "version": "26.3.0", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -194,7 +194,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProCANdi", - "version": "26.1.3", + "version": "26.3.0", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -208,7 +208,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProCANdle", - "version": "26.1.3", + "version": "26.3.0", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -224,7 +224,7 @@ { "groupId": "com.ctre.phoenix6", "artifactId": "wpiapi-cpp", - "version": "26.1.3", + "version": "26.3.0", "libName": "CTRE_Phoenix6_WPI", "headerClassifier": "headers", "sharedLibrary": true, @@ -240,7 +240,7 @@ { "groupId": "com.ctre.phoenix6", "artifactId": "tools", - "version": "26.1.3", + "version": "26.3.0", "libName": "CTRE_PhoenixTools", "headerClassifier": "headers", "sharedLibrary": true, @@ -256,7 +256,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "wpiapi-cpp-sim", - "version": "26.1.3", + "version": "26.3.0", "libName": "CTRE_Phoenix6_WPISim", "headerClassifier": "headers", "sharedLibrary": true, @@ -272,7 +272,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "tools-sim", - "version": "26.1.3", + "version": "26.3.0", "libName": "CTRE_PhoenixTools_Sim", "headerClassifier": "headers", "sharedLibrary": true, @@ -288,7 +288,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simTalonSRX", - "version": "26.1.3", + "version": "26.3.0", "libName": "CTRE_SimTalonSRX", "headerClassifier": "headers", "sharedLibrary": true, @@ -304,7 +304,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simVictorSPX", - "version": "26.1.3", + "version": "26.3.0", "libName": "CTRE_SimVictorSPX", "headerClassifier": "headers", "sharedLibrary": true, @@ -320,7 +320,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simPigeonIMU", - "version": "26.1.3", + "version": "26.3.0", "libName": "CTRE_SimPigeonIMU", "headerClassifier": "headers", "sharedLibrary": true, @@ -336,7 +336,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProTalonFX", - "version": "26.1.3", + "version": "26.3.0", "libName": "CTRE_SimProTalonFX", "headerClassifier": "headers", "sharedLibrary": true, @@ -352,7 +352,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProTalonFXS", - "version": "26.1.3", + "version": "26.3.0", "libName": "CTRE_SimProTalonFXS", "headerClassifier": "headers", "sharedLibrary": true, @@ -368,7 +368,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProCANcoder", - "version": "26.1.3", + "version": "26.3.0", "libName": "CTRE_SimProCANcoder", "headerClassifier": "headers", "sharedLibrary": true, @@ -384,7 +384,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProPigeon2", - "version": "26.1.3", + "version": "26.3.0", "libName": "CTRE_SimProPigeon2", "headerClassifier": "headers", "sharedLibrary": true, @@ -400,7 +400,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProCANrange", - "version": "26.1.3", + "version": "26.3.0", "libName": "CTRE_SimProCANrange", "headerClassifier": "headers", "sharedLibrary": true, @@ -416,7 +416,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProCANdi", - "version": "26.1.3", + "version": "26.3.0", "libName": "CTRE_SimProCANdi", "headerClassifier": "headers", "sharedLibrary": true, @@ -432,7 +432,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProCANdle", - "version": "26.1.3", + "version": "26.3.0", "libName": "CTRE_SimProCANdle", "headerClassifier": "headers", "sharedLibrary": true, diff --git a/vendordeps/photonlib.json b/vendordeps/photonlib.json deleted file mode 100644 index 6d1e8174..00000000 --- a/vendordeps/photonlib.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "fileName": "photonlib.json", - "name": "photonlib", - "version": "v2026.3.4", - "uuid": "515fe07e-bfc6-11fa-b3de-0242ac130004", - "frcYear": "2026", - "mavenUrls": [ - "https://maven.photonvision.org/repository/internal", - "https://maven.photonvision.org/repository/snapshots" - ], - "jsonUrl": "https://maven.photonvision.org/repository/internal/org/photonvision/photonlib-json/1.0/photonlib-json-1.0.json", - "jniDependencies": [ - { - "groupId": "org.photonvision", - "artifactId": "photontargeting-cpp", - "version": "v2026.3.4", - "skipInvalidPlatforms": true, - "isJar": false, - "validPlatforms": [ - "windowsx86-64", - "linuxathena", - "linuxx86-64", - "osxuniversal" - ] - } - ], - "cppDependencies": [ - { - "groupId": "org.photonvision", - "artifactId": "photonlib-cpp", - "version": "v2026.3.4", - "libName": "photonlib", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxathena", - "linuxx86-64", - "osxuniversal" - ] - }, - { - "groupId": "org.photonvision", - "artifactId": "photontargeting-cpp", - "version": "v2026.3.4", - "libName": "photontargeting", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxathena", - "linuxx86-64", - "osxuniversal" - ] - } - ], - "javaDependencies": [ - { - "groupId": "org.photonvision", - "artifactId": "photonlib-java", - "version": "v2026.3.4" - }, - { - "groupId": "org.photonvision", - "artifactId": "photontargeting-java", - "version": "v2026.3.4" - } - ] -} \ No newline at end of file